Passed
Push — master ( 34e8da...f497d2 )
by
unknown
06:10 queued 02:50
created
lib/grommunio/grommunio.php 3 patches
Indentation   +2928 added lines, -2928 removed lines patch added patch discarded remove patch
@@ -21,2359 +21,2359 @@  discard block
 block discarded – undo
21 21
 setlocale(LC_CTYPE, "en_US.UTF-8");
22 22
 
23 23
 class Grommunio extends InterProcessData implements IBackend, ISearchProvider, IStateMachine {
24
-	private $mainUser;
25
-	private $session;
26
-	private $defaultstore;
27
-	private $store;
28
-	private $storeName;
29
-	private $storeCache;
30
-	private $notifications;
31
-	private $changesSink;
32
-	private $changesSinkFolders;
33
-	private $changesSinkHierarchyHash;
34
-	private $changesSinkStores;
35
-	private $wastebasket;
36
-	private $addressbook;
37
-	private $folderStatCache;
38
-	private $impersonateUser;
39
-	private $stateFolder;
40
-	private $userDeviceData;
41
-
42
-	// KC config parameter for PR_EC_ENABLED_FEATURES / PR_EC_DISABLED_FEATURES
43
-	public const MOBILE_ENABLED = 'mobile';
44
-
45
-	public const MAXAMBIGUOUSRECIPIENTS = 9999;
46
-	public const FREEBUSYENUMBLOCKS = 50;
47
-	public const MAXFREEBUSYSLOTS = 32767; // max length of 32k for the MergedFreeBusy element is allowed
48
-	public const HALFHOURSECONDS = 1800;
49
-
50
-	/**
51
-	 * Constructor of the grommunio Backend.
52
-	 */
53
-	public function __construct() {
54
-		$this->session = false;
55
-		$this->store = false;
56
-		$this->storeName = false;
57
-		$this->storeCache = [];
58
-		$this->notifications = false;
59
-		$this->changesSink = false;
60
-		$this->changesSinkFolders = [];
61
-		$this->changesSinkStores = [];
62
-		$this->changesSinkHierarchyHash = false;
63
-		$this->wastebasket = false;
64
-		$this->session = false;
65
-		$this->folderStatCache = [];
66
-		$this->impersonateUser = false;
67
-		$this->stateFolder = null;
68
-
69
-		SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio using PHP-MAPI version: %s - PHP version: %s", phpversion("mapi"), phpversion()));
70
-
71
-		# Interprocessdata
72
-		$this->allocate = 0;
73
-		$this->type = "grommunio-sync:userdevices";
74
-		$this->userDeviceData = "grommunio-sync:statefoldercache";
75
-		parent::__construct();
76
-	}
77
-
78
-	/**
79
-	 * Indicates which StateMachine should be used.
80
-	 *
81
-	 * @return bool Grommunio uses own state machine
82
-	 */
83
-	public function GetStateMachine() {
84
-		return $this;
85
-	}
86
-
87
-	/**
88
-	 * Returns the Grommunio as it implements the ISearchProvider interface
89
-	 * This could be overwritten by the global configuration.
90
-	 *
91
-	 * @return object Implementation of ISearchProvider
92
-	 */
93
-	public function GetSearchProvider() {
94
-		return $this;
95
-	}
96
-
97
-	/**
98
-	 * Indicates which AS version is supported by the backend.
99
-	 *
100
-	 * @return string AS version constant
101
-	 */
102
-	public function GetSupportedASVersion() {
103
-		return GSync::ASV_141;
104
-	}
105
-
106
-	/**
107
-	 * Authenticates the user with the configured grommunio server.
108
-	 *
109
-	 * @param string $username
110
-	 * @param string $domain
111
-	 * @param string $password
112
-	 * @param mixed  $user
113
-	 * @param mixed  $pass
114
-	 *
115
-	 * @throws AuthenticationRequiredException
116
-	 *
117
-	 * @return bool
118
-	 */
119
-	public function Logon($user, $domain, $pass) {
120
-		SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->Logon(): Trying to authenticate user '%s'..", $user));
121
-
122
-		$this->mainUser = strtolower($user);
123
-		// TODO the impersonated user should be passed directly to IBackend->Logon() - ZP-1351
124
-		if (Request::GetImpersonatedUser()) {
125
-			$this->impersonateUser = strtolower(Request::GetImpersonatedUser());
126
-		}
127
-
128
-		// check if we are impersonating someone
129
-		// $defaultUser will be used for $this->defaultStore
130
-		if ($this->impersonateUser !== false) {
131
-			SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->Logon(): Impersonation active - authenticating: '%s' - impersonating '%s'", $this->mainUser, $this->impersonateUser));
132
-			$defaultUser = $this->impersonateUser;
133
-		}
134
-		else {
135
-			$defaultUser = $this->mainUser;
136
-		}
137
-
138
-		$deviceId = Request::GetDeviceID();
139
-
140
-		try {
141
-			// check if notifications are available in php-mapi
142
-			if (function_exists('mapi_feature') && mapi_feature('LOGONFLAGS')) {
143
-				// send grommunio-sync version and user agent to ZCP - ZP-589
144
-				if (Utils::CheckMapiExtVersion('7.2.0')) {
145
-					$gsync_version = 'Grommunio-Sync_' . @constant('GROMMUNIOSYNC_VERSION');
146
-					$user_agent = ($deviceId) ? GSync::GetDeviceManager()->GetUserAgent() : "unknown";
147
-					$this->session = @mapi_logon_zarafa($this->mainUser, $pass, MAPI_SERVER, null, null, 0, $gsync_version, $user_agent);
148
-				}
149
-				else {
150
-					$this->session = @mapi_logon_zarafa($this->mainUser, $pass, MAPI_SERVER, null, null, 0);
151
-				}
152
-				$this->notifications = true;
153
-			}
154
-			// old fashioned session
155
-			else {
156
-				$this->session = @mapi_logon_zarafa($this->mainUser, $pass, MAPI_SERVER);
157
-				$this->notifications = false;
158
-			}
159
-
160
-			if (mapi_last_hresult()) {
161
-				SLog::Write(LOGLEVEL_ERROR, sprintf("Grommunio->Logon(): login failed with error code: 0x%X", mapi_last_hresult()));
162
-				if (mapi_last_hresult() == MAPI_E_NETWORK_ERROR) {
163
-					throw new ServiceUnavailableException("Error connecting to KC (login)");
164
-				}
165
-			}
166
-		}
167
-		catch (MAPIException $ex) {
168
-			throw new AuthenticationRequiredException($ex->getDisplayMessage());
169
-		}
170
-
171
-		if (!$this->session) {
172
-			SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->Logon(): logon failed for user '%s'", $this->mainUser));
173
-			$this->defaultstore = false;
174
-
175
-			return false;
176
-		}
177
-
178
-		// Get/open default store
179
-		$this->defaultstore = $this->openMessageStore($this->mainUser);
180
-
181
-		// To impersonate, we overwrite the defaultstore. We still need to open it before we can do that.
182
-		if ($this->impersonateUser) {
183
-			SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->Logon(): Impersonating user '%s'", $defaultUser));
184
-			$this->defaultstore = $this->openMessageStore($defaultUser);
185
-		}
186
-
187
-		if (mapi_last_hresult() == MAPI_E_FAILONEPROVIDER) {
188
-			throw new ServiceUnavailableException("Error connecting to KC (open store)");
189
-		}
190
-
191
-		if ($this->defaultstore === false) {
192
-			throw new AuthenticationRequiredException(sprintf("Grommunio->Logon(): User '%s' has no default store", $defaultUser));
193
-		}
194
-
195
-		$this->store = $this->defaultstore;
196
-		$this->storeName = $defaultUser;
197
-
198
-		SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->Logon(): User '%s' is authenticated%s", $this->mainUser, ($this->impersonateUser ? " impersonating '" . $this->impersonateUser . "'" : '')));
199
-
200
-		$this->isGSyncEnabled();
201
-
202
-		// check if this is a Zarafa 7 store with unicode support
203
-		MAPIUtils::IsUnicodeStore($this->store);
204
-
205
-		// open the state folder
206
-		$this->getStateFolder($deviceId);
207
-
208
-		return true;
209
-	}
210
-
211
-	/**
212
-	 * Setup the backend to work on a specific store or checks ACLs there.
213
-	 * If only the $store is submitted, all Import/Export/Fetch/Etc operations should be
214
-	 * performed on this store (switch operations store).
215
-	 * If the ACL check is enabled, this operation should just indicate the ACL status on
216
-	 * the submitted store, without changing the store for operations.
217
-	 * For the ACL status, the currently logged on user MUST have access rights on
218
-	 *  - the entire store - admin access if no folderid is sent, or
219
-	 *  - on a specific folderid in the store (secretary/full access rights).
220
-	 *
221
-	 * The ACLcheck MUST fail if a folder of the authenticated user is checked!
222
-	 *
223
-	 * @param string $store        target store, could contain a "domain\user" value
224
-	 * @param bool   $checkACLonly if set to true, Setup() should just check ACLs
225
-	 * @param string $folderid     if set, only ACLs on this folderid are relevant
226
-	 *
227
-	 * @return bool
228
-	 */
229
-	public function Setup($store, $checkACLonly = false, $folderid = false) {
230
-		list($user, $domain) = Utils::SplitDomainUser($store);
231
-
232
-		if (!isset($this->mainUser)) {
233
-			return false;
234
-		}
235
-
236
-		$mainUser = $this->mainUser;
237
-		// when impersonating we need to check against the impersonated user
238
-		if ($this->impersonateUser) {
239
-			$mainUser = $this->impersonateUser;
240
-		}
241
-
242
-		if ($user === false) {
243
-			$user = $mainUser;
244
-		}
245
-
246
-		// This is a special case. A user will get his entire folder structure by the foldersync by default.
247
-		// The ACL check is executed when an additional folder is going to be sent to the mobile.
248
-		// Configured that way the user could receive the same folderid twice, with two different names.
249
-		if ($mainUser == $user && $checkACLonly && $folderid && !$this->impersonateUser) {
250
-			SLog::Write(LOGLEVEL_DEBUG, "Grommunio->Setup(): Checking ACLs for folder of the users defaultstore. Fail is forced to avoid folder duplications on mobile.");
251
-
252
-			return false;
253
-		}
254
-
255
-		// get the users store
256
-		$userstore = $this->openMessageStore($user);
257
-
258
-		// only proceed if a store was found, else return false
259
-		if ($userstore) {
260
-			// only check permissions
261
-			if ($checkACLonly == true) {
262
-				// check for admin rights
263
-				if (!$folderid) {
264
-					if ($user != $this->mainUser) {
265
-						if ($this->impersonateUser) {
266
-							$storeProps = mapi_getprops($userstore, [PR_IPM_SUBTREE_ENTRYID]);
267
-							$rights = $this->HasSecretaryACLs($userstore, '', $storeProps[PR_IPM_SUBTREE_ENTRYID]);
268
-							SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->Setup(): Checking for secretary ACLs on root folder of impersonated store '%s': '%s'", $user, Utils::PrintAsString($rights)));
269
-						}
270
-						else {
271
-							$zarafauserinfo = @nsp_getuserinfo($this->mainUser);
272
-							$rights = (isset($zarafauserinfo['admin']) && $zarafauserinfo['admin']) ? true : false;
273
-							SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->Setup(): Checking for admin ACLs on store '%s': '%s'", $user, Utils::PrintAsString($rights)));
274
-						}
275
-					}
276
-					// the user has always full access to his own store
277
-					else {
278
-						$rights = true;
279
-						SLog::Write(LOGLEVEL_DEBUG, "Grommunio->Setup(): the user has always full access to his own store");
280
-					}
281
-
282
-					return $rights;
283
-				}
284
-				// check permissions on this folder
285
-
286
-				$rights = $this->HasSecretaryACLs($userstore, $folderid);
287
-				SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->Setup(): Checking for secretary ACLs on '%s' of store '%s': '%s'", $folderid, $user, Utils::PrintAsString($rights)));
288
-
289
-				return $rights;
290
-			}
291
-
292
-			// switch operations store
293
-			// this should also be done if called with user = mainuser or user = false
294
-			// which means to switch back to the default store
295
-
296
-			// switch active store
297
-			$this->store = $userstore;
298
-			$this->storeName = $user;
299
-
300
-			return true;
301
-		}
302
-
303
-		return false;
304
-	}
305
-
306
-	/**
307
-	 * Logs off
308
-	 * Free/Busy information is updated for modified calendars
309
-	 * This is done after the synchronization process is completed.
310
-	 *
311
-	 * @return bool
312
-	 */
313
-	public function Logoff() {
314
-		return true;
315
-	}
316
-
317
-	/**
318
-	 * Returns an array of SyncFolder types with the entire folder hierarchy
319
-	 * on the server (the array itself is flat, but refers to parents via the 'parent' property.
320
-	 *
321
-	 * provides AS 1.0 compatibility
322
-	 *
323
-	 * @return array SYNC_FOLDER
324
-	 */
325
-	public function GetHierarchy() {
326
-		$folders = [];
327
-		$mapiprovider = new MAPIProvider($this->session, $this->store);
328
-		$storeProps = $mapiprovider->GetStoreProps();
329
-
330
-		// for SYSTEM user open the public folders
331
-		if (strtoupper($this->storeName) == "SYSTEM") {
332
-			$rootfolder = mapi_msgstore_openentry($this->store, $storeProps[PR_IPM_PUBLIC_FOLDERS_ENTRYID]);
333
-		}
334
-		else {
335
-			$rootfolder = mapi_msgstore_openentry($this->store);
336
-		}
337
-
338
-		$rootfolderprops = mapi_getprops($rootfolder, [PR_SOURCE_KEY]);
339
-
340
-		$hierarchy = mapi_folder_gethierarchytable($rootfolder, CONVENIENT_DEPTH);
341
-		$rows = mapi_table_queryallrows($hierarchy, [PR_DISPLAY_NAME, PR_PARENT_ENTRYID, PR_ENTRYID, PR_SOURCE_KEY, PR_PARENT_SOURCE_KEY, PR_CONTAINER_CLASS, PR_ATTR_HIDDEN, PR_EXTENDED_FOLDER_FLAGS, PR_FOLDER_TYPE]);
342
-		SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetHierarchy(): fetched %d folders from MAPI", count($rows)));
343
-
344
-		foreach ($rows as $row) {
345
-			// do not display hidden and search folders
346
-			if ((isset($row[PR_ATTR_HIDDEN]) && $row[PR_ATTR_HIDDEN]) ||
347
-				(isset($row[PR_FOLDER_TYPE]) && $row[PR_FOLDER_TYPE] == FOLDER_SEARCH) ||
348
-				// for SYSTEM user $row[PR_PARENT_SOURCE_KEY] == $rootfolderprops[PR_SOURCE_KEY] is true, but we need those folders
349
-				(isset($row[PR_PARENT_SOURCE_KEY]) && $row[PR_PARENT_SOURCE_KEY] == $rootfolderprops[PR_SOURCE_KEY] && strtoupper($this->storeName) != "SYSTEM")) {
350
-				SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetHierarchy(): ignoring folder '%s' as it's a hidden/search/root folder", (isset($row[PR_DISPLAY_NAME]) ? $row[PR_DISPLAY_NAME] : "unknown")));
351
-
352
-				continue;
353
-			}
354
-			$folder = $mapiprovider->GetFolder($row);
355
-			if ($folder) {
356
-				$folders[] = $folder;
357
-			}
358
-			else {
359
-				SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetHierarchy(): ignoring folder '%s' as MAPIProvider->GetFolder() did not return a SyncFolder object", (isset($row[PR_DISPLAY_NAME]) ? $row[PR_DISPLAY_NAME] : "unknown")));
360
-			}
361
-		}
362
-
363
-		SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetHierarchy(): processed %d folders, starting parent remap", count($folders)));
364
-		// reloop the folders to make sure all parentids are mapped correctly
365
-		$dm = GSync::GetDeviceManager();
366
-		foreach ($folders as $folder) {
367
-			if ($folder->parentid !== "0") {
368
-				// SYSTEM user's parentid points to $rootfolderprops[PR_SOURCE_KEY], but they need to be on the top level
369
-				$folder->parentid = (strtoupper($this->storeName) == "SYSTEM" && $folder->parentid == bin2hex($rootfolderprops[PR_SOURCE_KEY])) ? '0' : $dm->GetFolderIdForBackendId($folder->parentid);
370
-			}
371
-		}
372
-
373
-		return $folders;
374
-	}
375
-
376
-	/**
377
-	 * Returns the importer to process changes from the mobile
378
-	 * If no $folderid is given, hierarchy importer is expected.
379
-	 *
380
-	 * @param string $folderid (opt)
381
-	 *
382
-	 * @return object(ImportChanges)
383
-	 */
384
-	public function GetImporter($folderid = false) {
385
-		SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetImporter() folderid: '%s'", Utils::PrintAsString($folderid)));
386
-		if ($folderid !== false) {
387
-			// check if the user of the current store has permissions to import to this folderid
388
-			if ($this->storeName != $this->mainUser && !$this->hasSecretaryACLs($this->store, $folderid)) {
389
-				SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetImporter(): missing permissions on folderid: '%s'.", Utils::PrintAsString($folderid)));
390
-
391
-				return false;
392
-			}
393
-
394
-			return new ImportChangesICS($this->session, $this->store, hex2bin($folderid));
395
-		}
396
-
397
-		return new ImportChangesICS($this->session, $this->store);
398
-	}
399
-
400
-	/**
401
-	 * Returns the exporter to send changes to the mobile
402
-	 * If no $folderid is given, hierarchy exporter is expected.
403
-	 *
404
-	 * @param string $folderid (opt)
405
-	 *
406
-	 * @throws StatusException
407
-	 *
408
-	 * @return object(ExportChanges)
409
-	 */
410
-	public function GetExporter($folderid = false) {
411
-		if ($folderid !== false) {
412
-			// check if the user of the current store has permissions to export from this folderid
413
-			if ($this->storeName != $this->mainUser && !$this->hasSecretaryACLs($this->store, $folderid)) {
414
-				SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetExporter(): missing permissions on folderid: '%s'.", Utils::PrintAsString($folderid)));
415
-
416
-				return false;
417
-			}
418
-
419
-			return new ExportChangesICS($this->session, $this->store, hex2bin($folderid));
420
-		}
421
-
422
-		return new ExportChangesICS($this->session, $this->store);
423
-	}
424
-
425
-	/**
426
-	 * Sends an e-mail
427
-	 * This messages needs to be saved into the 'sent items' folder.
428
-	 *
429
-	 * @param SyncSendMail $sm SyncSendMail object
430
-	 *
431
-	 * @throws StatusException
432
-	 *
433
-	 * @return bool
434
-	 */
435
-	public function SendMail($sm) {
436
-		// Check if imtomapi function is available and use it to send the mime message.
437
-		// It is available since ZCP 7.0.6
438
-		// @see http://jira.zarafa.com/browse/ZCP-9508
439
-		if (!(function_exists('mapi_feature') && mapi_feature('INETMAPI_IMTOMAPI'))) {
440
-			throw new StatusException("Grommunio->SendMail(): ZCP/KC version is too old, INETMAPI_IMTOMAPI is not available. Install at least ZCP version 7.0.6 or later.", SYNC_COMMONSTATUS_MAILSUBMISSIONFAILED, null, LOGLEVEL_FATAL);
441
-
442
-			return false;
443
-		}
444
-		$mimeLength = strlen($sm->mime);
445
-		SLog::Write(LOGLEVEL_DEBUG, sprintf(
446
-			"Grommunio->SendMail(): RFC822: %d bytes  forward-id: '%s' reply-id: '%s' parent-id: '%s' SaveInSent: '%s' ReplaceMIME: '%s'",
447
-			$mimeLength,
448
-			Utils::PrintAsString($sm->forwardflag),
449
-			Utils::PrintAsString($sm->replyflag),
450
-			Utils::PrintAsString((isset($sm->source->folderid) ? $sm->source->folderid : false)),
451
-			Utils::PrintAsString(($sm->saveinsent)),
452
-			Utils::PrintAsString(isset($sm->replacemime))
453
-		));
454
-		if ($mimeLength == 0) {
455
-			throw new StatusException("Grommunio->SendMail(): empty mail data", SYNC_COMMONSTATUS_MAILSUBMISSIONFAILED);
456
-		}
457
-
458
-		$sendMailProps = MAPIMapping::GetSendMailProperties();
459
-		$sendMailProps = getPropIdsFromStrings($this->defaultstore, $sendMailProps);
460
-
461
-		// Open the outbox and create the message there
462
-		$storeprops = mapi_getprops($this->defaultstore, [$sendMailProps["outboxentryid"], $sendMailProps["ipmsentmailentryid"]]);
463
-		if (isset($storeprops[$sendMailProps["outboxentryid"]])) {
464
-			$outbox = mapi_msgstore_openentry($this->defaultstore, $storeprops[$sendMailProps["outboxentryid"]]);
465
-		}
466
-
467
-		if (!$outbox) {
468
-			throw new StatusException(sprintf("Grommunio->SendMail(): No Outbox found or unable to create message: 0x%X", mapi_last_hresult()), SYNC_COMMONSTATUS_SERVERERROR);
469
-		}
470
-
471
-		$mapimessage = mapi_folder_createmessage($outbox);
472
-
473
-		// message properties to be set
474
-		$mapiprops = [];
475
-		// only save the outgoing in sent items folder if the mobile requests it
476
-		$mapiprops[$sendMailProps["sentmailentryid"]] = $storeprops[$sendMailProps["ipmsentmailentryid"]];
477
-
478
-		SLog::Write(LOGLEVEL_DEBUG, "Grommunio->SendMail(): Use the mapi_inetmapi_imtomapi function");
479
-		$ab = mapi_openaddressbook($this->session);
480
-		mapi_inetmapi_imtomapi($this->session, $this->defaultstore, $ab, $mapimessage, $sm->mime, []);
481
-
482
-		// Set the appSeqNr so that tracking tab can be updated for meeting request updates
483
-		// @see http://jira.zarafa.com/browse/ZP-68
484
-		$meetingRequestProps = MAPIMapping::GetMeetingRequestProperties();
485
-		$meetingRequestProps = getPropIdsFromStrings($this->defaultstore, $meetingRequestProps);
486
-		$props = mapi_getprops($mapimessage, [PR_MESSAGE_CLASS, $meetingRequestProps["goidtag"], $sendMailProps["internetcpid"], $sendMailProps["body"], $sendMailProps["html"], $sendMailProps["rtf"], $sendMailProps["rtfinsync"]]);
487
-
488
-		// Convert sent message's body to UTF-8 if it was a HTML message.
489
-		// @see http://jira.zarafa.com/browse/ZP-505 and http://jira.zarafa.com/browse/ZP-555
490
-		if (isset($props[$sendMailProps["internetcpid"]]) && $props[$sendMailProps["internetcpid"]] != INTERNET_CPID_UTF8 && MAPIUtils::GetNativeBodyType($props) == SYNC_BODYPREFERENCE_HTML) {
491
-			SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->SendMail(): Sent email cpid is not unicode (%d). Set it to unicode and convert email html body.", $props[$sendMailProps["internetcpid"]]));
492
-			$mapiprops[$sendMailProps["internetcpid"]] = INTERNET_CPID_UTF8;
493
-
494
-			$bodyHtml = MAPIUtils::readPropStream($mapimessage, PR_HTML);
495
-			$bodyHtml = Utils::ConvertCodepageStringToUtf8($props[$sendMailProps["internetcpid"]], $bodyHtml);
496
-			$mapiprops[$sendMailProps["html"]] = $bodyHtml;
497
-
498
-			mapi_setprops($mapimessage, $mapiprops);
499
-		}
500
-		if (stripos($props[PR_MESSAGE_CLASS], "IPM.Schedule.Meeting.Resp.") === 0) {
501
-			// search for calendar items using goid
502
-			$mr = new Meetingrequest($this->defaultstore, $mapimessage);
503
-			$appointments = $mr->findCalendarItems($props[$meetingRequestProps["goidtag"]]);
504
-			if (is_array($appointments) && !empty($appointments)) {
505
-				$app = mapi_msgstore_openentry($this->defaultstore, $appointments[0]);
506
-				$appprops = mapi_getprops($app, [$meetingRequestProps["appSeqNr"]]);
507
-				if (isset($appprops[$meetingRequestProps["appSeqNr"]]) && $appprops[$meetingRequestProps["appSeqNr"]]) {
508
-					$mapiprops[$meetingRequestProps["appSeqNr"]] = $appprops[$meetingRequestProps["appSeqNr"]];
509
-					SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->SendMail(): Set sequence number to:%d", $appprops[$meetingRequestProps["appSeqNr"]]));
510
-				}
511
-			}
512
-		}
513
-
514
-		// Delete the PR_SENT_REPRESENTING_* properties because some android devices
515
-		// do not send neither From nor Sender header causing empty PR_SENT_REPRESENTING_NAME and
516
-		// PR_SENT_REPRESENTING_EMAIL_ADDRESS properties and "broken" PR_SENT_REPRESENTING_ENTRYID
517
-		// which results in spooler not being able to send the message.
518
-		// @see http://jira.zarafa.com/browse/ZP-85
519
-		mapi_deleteprops(
520
-			$mapimessage,
521
-			[
522
-				$sendMailProps["sentrepresentingname"],
523
-				$sendMailProps["sentrepresentingemail"],
524
-				$sendMailProps["representingentryid"],
525
-				$sendMailProps["sentrepresentingaddt"],
526
-				$sendMailProps["sentrepresentinsrchk"],
527
-			]
528
-		);
529
-
530
-		if (isset($sm->source->itemid) && $sm->source->itemid) {
531
-			// answering an email in a public/shared folder
532
-			// TODO as the store is setup, we should actually user $this->store instead of $this->defaultstore - nevertheless we need to make sure this store is able to send mail (has an outbox)
533
-			if (!$this->Setup(GSync::GetAdditionalSyncFolderStore($sm->source->folderid))) {
534
-				throw new StatusException(sprintf("Grommunio->SendMail() could not Setup() the backend for folder id '%s'", $sm->source->folderid), SYNC_COMMONSTATUS_SERVERERROR);
535
-			}
536
-
537
-			$entryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($sm->source->folderid), hex2bin($sm->source->itemid));
538
-			if ($entryid) {
539
-				$fwmessage = mapi_msgstore_openentry($this->store, $entryid);
540
-			}
541
-
542
-			if (isset($fwmessage) && $fwmessage) {
543
-				// update icon and last_verb when forwarding or replying message
544
-				// reply-all (verb 103) is not supported, as we cannot really detect this case
545
-				if ($sm->forwardflag) {
546
-					$updateProps = [
547
-						PR_ICON_INDEX => 262,
548
-						PR_LAST_VERB_EXECUTED => 104,
549
-					];
550
-				}
551
-				elseif ($sm->replyflag) {
552
-					$updateProps = [
553
-						PR_ICON_INDEX => 261,
554
-						PR_LAST_VERB_EXECUTED => 102,
555
-					];
556
-				}
557
-				if (isset($updateProps)) {
558
-					$updateProps[PR_LAST_VERB_EXECUTION_TIME] = time();
559
-					mapi_setprops($fwmessage, $updateProps);
560
-					mapi_savechanges($fwmessage);
561
-				}
562
-
563
-				// only attach the original message if the mobile does not send it itself
564
-				if (!isset($sm->replacemime)) {
565
-					// get message's body in order to append forward or reply text
566
-					if (!isset($body)) {
567
-						$body = MAPIUtils::readPropStream($mapimessage, PR_BODY);
568
-					}
569
-					if (!isset($bodyHtml)) {
570
-						$bodyHtml = MAPIUtils::readPropStream($mapimessage, PR_HTML);
571
-					}
572
-					$cpid = mapi_getprops($fwmessage, [$sendMailProps["internetcpid"]]);
573
-					if ($sm->forwardflag) {
574
-						// attach the original attachments to the outgoing message
575
-						$this->copyAttachments($mapimessage, $fwmessage);
576
-					}
577
-
578
-					// regarding the conversion @see ZP-470
579
-					if (strlen($body) > 0) {
580
-						$fwbody = MAPIUtils::readPropStream($fwmessage, PR_BODY);
581
-						// if only the old message's cpid is set, convert from old charset to utf-8
582
-						if (isset($cpid[$sendMailProps["internetcpid"]]) && $cpid[$sendMailProps["internetcpid"]] != INTERNET_CPID_UTF8) {
583
-							SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->SendMail(): convert plain forwarded message charset (only fw set) from '%s' to '65001'", $cpid[$sendMailProps["internetcpid"]]));
584
-							$fwbody = Utils::ConvertCodepageStringToUtf8($cpid[$sendMailProps["internetcpid"]], $fwbody);
585
-						}
586
-						// otherwise to the general conversion
587
-						else {
588
-							SLog::Write(LOGLEVEL_DEBUG, "Grommunio->SendMail(): no charset conversion done for plain forwarded message");
589
-							$fwbody = w2u($fwbody);
590
-						}
591
-
592
-						$mapiprops[$sendMailProps["body"]] = $body . "\r\n\r\n" . $fwbody;
593
-					}
594
-
595
-					if (strlen($bodyHtml) > 0) {
596
-						$fwbodyHtml = MAPIUtils::readPropStream($fwmessage, PR_HTML);
597
-						// if only new message's cpid is set, convert to UTF-8
598
-						if (isset($cpid[$sendMailProps["internetcpid"]]) && $cpid[$sendMailProps["internetcpid"]] != INTERNET_CPID_UTF8) {
599
-							SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->SendMail(): convert html forwarded message charset (only fw set) from '%s' to '65001'", $cpid[$sendMailProps["internetcpid"]]));
600
-							$fwbodyHtml = Utils::ConvertCodepageStringToUtf8($cpid[$sendMailProps["internetcpid"]], $fwbodyHtml);
601
-						}
602
-						// otherwise to the general conversion
603
-						else {
604
-							SLog::Write(LOGLEVEL_DEBUG, "Grommunio->SendMail(): no charset conversion done for html forwarded message");
605
-							$fwbodyHtml = w2u($fwbodyHtml);
606
-						}
607
-
608
-						$mapiprops[$sendMailProps["html"]] = $bodyHtml . "<br><br>" . $fwbodyHtml;
609
-					}
610
-				}
611
-			}
612
-			else {
613
-				// no fwmessage could be opened and we need it because we do not replace mime
614
-				if (!isset($sm->replacemime) || $sm->replacemime == false) {
615
-					throw new StatusException(sprintf("Grommunio->SendMail(): Could not open message id '%s' in folder id '%s' to be replied/forwarded: 0x%X", $sm->source->itemid, $sm->source->folderid, mapi_last_hresult()), SYNC_COMMONSTATUS_ITEMNOTFOUND);
616
-				}
617
-			}
618
-		}
619
-
620
-		mapi_setprops($mapimessage, $mapiprops);
621
-		mapi_savechanges($mapimessage);
622
-		mapi_message_submitmessage($mapimessage);
623
-		$hr = mapi_last_hresult();
624
-
625
-		if ($hr) {
626
-			switch ($hr) {
627
-				case MAPI_E_STORE_FULL:
628
-					$code = SYNC_COMMONSTATUS_MAILBOXQUOTAEXCEEDED;
629
-					break;
630
-
631
-				default:
632
-					$code = SYNC_COMMONSTATUS_MAILSUBMISSIONFAILED;
633
-					break;
634
-			}
635
-
636
-			throw new StatusException(sprintf("Grommunio->SendMail(): Error saving/submitting the message to the Outbox: 0x%X", $hr), $code);
637
-		}
638
-
639
-		SLog::Write(LOGLEVEL_DEBUG, "Grommunio->SendMail(): email submitted");
640
-
641
-		return true;
642
-	}
643
-
644
-	/**
645
-	 * Returns all available data of a single message.
646
-	 *
647
-	 * @param string            $folderid
648
-	 * @param string            $id
649
-	 * @param ContentParameters $contentparameters flag
650
-	 *
651
-	 * @throws StatusException
652
-	 *
653
-	 * @return object(SyncObject)
654
-	 */
655
-	public function Fetch($folderid, $id, $contentparameters) {
656
-		// SEARCH fetches with folderid == false and PR_ENTRYID as ID
657
-		if (!$folderid) {
658
-			$entryid = hex2bin($id);
659
-			$sk = $id;
660
-		}
661
-		else {
662
-			// id might be in the new longid format, so we have to split it here
663
-			list($fsk, $sk) = Utils::SplitMessageId($id);
664
-			// get the entry id of the message
665
-			$entryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($folderid), hex2bin($sk));
666
-		}
667
-		if (!$entryid) {
668
-			throw new StatusException(sprintf("Grommunio->Fetch('%s','%s'): Error getting entryid: 0x%X", $folderid, $sk, mapi_last_hresult()), SYNC_STATUS_OBJECTNOTFOUND);
669
-		}
670
-
671
-		// open the message
672
-		$message = mapi_msgstore_openentry($this->store, $entryid);
673
-		if (!$message) {
674
-			throw new StatusException(sprintf("Grommunio->Fetch('%s','%s'): Error, unable to open message: 0x%X", $folderid, $sk, mapi_last_hresult()), SYNC_STATUS_OBJECTNOTFOUND);
675
-		}
676
-
677
-		// convert the mapi message into a SyncObject and return it
678
-		$mapiprovider = new MAPIProvider($this->session, $this->store);
679
-
680
-		// override truncation
681
-		$contentparameters->SetTruncation(SYNC_TRUNCATION_ALL);
682
-		// TODO check for body preferences
683
-		return $mapiprovider->GetMessage($message, $contentparameters);
684
-	}
685
-
686
-	/**
687
-	 * Returns the waste basket.
688
-	 *
689
-	 * @return string
690
-	 */
691
-	public function GetWasteBasket() {
692
-		if ($this->wastebasket) {
693
-			return $this->wastebasket;
694
-		}
695
-
696
-		$storeprops = mapi_getprops($this->defaultstore, [PR_IPM_WASTEBASKET_ENTRYID]);
697
-		if (isset($storeprops[PR_IPM_WASTEBASKET_ENTRYID])) {
698
-			$wastebasket = mapi_msgstore_openentry($this->defaultstore, $storeprops[PR_IPM_WASTEBASKET_ENTRYID]);
699
-			$wastebasketprops = mapi_getprops($wastebasket, [PR_SOURCE_KEY]);
700
-			if (isset($wastebasketprops[PR_SOURCE_KEY])) {
701
-				$this->wastebasket = bin2hex($wastebasketprops[PR_SOURCE_KEY]);
702
-				SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetWasteBasket(): Got waste basket with id '%s'", $this->wastebasket));
703
-
704
-				return $this->wastebasket;
705
-			}
706
-		}
707
-
708
-		return false;
709
-	}
710
-
711
-	/**
712
-	 * Returns the content of the named attachment as stream.
713
-	 *
714
-	 * @param string $attname
715
-	 *
716
-	 * @throws StatusException
717
-	 *
718
-	 * @return SyncItemOperationsAttachment
719
-	 */
720
-	public function GetAttachmentData($attname) {
721
-		SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetAttachmentData('%s')", $attname));
722
-
723
-		if (!strpos($attname, ":")) {
724
-			throw new StatusException(sprintf("Grommunio->GetAttachmentData('%s'): Error, attachment requested for non-existing item", $attname), SYNC_ITEMOPERATIONSSTATUS_INVALIDATT);
725
-		}
726
-
727
-		list($id, $attachnum, $parentEntryid) = explode(":", $attname);
728
-		if (isset($parentEntryid)) {
729
-			$this->Setup(GSync::GetAdditionalSyncFolderStore($parentEntryid));
730
-		}
731
-
732
-		$entryid = hex2bin($id);
733
-		$message = mapi_msgstore_openentry($this->store, $entryid);
734
-		if (!$message) {
735
-			throw new StatusException(sprintf("Grommunio->GetAttachmentData('%s'): Error, unable to open item for attachment data for id '%s' with: 0x%X", $attname, $id, mapi_last_hresult()), SYNC_ITEMOPERATIONSSTATUS_INVALIDATT);
736
-		}
737
-
738
-		MAPIUtils::ParseSmime($this->session, $this->defaultstore, $this->getAddressbook(), $message);
739
-		$attach = mapi_message_openattach($message, $attachnum);
740
-		if (!$attach) {
741
-			throw new StatusException(sprintf("Grommunio->GetAttachmentData('%s'): Error, unable to open attachment number '%s' with: 0x%X", $attname, $attachnum, mapi_last_hresult()), SYNC_ITEMOPERATIONSSTATUS_INVALIDATT);
742
-		}
743
-
744
-		// get necessary attachment props
745
-		$attprops = mapi_getprops($attach, [PR_ATTACH_MIME_TAG, PR_ATTACH_MIME_TAG_W, PR_ATTACH_METHOD]);
746
-		$attachment = new SyncItemOperationsAttachment();
747
-		// check if it's an embedded message and open it in such a case
748
-		if (isset($attprops[PR_ATTACH_METHOD]) && $attprops[PR_ATTACH_METHOD] == ATTACH_EMBEDDED_MSG) {
749
-			$embMessage = mapi_attach_openobj($attach);
750
-			$addrbook = $this->getAddressbook();
751
-			$stream = mapi_inetmapi_imtoinet($this->session, $addrbook, $embMessage, ['use_tnef' => -1]);
752
-			// set the default contenttype for this kind of messages
753
-			$attachment->contenttype = "message/rfc822";
754
-		}
755
-		else {
756
-			$stream = mapi_openproperty($attach, PR_ATTACH_DATA_BIN, IID_IStream, 0, 0);
757
-		}
758
-
759
-		if (!$stream) {
760
-			throw new StatusException(sprintf("Grommunio->GetAttachmentData('%s'): Error, unable to open attachment data stream: 0x%X", $attname, mapi_last_hresult()), SYNC_ITEMOPERATIONSSTATUS_INVALIDATT);
761
-		}
762
-
763
-		// put the mapi stream into a wrapper to get a standard stream
764
-		$attachment->data = MAPIStreamWrapper::Open($stream);
765
-		if (isset($attprops[PR_ATTACH_MIME_TAG])) {
766
-			$attachment->contenttype = $attprops[PR_ATTACH_MIME_TAG];
767
-		}
768
-		elseif (isset($attprops[PR_ATTACH_MIME_TAG_W])) {
769
-			$attachment->contenttype = $attprops[PR_ATTACH_MIME_TAG_W];
770
-		}
771
-		// TODO default contenttype
772
-		return $attachment;
773
-	}
774
-
775
-	/**
776
-	 * Deletes all contents of the specified folder.
777
-	 * This is generally used to empty the trash (wastebasked), but could also be used on any
778
-	 * other folder.
779
-	 *
780
-	 * @param string $folderid
781
-	 * @param bool   $includeSubfolders (opt) also delete sub folders, default true
782
-	 *
783
-	 * @throws StatusException
784
-	 *
785
-	 * @return bool
786
-	 */
787
-	public function EmptyFolder($folderid, $includeSubfolders = true) {
788
-		$folderentryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($folderid));
789
-		if (!$folderentryid) {
790
-			throw new StatusException(sprintf("Grommunio->EmptyFolder('%s','%s'): Error, unable to open folder (no entry id)", $folderid, Utils::PrintAsString($includeSubfolders)), SYNC_ITEMOPERATIONSSTATUS_SERVERERROR);
791
-		}
792
-		$folder = mapi_msgstore_openentry($this->store, $folderentryid);
793
-
794
-		if (!$folder) {
795
-			throw new StatusException(sprintf("Grommunio->EmptyFolder('%s','%s'): Error, unable to open parent folder (open entry)", $folderid, Utils::PrintAsString($includeSubfolders)), SYNC_ITEMOPERATIONSSTATUS_SERVERERROR);
796
-		}
797
-
798
-		$flags = 0;
799
-		if ($includeSubfolders) {
800
-			$flags = DEL_ASSOCIATED;
801
-		}
802
-
803
-		SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->EmptyFolder('%s','%s'): emptying folder", $folderid, Utils::PrintAsString($includeSubfolders)));
804
-
805
-		// empty folder!
806
-		mapi_folder_emptyfolder($folder, $flags);
807
-		if (mapi_last_hresult()) {
808
-			throw new StatusException(sprintf("Grommunio->EmptyFolder('%s','%s'): Error, mapi_folder_emptyfolder() failed: 0x%X", $folderid, Utils::PrintAsString($includeSubfolders), mapi_last_hresult()), SYNC_ITEMOPERATIONSSTATUS_SERVERERROR);
809
-		}
810
-
811
-		return true;
812
-	}
813
-
814
-	/**
815
-	 * Processes a response to a meeting request.
816
-	 * CalendarID is a reference and has to be set if a new calendar item is created.
817
-	 *
818
-	 * @param string $requestid id of the object containing the request
819
-	 * @param string $folderid  id of the parent folder of $requestid
820
-	 * @param string $response
821
-	 *
822
-	 * @throws StatusException
823
-	 *
824
-	 * @return string id of the created/updated calendar obj
825
-	 */
826
-	public function MeetingResponse($requestid, $folderid, $response) {
827
-		// Use standard meeting response code to process meeting request
828
-		list($fid, $requestid) = Utils::SplitMessageId($requestid);
829
-		$reqentryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($folderid), hex2bin($requestid));
830
-		if (!$reqentryid) {
831
-			throw new StatusException(sprintf("Grommunio->MeetingResponse('%s', '%s', '%s'): Error, unable to entryid of the message 0x%X", $requestid, $folderid, $response, mapi_last_hresult()), SYNC_MEETRESPSTATUS_INVALIDMEETREQ);
832
-		}
833
-
834
-		$mapimessage = mapi_msgstore_openentry($this->store, $reqentryid);
835
-		if (!$mapimessage) {
836
-			throw new StatusException(sprintf("Grommunio->MeetingResponse('%s','%s', '%s'): Error, unable to open request message for response 0x%X", $requestid, $folderid, $response, mapi_last_hresult()), SYNC_MEETRESPSTATUS_INVALIDMEETREQ);
837
-		}
838
-
839
-		// ios sends calendar item in MeetingResponse
840
-		// @see https://jira.z-hub.io/browse/ZP-1524
841
-		$folderClass = GSync::GetDeviceManager()->GetFolderClassFromCacheByID($fid);
842
-		// find the corresponding meeting request
843
-		if ($folderClass != 'Email') {
844
-			$props = MAPIMapping::GetMeetingRequestProperties();
845
-			$props = getPropIdsFromStrings($this->store, $props);
846
-
847
-			$messageprops = mapi_getprops($mapimessage, [$props["goidtag"]]);
848
-			$goid = $messageprops[$props["goidtag"]];
849
-
850
-			$mapiprovider = new MAPIProvider($this->session, $this->store);
851
-			$inboxprops = $mapiprovider->GetInboxProps();
852
-			$folder = mapi_msgstore_openentry($this->store, $inboxprops[PR_ENTRYID]);
853
-
854
-			// Find the item by restricting all items to the correct ID
855
-			$restrict = [RES_AND, [
856
-				[RES_PROPERTY,
857
-					[
858
-						RELOP => RELOP_EQ,
859
-						ULPROPTAG => $props["goidtag"],
860
-						VALUE => $goid,
861
-					],
862
-				],
863
-			]];
864
-
865
-			$inboxcontents = mapi_folder_getcontentstable($folder);
866
-
867
-			$rows = mapi_table_queryallrows($inboxcontents, [PR_ENTRYID], $restrict);
868
-			if (empty($rows)) {
869
-				throw new StatusException(sprintf("Grommunio->MeetingResponse('%s','%s', '%s'): Error, meeting request not found in the inbox", $requestid, $folderid, $response), SYNC_MEETRESPSTATUS_INVALIDMEETREQ);
870
-			}
871
-			SLog::Write(LOGLEVEL_DEBUG, "Grommunio->MeetingResponse found meeting request in the inbox");
872
-			$mapimessage = mapi_msgstore_openentry($this->store, $rows[0][PR_ENTRYID]);
873
-			$reqentryid = $rows[0][PR_ENTRYID];
874
-		}
875
-
876
-		$meetingrequest = new Meetingrequest($this->store, $mapimessage, $this->session);
877
-
878
-		if (!$meetingrequest->isMeetingRequest()) {
879
-			throw new StatusException(sprintf("Grommunio->MeetingResponse('%s','%s', '%s'): Error, attempt to respond to non-meeting request", $requestid, $folderid, $response), SYNC_MEETRESPSTATUS_INVALIDMEETREQ);
880
-		}
881
-
882
-		if ($meetingrequest->isLocalOrganiser()) {
883
-			throw new StatusException(sprintf("Grommunio->MeetingResponse('%s','%s', '%s'): Error, attempt to response to meeting request that we organized", $requestid, $folderid, $response), SYNC_MEETRESPSTATUS_INVALIDMEETREQ);
884
-		}
885
-
886
-		// Process the meeting response. We don't have to send the actual meeting response
887
-		// e-mail, because the device will send it itself. This seems not to be the case
888
-		// anymore for the ios devices since at least version 12.4. grommunio-sync will send the
889
-		// accepted email in such a case.
890
-		// @see https://jira.z-hub.io/browse/ZP-1524
891
-		$sendresponse = false;
892
-		$deviceType = strtolower(Request::GetDeviceType());
893
-		if ($deviceType == 'iphone' || $deviceType == 'ipad' || $deviceType == 'ipod') {
894
-			$matches = [];
895
-			if (preg_match("/^Apple-.*?\\/(\\d{4})\\./", Request::GetUserAgent(), $matches) && isset($matches[1]) && $matches[1] >= 1607) {
896
-				SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->MeetingResponse: iOS device %s->%s", Request::GetDeviceType(), Request::GetUserAgent()));
897
-				$sendresponse = true;
898
-			}
899
-		}
900
-
901
-		switch ($response) {
902
-			case 1:     // accept
903
-			default:
904
-				$entryid = $meetingrequest->doAccept(false, $sendresponse, false, false, false, false, true); // last true is the $userAction
905
-				break;
906
-
907
-			case 2:        // tentative
908
-				$entryid = $meetingrequest->doAccept(true, $sendresponse, false, false, false, false, true); // last true is the $userAction
909
-				break;
910
-
911
-			case 3:        // decline
912
-				$meetingrequest->doDecline(false);
913
-				break;
914
-		}
915
-
916
-		// F/B will be updated on logoff
917
-
918
-		// We have to return the ID of the new calendar item, so do that here
919
-		$calendarid = "";
920
-		$calFolderId = "";
921
-		if (isset($entryid)) {
922
-			$newitem = mapi_msgstore_openentry($this->store, $entryid);
923
-			// new item might be in a delegator's store. ActiveSync does not support accepting them.
924
-			if (!$newitem) {
925
-				throw new StatusException(sprintf("Grommunio->MeetingResponse('%s','%s', '%s'): Object with entryid '%s' was not found in user's store (0x%X). It might be in a delegator's store.", $requestid, $folderid, $response, bin2hex($entryid), mapi_last_hresult()), SYNC_MEETRESPSTATUS_SERVERERROR, null, LOGLEVEL_WARN);
926
-			}
927
-
928
-			$newprops = mapi_getprops($newitem, [PR_SOURCE_KEY, PR_PARENT_SOURCE_KEY]);
929
-			$calendarid = bin2hex($newprops[PR_SOURCE_KEY]);
930
-			$calFolderId = bin2hex($newprops[PR_PARENT_SOURCE_KEY]);
931
-		}
932
-
933
-		// on recurring items, the MeetingRequest class responds with a wrong entryid
934
-		if ($requestid == $calendarid) {
935
-			SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->MeetingResponse('%s','%s', '%s'): returned calendar id is the same as the requestid - re-searching", $requestid, $folderid, $response));
936
-
937
-			if (empty($props)) {
938
-				$props = MAPIMapping::GetMeetingRequestProperties();
939
-				$props = getPropIdsFromStrings($this->store, $props);
940
-
941
-				$messageprops = mapi_getprops($mapimessage, [$props["goidtag"]]);
942
-				$goid = $messageprops[$props["goidtag"]];
943
-			}
944
-
945
-			$items = $meetingrequest->findCalendarItems($goid);
946
-
947
-			if (is_array($items)) {
948
-				$newitem = mapi_msgstore_openentry($this->store, $items[0]);
949
-				$newprops = mapi_getprops($newitem, [PR_SOURCE_KEY, PR_PARENT_SOURCE_KEY]);
950
-				$calendarid = bin2hex($newprops[PR_SOURCE_KEY]);
951
-				$calFolderId = bin2hex($newprops[PR_PARENT_SOURCE_KEY]);
952
-				SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->MeetingResponse('%s','%s', '%s'): found other calendar entryid", $requestid, $folderid, $response));
953
-			}
954
-
955
-			if ($requestid == $calendarid) {
956
-				throw new StatusException(sprintf("Grommunio->MeetingResponse('%s','%s', '%s'): Error finding the accepted meeting response in the calendar", $requestid, $folderid, $response), SYNC_MEETRESPSTATUS_INVALIDMEETREQ);
957
-			}
958
-		}
959
-
960
-		// delete meeting request from Inbox
961
-		if ($folderClass == 'Email') {
962
-			$folderentryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($folderid));
963
-			$folder = mapi_msgstore_openentry($this->store, $folderentryid);
964
-		}
965
-		mapi_folder_deletemessages($folder, [$reqentryid], 0);
966
-
967
-		$prefix = '';
968
-		// prepend the short folderid of the target calendar: if available and short ids are used
969
-		if ($calFolderId) {
970
-			$shortFolderId = GSync::GetDeviceManager()->GetFolderIdForBackendId($calFolderId);
971
-			if ($calFolderId != $shortFolderId) {
972
-				$prefix = $shortFolderId . ':';
973
-			}
974
-		}
975
-
976
-		return $prefix . $calendarid;
977
-	}
978
-
979
-	/**
980
-	 * Indicates if the backend has a ChangesSink.
981
-	 * A sink is an active notification mechanism which does not need polling.
982
-	 * Since Zarafa 7.0.5 such a sink is available.
983
-	 * The grommunio backend uses this method to initialize the sink with mapi.
984
-	 *
985
-	 * @return bool
986
-	 */
987
-	public function HasChangesSink() {
988
-		if (!$this->notifications) {
989
-			SLog::Write(LOGLEVEL_DEBUG, "Grommunio->HasChangesSink(): sink is not available");
990
-
991
-			return false;
992
-		}
993
-
994
-		$this->changesSink = @mapi_sink_create();
995
-
996
-		if (!$this->changesSink || mapi_last_hresult()) {
997
-			SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->HasChangesSink(): sink could not be created with  0x%X", mapi_last_hresult()));
998
-
999
-			return false;
1000
-		}
1001
-
1002
-		$this->changesSinkHierarchyHash = $this->getHierarchyHash();
1003
-		SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->HasChangesSink(): created - HierarchyHash: %s", $this->changesSinkHierarchyHash));
1004
-
1005
-		// advise the main store and also to check if the connection supports it
1006
-		return $this->adviseStoreToSink($this->defaultstore);
1007
-	}
1008
-
1009
-	/**
1010
-	 * The folder should be considered by the sink.
1011
-	 * Folders which were not initialized should not result in a notification
1012
-	 * of IBackend->ChangesSink().
1013
-	 *
1014
-	 * @param string $folderid
1015
-	 *
1016
-	 * @return bool false if entryid can not be found for that folder
1017
-	 */
1018
-	public function ChangesSinkInitialize($folderid) {
1019
-		SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->ChangesSinkInitialize(): folderid '%s'", $folderid));
1020
-
1021
-		$entryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($folderid));
1022
-		if (!$entryid) {
1023
-			return false;
1024
-		}
1025
-
1026
-		// add entryid to the monitored folders
1027
-		$this->changesSinkFolders[$entryid] = $folderid;
1028
-
1029
-		// advise the current store to the sink
1030
-		return $this->adviseStoreToSink($this->store);
1031
-	}
1032
-
1033
-	/**
1034
-	 * The actual ChangesSink.
1035
-	 * For max. the $timeout value this method should block and if no changes
1036
-	 * are available return an empty array.
1037
-	 * If changes are available a list of folderids is expected.
1038
-	 *
1039
-	 * @param int $timeout max. amount of seconds to block
1040
-	 *
1041
-	 * @return array
1042
-	 */
1043
-	public function ChangesSink($timeout = 30) {
1044
-		// clear the folder stats cache
1045
-		unset($this->folderStatCache);
1046
-
1047
-		$notifications = [];
1048
-		$hierarchyNotifications = [];
1049
-		$sinkresult = @mapi_sink_timedwait($this->changesSink, $timeout * 1000);
1050
-
1051
-		if (!is_array($sinkresult)) {
1052
-			throw new StatusException("Grommunio->ChangesSink(): Sink returned invalid notification, aborting", SyncCollections::OBSOLETE_CONNECTION);
1053
-		}
1054
-
1055
-		// reverse array so that the changes on folders are before changes on messages and
1056
-		// it's possible to filter such notifications
1057
-		$sinkresult = array_reverse($sinkresult, true);
1058
-		foreach ($sinkresult as $sinknotif) {
1059
-			// add a notification on a folder
1060
-			if ($sinknotif['objtype'] == MAPI_FOLDER) {
1061
-				$hierarchyNotifications[$sinknotif['entryid']] = IBackend::HIERARCHYNOTIFICATION;
1062
-			}
1063
-			// change on a message, remove hierarchy notification
1064
-			if (isset($sinknotif['parentid']) && $sinknotif['objtype'] == MAPI_MESSAGE && isset($notifications[$sinknotif['parentid']])) {
1065
-				unset($hierarchyNotifications[$sinknotif['parentid']]);
1066
-			}
1067
-
1068
-			// TODO check if adding $sinknotif['objtype'] = MAPI_MESSAGE wouldn't break anything
1069
-			// check if something in the monitored folders changed
1070
-			if (isset($sinknotif['parentid']) && array_key_exists($sinknotif['parentid'], $this->changesSinkFolders)) {
1071
-				$notifications[] = $this->changesSinkFolders[$sinknotif['parentid']];
1072
-			}
1073
-			// deletes and moves
1074
-			if (isset($sinknotif['oldparentid']) && array_key_exists($sinknotif['oldparentid'], $this->changesSinkFolders)) {
1075
-				$notifications[] = $this->changesSinkFolders[$sinknotif['oldparentid']];
1076
-			}
1077
-		}
1078
-
1079
-		// validate hierarchy notifications by comparing the hierarchy hashes (too many false positives otherwise)
1080
-		if (!empty($hierarchyNotifications)) {
1081
-			$hash = $this->getHierarchyHash();
1082
-			if ($hash !== $this->changesSinkHierarchyHash) {
1083
-				SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->ChangesSink() Hierarchy notification, pending validation. New hierarchyHash: %s", $hash));
1084
-				$notifications[] = IBackend::HIERARCHYNOTIFICATION;
1085
-				$this->changesSinkHierarchyHash = $hash;
1086
-			}
1087
-		}
1088
-
1089
-		return $notifications;
1090
-	}
1091
-
1092
-	/**
1093
-	 * Applies settings to and gets information from the device.
1094
-	 *
1095
-	 * @param SyncObject $settings (SyncOOF, SyncUserInformation, SyncRightsManagementTemplates possible)
1096
-	 *
1097
-	 * @return SyncObject $settings
1098
-	 */
1099
-	public function Settings($settings) {
1100
-		if ($settings instanceof SyncOOF) {
1101
-			$this->settingsOOF($settings);
1102
-		}
1103
-
1104
-		if ($settings instanceof SyncUserInformation) {
1105
-			$this->settingsUserInformation($settings);
1106
-		}
1107
-
1108
-		if ($settings instanceof SyncRightsManagementTemplates) {
1109
-			$this->settingsRightsManagementTemplates($settings);
1110
-		}
1111
-
1112
-		return $settings;
1113
-	}
1114
-
1115
-	/**
1116
-	 * Resolves recipients.
1117
-	 *
1118
-	 * @param SyncObject $resolveRecipients
1119
-	 *
1120
-	 * @return SyncObject $resolveRecipients
1121
-	 */
1122
-	public function ResolveRecipients($resolveRecipients) {
1123
-		if ($resolveRecipients instanceof SyncResolveRecipients) {
1124
-			$resolveRecipients->status = SYNC_RESOLVERECIPSSTATUS_SUCCESS;
1125
-			$resolveRecipients->response = [];
1126
-			$resolveRecipientsOptions = new SyncResolveRecipientsOptions();
1127
-			$maxAmbiguousRecipients = self::MAXAMBIGUOUSRECIPIENTS;
1128
-
1129
-			if (isset($resolveRecipients->options)) {
1130
-				$resolveRecipientsOptions = $resolveRecipients->options;
1131
-				// only limit ambiguous recipients if the client requests it.
1132
-
1133
-				if (isset($resolveRecipientsOptions->maxambiguousrecipients) &&
1134
-						$resolveRecipientsOptions->maxambiguousrecipients >= 0 &&
1135
-						$resolveRecipientsOptions->maxambiguousrecipients <= self::MAXAMBIGUOUSRECIPIENTS) {
1136
-					$maxAmbiguousRecipients = $resolveRecipientsOptions->maxambiguousrecipients;
1137
-					SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->ResolveRecipients(): The client requested %d max ambiguous recipients to resolve.", $maxAmbiguousRecipients));
1138
-				}
1139
-			}
1140
-
1141
-			foreach ($resolveRecipients->to as $i => $to) {
1142
-				$response = new SyncResolveRecipientsResponse();
1143
-				$response->to = $to;
1144
-				$response->status = SYNC_RESOLVERECIPSSTATUS_SUCCESS;
1145
-
1146
-				// do not expand distlists here
1147
-				$recipient = $this->resolveRecipient($to, $maxAmbiguousRecipients, false);
1148
-				if (is_array($recipient) && !empty($recipient)) {
1149
-					$response->recipientcount = 0;
1150
-					foreach ($recipient as $entry) {
1151
-						if ($entry instanceof SyncResolveRecipient) {
1152
-							// certificates are already set. Unset them if they weren't required.
1153
-							if (!isset($resolveRecipientsOptions->certificateretrieval)) {
1154
-								unset($entry->certificates);
1155
-							}
1156
-							if (isset($resolveRecipientsOptions->availability)) {
1157
-								if (!isset($resolveRecipientsOptions->starttime)) {
1158
-									// TODO error, the request must include a valid StartTime element value
1159
-								}
1160
-								$entry->availability = $this->getAvailability($to, $entry, $resolveRecipientsOptions);
1161
-							}
1162
-							if (isset($resolveRecipientsOptions->picture)) {
1163
-								// TODO implement picture retrieval of the recipient
1164
-							}
1165
-							++$response->recipientcount;
1166
-							$response->recipient[] = $entry;
1167
-						}
1168
-						elseif (is_int($recipient)) {
1169
-							$response->status = $recipient;
1170
-						}
1171
-					}
1172
-				}
1173
-
1174
-				$resolveRecipients->response[$i] = $response;
1175
-			}
1176
-
1177
-			return $resolveRecipients;
1178
-		}
1179
-
1180
-		SLog::Write(LOGLEVEL_WARN, "Grommunio->ResolveRecipients(): Not a valid SyncResolveRecipients object.");
1181
-		// return a SyncResolveRecipients object so that sync doesn't fail
1182
-		$r = new SyncResolveRecipients();
1183
-		$r->status = SYNC_RESOLVERECIPSSTATUS_PROTOCOLERROR;
1184
-
1185
-		return $r;
1186
-	}
1187
-
1188
-	/*----------------------------------------------------------------------------------------------------------
24
+    private $mainUser;
25
+    private $session;
26
+    private $defaultstore;
27
+    private $store;
28
+    private $storeName;
29
+    private $storeCache;
30
+    private $notifications;
31
+    private $changesSink;
32
+    private $changesSinkFolders;
33
+    private $changesSinkHierarchyHash;
34
+    private $changesSinkStores;
35
+    private $wastebasket;
36
+    private $addressbook;
37
+    private $folderStatCache;
38
+    private $impersonateUser;
39
+    private $stateFolder;
40
+    private $userDeviceData;
41
+
42
+    // KC config parameter for PR_EC_ENABLED_FEATURES / PR_EC_DISABLED_FEATURES
43
+    public const MOBILE_ENABLED = 'mobile';
44
+
45
+    public const MAXAMBIGUOUSRECIPIENTS = 9999;
46
+    public const FREEBUSYENUMBLOCKS = 50;
47
+    public const MAXFREEBUSYSLOTS = 32767; // max length of 32k for the MergedFreeBusy element is allowed
48
+    public const HALFHOURSECONDS = 1800;
49
+
50
+    /**
51
+     * Constructor of the grommunio Backend.
52
+     */
53
+    public function __construct() {
54
+        $this->session = false;
55
+        $this->store = false;
56
+        $this->storeName = false;
57
+        $this->storeCache = [];
58
+        $this->notifications = false;
59
+        $this->changesSink = false;
60
+        $this->changesSinkFolders = [];
61
+        $this->changesSinkStores = [];
62
+        $this->changesSinkHierarchyHash = false;
63
+        $this->wastebasket = false;
64
+        $this->session = false;
65
+        $this->folderStatCache = [];
66
+        $this->impersonateUser = false;
67
+        $this->stateFolder = null;
68
+
69
+        SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio using PHP-MAPI version: %s - PHP version: %s", phpversion("mapi"), phpversion()));
70
+
71
+        # Interprocessdata
72
+        $this->allocate = 0;
73
+        $this->type = "grommunio-sync:userdevices";
74
+        $this->userDeviceData = "grommunio-sync:statefoldercache";
75
+        parent::__construct();
76
+    }
77
+
78
+    /**
79
+     * Indicates which StateMachine should be used.
80
+     *
81
+     * @return bool Grommunio uses own state machine
82
+     */
83
+    public function GetStateMachine() {
84
+        return $this;
85
+    }
86
+
87
+    /**
88
+     * Returns the Grommunio as it implements the ISearchProvider interface
89
+     * This could be overwritten by the global configuration.
90
+     *
91
+     * @return object Implementation of ISearchProvider
92
+     */
93
+    public function GetSearchProvider() {
94
+        return $this;
95
+    }
96
+
97
+    /**
98
+     * Indicates which AS version is supported by the backend.
99
+     *
100
+     * @return string AS version constant
101
+     */
102
+    public function GetSupportedASVersion() {
103
+        return GSync::ASV_141;
104
+    }
105
+
106
+    /**
107
+     * Authenticates the user with the configured grommunio server.
108
+     *
109
+     * @param string $username
110
+     * @param string $domain
111
+     * @param string $password
112
+     * @param mixed  $user
113
+     * @param mixed  $pass
114
+     *
115
+     * @throws AuthenticationRequiredException
116
+     *
117
+     * @return bool
118
+     */
119
+    public function Logon($user, $domain, $pass) {
120
+        SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->Logon(): Trying to authenticate user '%s'..", $user));
121
+
122
+        $this->mainUser = strtolower($user);
123
+        // TODO the impersonated user should be passed directly to IBackend->Logon() - ZP-1351
124
+        if (Request::GetImpersonatedUser()) {
125
+            $this->impersonateUser = strtolower(Request::GetImpersonatedUser());
126
+        }
127
+
128
+        // check if we are impersonating someone
129
+        // $defaultUser will be used for $this->defaultStore
130
+        if ($this->impersonateUser !== false) {
131
+            SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->Logon(): Impersonation active - authenticating: '%s' - impersonating '%s'", $this->mainUser, $this->impersonateUser));
132
+            $defaultUser = $this->impersonateUser;
133
+        }
134
+        else {
135
+            $defaultUser = $this->mainUser;
136
+        }
137
+
138
+        $deviceId = Request::GetDeviceID();
139
+
140
+        try {
141
+            // check if notifications are available in php-mapi
142
+            if (function_exists('mapi_feature') && mapi_feature('LOGONFLAGS')) {
143
+                // send grommunio-sync version and user agent to ZCP - ZP-589
144
+                if (Utils::CheckMapiExtVersion('7.2.0')) {
145
+                    $gsync_version = 'Grommunio-Sync_' . @constant('GROMMUNIOSYNC_VERSION');
146
+                    $user_agent = ($deviceId) ? GSync::GetDeviceManager()->GetUserAgent() : "unknown";
147
+                    $this->session = @mapi_logon_zarafa($this->mainUser, $pass, MAPI_SERVER, null, null, 0, $gsync_version, $user_agent);
148
+                }
149
+                else {
150
+                    $this->session = @mapi_logon_zarafa($this->mainUser, $pass, MAPI_SERVER, null, null, 0);
151
+                }
152
+                $this->notifications = true;
153
+            }
154
+            // old fashioned session
155
+            else {
156
+                $this->session = @mapi_logon_zarafa($this->mainUser, $pass, MAPI_SERVER);
157
+                $this->notifications = false;
158
+            }
159
+
160
+            if (mapi_last_hresult()) {
161
+                SLog::Write(LOGLEVEL_ERROR, sprintf("Grommunio->Logon(): login failed with error code: 0x%X", mapi_last_hresult()));
162
+                if (mapi_last_hresult() == MAPI_E_NETWORK_ERROR) {
163
+                    throw new ServiceUnavailableException("Error connecting to KC (login)");
164
+                }
165
+            }
166
+        }
167
+        catch (MAPIException $ex) {
168
+            throw new AuthenticationRequiredException($ex->getDisplayMessage());
169
+        }
170
+
171
+        if (!$this->session) {
172
+            SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->Logon(): logon failed for user '%s'", $this->mainUser));
173
+            $this->defaultstore = false;
174
+
175
+            return false;
176
+        }
177
+
178
+        // Get/open default store
179
+        $this->defaultstore = $this->openMessageStore($this->mainUser);
180
+
181
+        // To impersonate, we overwrite the defaultstore. We still need to open it before we can do that.
182
+        if ($this->impersonateUser) {
183
+            SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->Logon(): Impersonating user '%s'", $defaultUser));
184
+            $this->defaultstore = $this->openMessageStore($defaultUser);
185
+        }
186
+
187
+        if (mapi_last_hresult() == MAPI_E_FAILONEPROVIDER) {
188
+            throw new ServiceUnavailableException("Error connecting to KC (open store)");
189
+        }
190
+
191
+        if ($this->defaultstore === false) {
192
+            throw new AuthenticationRequiredException(sprintf("Grommunio->Logon(): User '%s' has no default store", $defaultUser));
193
+        }
194
+
195
+        $this->store = $this->defaultstore;
196
+        $this->storeName = $defaultUser;
197
+
198
+        SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->Logon(): User '%s' is authenticated%s", $this->mainUser, ($this->impersonateUser ? " impersonating '" . $this->impersonateUser . "'" : '')));
199
+
200
+        $this->isGSyncEnabled();
201
+
202
+        // check if this is a Zarafa 7 store with unicode support
203
+        MAPIUtils::IsUnicodeStore($this->store);
204
+
205
+        // open the state folder
206
+        $this->getStateFolder($deviceId);
207
+
208
+        return true;
209
+    }
210
+
211
+    /**
212
+     * Setup the backend to work on a specific store or checks ACLs there.
213
+     * If only the $store is submitted, all Import/Export/Fetch/Etc operations should be
214
+     * performed on this store (switch operations store).
215
+     * If the ACL check is enabled, this operation should just indicate the ACL status on
216
+     * the submitted store, without changing the store for operations.
217
+     * For the ACL status, the currently logged on user MUST have access rights on
218
+     *  - the entire store - admin access if no folderid is sent, or
219
+     *  - on a specific folderid in the store (secretary/full access rights).
220
+     *
221
+     * The ACLcheck MUST fail if a folder of the authenticated user is checked!
222
+     *
223
+     * @param string $store        target store, could contain a "domain\user" value
224
+     * @param bool   $checkACLonly if set to true, Setup() should just check ACLs
225
+     * @param string $folderid     if set, only ACLs on this folderid are relevant
226
+     *
227
+     * @return bool
228
+     */
229
+    public function Setup($store, $checkACLonly = false, $folderid = false) {
230
+        list($user, $domain) = Utils::SplitDomainUser($store);
231
+
232
+        if (!isset($this->mainUser)) {
233
+            return false;
234
+        }
235
+
236
+        $mainUser = $this->mainUser;
237
+        // when impersonating we need to check against the impersonated user
238
+        if ($this->impersonateUser) {
239
+            $mainUser = $this->impersonateUser;
240
+        }
241
+
242
+        if ($user === false) {
243
+            $user = $mainUser;
244
+        }
245
+
246
+        // This is a special case. A user will get his entire folder structure by the foldersync by default.
247
+        // The ACL check is executed when an additional folder is going to be sent to the mobile.
248
+        // Configured that way the user could receive the same folderid twice, with two different names.
249
+        if ($mainUser == $user && $checkACLonly && $folderid && !$this->impersonateUser) {
250
+            SLog::Write(LOGLEVEL_DEBUG, "Grommunio->Setup(): Checking ACLs for folder of the users defaultstore. Fail is forced to avoid folder duplications on mobile.");
251
+
252
+            return false;
253
+        }
254
+
255
+        // get the users store
256
+        $userstore = $this->openMessageStore($user);
257
+
258
+        // only proceed if a store was found, else return false
259
+        if ($userstore) {
260
+            // only check permissions
261
+            if ($checkACLonly == true) {
262
+                // check for admin rights
263
+                if (!$folderid) {
264
+                    if ($user != $this->mainUser) {
265
+                        if ($this->impersonateUser) {
266
+                            $storeProps = mapi_getprops($userstore, [PR_IPM_SUBTREE_ENTRYID]);
267
+                            $rights = $this->HasSecretaryACLs($userstore, '', $storeProps[PR_IPM_SUBTREE_ENTRYID]);
268
+                            SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->Setup(): Checking for secretary ACLs on root folder of impersonated store '%s': '%s'", $user, Utils::PrintAsString($rights)));
269
+                        }
270
+                        else {
271
+                            $zarafauserinfo = @nsp_getuserinfo($this->mainUser);
272
+                            $rights = (isset($zarafauserinfo['admin']) && $zarafauserinfo['admin']) ? true : false;
273
+                            SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->Setup(): Checking for admin ACLs on store '%s': '%s'", $user, Utils::PrintAsString($rights)));
274
+                        }
275
+                    }
276
+                    // the user has always full access to his own store
277
+                    else {
278
+                        $rights = true;
279
+                        SLog::Write(LOGLEVEL_DEBUG, "Grommunio->Setup(): the user has always full access to his own store");
280
+                    }
281
+
282
+                    return $rights;
283
+                }
284
+                // check permissions on this folder
285
+
286
+                $rights = $this->HasSecretaryACLs($userstore, $folderid);
287
+                SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->Setup(): Checking for secretary ACLs on '%s' of store '%s': '%s'", $folderid, $user, Utils::PrintAsString($rights)));
288
+
289
+                return $rights;
290
+            }
291
+
292
+            // switch operations store
293
+            // this should also be done if called with user = mainuser or user = false
294
+            // which means to switch back to the default store
295
+
296
+            // switch active store
297
+            $this->store = $userstore;
298
+            $this->storeName = $user;
299
+
300
+            return true;
301
+        }
302
+
303
+        return false;
304
+    }
305
+
306
+    /**
307
+     * Logs off
308
+     * Free/Busy information is updated for modified calendars
309
+     * This is done after the synchronization process is completed.
310
+     *
311
+     * @return bool
312
+     */
313
+    public function Logoff() {
314
+        return true;
315
+    }
316
+
317
+    /**
318
+     * Returns an array of SyncFolder types with the entire folder hierarchy
319
+     * on the server (the array itself is flat, but refers to parents via the 'parent' property.
320
+     *
321
+     * provides AS 1.0 compatibility
322
+     *
323
+     * @return array SYNC_FOLDER
324
+     */
325
+    public function GetHierarchy() {
326
+        $folders = [];
327
+        $mapiprovider = new MAPIProvider($this->session, $this->store);
328
+        $storeProps = $mapiprovider->GetStoreProps();
329
+
330
+        // for SYSTEM user open the public folders
331
+        if (strtoupper($this->storeName) == "SYSTEM") {
332
+            $rootfolder = mapi_msgstore_openentry($this->store, $storeProps[PR_IPM_PUBLIC_FOLDERS_ENTRYID]);
333
+        }
334
+        else {
335
+            $rootfolder = mapi_msgstore_openentry($this->store);
336
+        }
337
+
338
+        $rootfolderprops = mapi_getprops($rootfolder, [PR_SOURCE_KEY]);
339
+
340
+        $hierarchy = mapi_folder_gethierarchytable($rootfolder, CONVENIENT_DEPTH);
341
+        $rows = mapi_table_queryallrows($hierarchy, [PR_DISPLAY_NAME, PR_PARENT_ENTRYID, PR_ENTRYID, PR_SOURCE_KEY, PR_PARENT_SOURCE_KEY, PR_CONTAINER_CLASS, PR_ATTR_HIDDEN, PR_EXTENDED_FOLDER_FLAGS, PR_FOLDER_TYPE]);
342
+        SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetHierarchy(): fetched %d folders from MAPI", count($rows)));
343
+
344
+        foreach ($rows as $row) {
345
+            // do not display hidden and search folders
346
+            if ((isset($row[PR_ATTR_HIDDEN]) && $row[PR_ATTR_HIDDEN]) ||
347
+                (isset($row[PR_FOLDER_TYPE]) && $row[PR_FOLDER_TYPE] == FOLDER_SEARCH) ||
348
+                // for SYSTEM user $row[PR_PARENT_SOURCE_KEY] == $rootfolderprops[PR_SOURCE_KEY] is true, but we need those folders
349
+                (isset($row[PR_PARENT_SOURCE_KEY]) && $row[PR_PARENT_SOURCE_KEY] == $rootfolderprops[PR_SOURCE_KEY] && strtoupper($this->storeName) != "SYSTEM")) {
350
+                SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetHierarchy(): ignoring folder '%s' as it's a hidden/search/root folder", (isset($row[PR_DISPLAY_NAME]) ? $row[PR_DISPLAY_NAME] : "unknown")));
351
+
352
+                continue;
353
+            }
354
+            $folder = $mapiprovider->GetFolder($row);
355
+            if ($folder) {
356
+                $folders[] = $folder;
357
+            }
358
+            else {
359
+                SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetHierarchy(): ignoring folder '%s' as MAPIProvider->GetFolder() did not return a SyncFolder object", (isset($row[PR_DISPLAY_NAME]) ? $row[PR_DISPLAY_NAME] : "unknown")));
360
+            }
361
+        }
362
+
363
+        SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetHierarchy(): processed %d folders, starting parent remap", count($folders)));
364
+        // reloop the folders to make sure all parentids are mapped correctly
365
+        $dm = GSync::GetDeviceManager();
366
+        foreach ($folders as $folder) {
367
+            if ($folder->parentid !== "0") {
368
+                // SYSTEM user's parentid points to $rootfolderprops[PR_SOURCE_KEY], but they need to be on the top level
369
+                $folder->parentid = (strtoupper($this->storeName) == "SYSTEM" && $folder->parentid == bin2hex($rootfolderprops[PR_SOURCE_KEY])) ? '0' : $dm->GetFolderIdForBackendId($folder->parentid);
370
+            }
371
+        }
372
+
373
+        return $folders;
374
+    }
375
+
376
+    /**
377
+     * Returns the importer to process changes from the mobile
378
+     * If no $folderid is given, hierarchy importer is expected.
379
+     *
380
+     * @param string $folderid (opt)
381
+     *
382
+     * @return object(ImportChanges)
383
+     */
384
+    public function GetImporter($folderid = false) {
385
+        SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetImporter() folderid: '%s'", Utils::PrintAsString($folderid)));
386
+        if ($folderid !== false) {
387
+            // check if the user of the current store has permissions to import to this folderid
388
+            if ($this->storeName != $this->mainUser && !$this->hasSecretaryACLs($this->store, $folderid)) {
389
+                SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetImporter(): missing permissions on folderid: '%s'.", Utils::PrintAsString($folderid)));
390
+
391
+                return false;
392
+            }
393
+
394
+            return new ImportChangesICS($this->session, $this->store, hex2bin($folderid));
395
+        }
396
+
397
+        return new ImportChangesICS($this->session, $this->store);
398
+    }
399
+
400
+    /**
401
+     * Returns the exporter to send changes to the mobile
402
+     * If no $folderid is given, hierarchy exporter is expected.
403
+     *
404
+     * @param string $folderid (opt)
405
+     *
406
+     * @throws StatusException
407
+     *
408
+     * @return object(ExportChanges)
409
+     */
410
+    public function GetExporter($folderid = false) {
411
+        if ($folderid !== false) {
412
+            // check if the user of the current store has permissions to export from this folderid
413
+            if ($this->storeName != $this->mainUser && !$this->hasSecretaryACLs($this->store, $folderid)) {
414
+                SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetExporter(): missing permissions on folderid: '%s'.", Utils::PrintAsString($folderid)));
415
+
416
+                return false;
417
+            }
418
+
419
+            return new ExportChangesICS($this->session, $this->store, hex2bin($folderid));
420
+        }
421
+
422
+        return new ExportChangesICS($this->session, $this->store);
423
+    }
424
+
425
+    /**
426
+     * Sends an e-mail
427
+     * This messages needs to be saved into the 'sent items' folder.
428
+     *
429
+     * @param SyncSendMail $sm SyncSendMail object
430
+     *
431
+     * @throws StatusException
432
+     *
433
+     * @return bool
434
+     */
435
+    public function SendMail($sm) {
436
+        // Check if imtomapi function is available and use it to send the mime message.
437
+        // It is available since ZCP 7.0.6
438
+        // @see http://jira.zarafa.com/browse/ZCP-9508
439
+        if (!(function_exists('mapi_feature') && mapi_feature('INETMAPI_IMTOMAPI'))) {
440
+            throw new StatusException("Grommunio->SendMail(): ZCP/KC version is too old, INETMAPI_IMTOMAPI is not available. Install at least ZCP version 7.0.6 or later.", SYNC_COMMONSTATUS_MAILSUBMISSIONFAILED, null, LOGLEVEL_FATAL);
441
+
442
+            return false;
443
+        }
444
+        $mimeLength = strlen($sm->mime);
445
+        SLog::Write(LOGLEVEL_DEBUG, sprintf(
446
+            "Grommunio->SendMail(): RFC822: %d bytes  forward-id: '%s' reply-id: '%s' parent-id: '%s' SaveInSent: '%s' ReplaceMIME: '%s'",
447
+            $mimeLength,
448
+            Utils::PrintAsString($sm->forwardflag),
449
+            Utils::PrintAsString($sm->replyflag),
450
+            Utils::PrintAsString((isset($sm->source->folderid) ? $sm->source->folderid : false)),
451
+            Utils::PrintAsString(($sm->saveinsent)),
452
+            Utils::PrintAsString(isset($sm->replacemime))
453
+        ));
454
+        if ($mimeLength == 0) {
455
+            throw new StatusException("Grommunio->SendMail(): empty mail data", SYNC_COMMONSTATUS_MAILSUBMISSIONFAILED);
456
+        }
457
+
458
+        $sendMailProps = MAPIMapping::GetSendMailProperties();
459
+        $sendMailProps = getPropIdsFromStrings($this->defaultstore, $sendMailProps);
460
+
461
+        // Open the outbox and create the message there
462
+        $storeprops = mapi_getprops($this->defaultstore, [$sendMailProps["outboxentryid"], $sendMailProps["ipmsentmailentryid"]]);
463
+        if (isset($storeprops[$sendMailProps["outboxentryid"]])) {
464
+            $outbox = mapi_msgstore_openentry($this->defaultstore, $storeprops[$sendMailProps["outboxentryid"]]);
465
+        }
466
+
467
+        if (!$outbox) {
468
+            throw new StatusException(sprintf("Grommunio->SendMail(): No Outbox found or unable to create message: 0x%X", mapi_last_hresult()), SYNC_COMMONSTATUS_SERVERERROR);
469
+        }
470
+
471
+        $mapimessage = mapi_folder_createmessage($outbox);
472
+
473
+        // message properties to be set
474
+        $mapiprops = [];
475
+        // only save the outgoing in sent items folder if the mobile requests it
476
+        $mapiprops[$sendMailProps["sentmailentryid"]] = $storeprops[$sendMailProps["ipmsentmailentryid"]];
477
+
478
+        SLog::Write(LOGLEVEL_DEBUG, "Grommunio->SendMail(): Use the mapi_inetmapi_imtomapi function");
479
+        $ab = mapi_openaddressbook($this->session);
480
+        mapi_inetmapi_imtomapi($this->session, $this->defaultstore, $ab, $mapimessage, $sm->mime, []);
481
+
482
+        // Set the appSeqNr so that tracking tab can be updated for meeting request updates
483
+        // @see http://jira.zarafa.com/browse/ZP-68
484
+        $meetingRequestProps = MAPIMapping::GetMeetingRequestProperties();
485
+        $meetingRequestProps = getPropIdsFromStrings($this->defaultstore, $meetingRequestProps);
486
+        $props = mapi_getprops($mapimessage, [PR_MESSAGE_CLASS, $meetingRequestProps["goidtag"], $sendMailProps["internetcpid"], $sendMailProps["body"], $sendMailProps["html"], $sendMailProps["rtf"], $sendMailProps["rtfinsync"]]);
487
+
488
+        // Convert sent message's body to UTF-8 if it was a HTML message.
489
+        // @see http://jira.zarafa.com/browse/ZP-505 and http://jira.zarafa.com/browse/ZP-555
490
+        if (isset($props[$sendMailProps["internetcpid"]]) && $props[$sendMailProps["internetcpid"]] != INTERNET_CPID_UTF8 && MAPIUtils::GetNativeBodyType($props) == SYNC_BODYPREFERENCE_HTML) {
491
+            SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->SendMail(): Sent email cpid is not unicode (%d). Set it to unicode and convert email html body.", $props[$sendMailProps["internetcpid"]]));
492
+            $mapiprops[$sendMailProps["internetcpid"]] = INTERNET_CPID_UTF8;
493
+
494
+            $bodyHtml = MAPIUtils::readPropStream($mapimessage, PR_HTML);
495
+            $bodyHtml = Utils::ConvertCodepageStringToUtf8($props[$sendMailProps["internetcpid"]], $bodyHtml);
496
+            $mapiprops[$sendMailProps["html"]] = $bodyHtml;
497
+
498
+            mapi_setprops($mapimessage, $mapiprops);
499
+        }
500
+        if (stripos($props[PR_MESSAGE_CLASS], "IPM.Schedule.Meeting.Resp.") === 0) {
501
+            // search for calendar items using goid
502
+            $mr = new Meetingrequest($this->defaultstore, $mapimessage);
503
+            $appointments = $mr->findCalendarItems($props[$meetingRequestProps["goidtag"]]);
504
+            if (is_array($appointments) && !empty($appointments)) {
505
+                $app = mapi_msgstore_openentry($this->defaultstore, $appointments[0]);
506
+                $appprops = mapi_getprops($app, [$meetingRequestProps["appSeqNr"]]);
507
+                if (isset($appprops[$meetingRequestProps["appSeqNr"]]) && $appprops[$meetingRequestProps["appSeqNr"]]) {
508
+                    $mapiprops[$meetingRequestProps["appSeqNr"]] = $appprops[$meetingRequestProps["appSeqNr"]];
509
+                    SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->SendMail(): Set sequence number to:%d", $appprops[$meetingRequestProps["appSeqNr"]]));
510
+                }
511
+            }
512
+        }
513
+
514
+        // Delete the PR_SENT_REPRESENTING_* properties because some android devices
515
+        // do not send neither From nor Sender header causing empty PR_SENT_REPRESENTING_NAME and
516
+        // PR_SENT_REPRESENTING_EMAIL_ADDRESS properties and "broken" PR_SENT_REPRESENTING_ENTRYID
517
+        // which results in spooler not being able to send the message.
518
+        // @see http://jira.zarafa.com/browse/ZP-85
519
+        mapi_deleteprops(
520
+            $mapimessage,
521
+            [
522
+                $sendMailProps["sentrepresentingname"],
523
+                $sendMailProps["sentrepresentingemail"],
524
+                $sendMailProps["representingentryid"],
525
+                $sendMailProps["sentrepresentingaddt"],
526
+                $sendMailProps["sentrepresentinsrchk"],
527
+            ]
528
+        );
529
+
530
+        if (isset($sm->source->itemid) && $sm->source->itemid) {
531
+            // answering an email in a public/shared folder
532
+            // TODO as the store is setup, we should actually user $this->store instead of $this->defaultstore - nevertheless we need to make sure this store is able to send mail (has an outbox)
533
+            if (!$this->Setup(GSync::GetAdditionalSyncFolderStore($sm->source->folderid))) {
534
+                throw new StatusException(sprintf("Grommunio->SendMail() could not Setup() the backend for folder id '%s'", $sm->source->folderid), SYNC_COMMONSTATUS_SERVERERROR);
535
+            }
536
+
537
+            $entryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($sm->source->folderid), hex2bin($sm->source->itemid));
538
+            if ($entryid) {
539
+                $fwmessage = mapi_msgstore_openentry($this->store, $entryid);
540
+            }
541
+
542
+            if (isset($fwmessage) && $fwmessage) {
543
+                // update icon and last_verb when forwarding or replying message
544
+                // reply-all (verb 103) is not supported, as we cannot really detect this case
545
+                if ($sm->forwardflag) {
546
+                    $updateProps = [
547
+                        PR_ICON_INDEX => 262,
548
+                        PR_LAST_VERB_EXECUTED => 104,
549
+                    ];
550
+                }
551
+                elseif ($sm->replyflag) {
552
+                    $updateProps = [
553
+                        PR_ICON_INDEX => 261,
554
+                        PR_LAST_VERB_EXECUTED => 102,
555
+                    ];
556
+                }
557
+                if (isset($updateProps)) {
558
+                    $updateProps[PR_LAST_VERB_EXECUTION_TIME] = time();
559
+                    mapi_setprops($fwmessage, $updateProps);
560
+                    mapi_savechanges($fwmessage);
561
+                }
562
+
563
+                // only attach the original message if the mobile does not send it itself
564
+                if (!isset($sm->replacemime)) {
565
+                    // get message's body in order to append forward or reply text
566
+                    if (!isset($body)) {
567
+                        $body = MAPIUtils::readPropStream($mapimessage, PR_BODY);
568
+                    }
569
+                    if (!isset($bodyHtml)) {
570
+                        $bodyHtml = MAPIUtils::readPropStream($mapimessage, PR_HTML);
571
+                    }
572
+                    $cpid = mapi_getprops($fwmessage, [$sendMailProps["internetcpid"]]);
573
+                    if ($sm->forwardflag) {
574
+                        // attach the original attachments to the outgoing message
575
+                        $this->copyAttachments($mapimessage, $fwmessage);
576
+                    }
577
+
578
+                    // regarding the conversion @see ZP-470
579
+                    if (strlen($body) > 0) {
580
+                        $fwbody = MAPIUtils::readPropStream($fwmessage, PR_BODY);
581
+                        // if only the old message's cpid is set, convert from old charset to utf-8
582
+                        if (isset($cpid[$sendMailProps["internetcpid"]]) && $cpid[$sendMailProps["internetcpid"]] != INTERNET_CPID_UTF8) {
583
+                            SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->SendMail(): convert plain forwarded message charset (only fw set) from '%s' to '65001'", $cpid[$sendMailProps["internetcpid"]]));
584
+                            $fwbody = Utils::ConvertCodepageStringToUtf8($cpid[$sendMailProps["internetcpid"]], $fwbody);
585
+                        }
586
+                        // otherwise to the general conversion
587
+                        else {
588
+                            SLog::Write(LOGLEVEL_DEBUG, "Grommunio->SendMail(): no charset conversion done for plain forwarded message");
589
+                            $fwbody = w2u($fwbody);
590
+                        }
591
+
592
+                        $mapiprops[$sendMailProps["body"]] = $body . "\r\n\r\n" . $fwbody;
593
+                    }
594
+
595
+                    if (strlen($bodyHtml) > 0) {
596
+                        $fwbodyHtml = MAPIUtils::readPropStream($fwmessage, PR_HTML);
597
+                        // if only new message's cpid is set, convert to UTF-8
598
+                        if (isset($cpid[$sendMailProps["internetcpid"]]) && $cpid[$sendMailProps["internetcpid"]] != INTERNET_CPID_UTF8) {
599
+                            SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->SendMail(): convert html forwarded message charset (only fw set) from '%s' to '65001'", $cpid[$sendMailProps["internetcpid"]]));
600
+                            $fwbodyHtml = Utils::ConvertCodepageStringToUtf8($cpid[$sendMailProps["internetcpid"]], $fwbodyHtml);
601
+                        }
602
+                        // otherwise to the general conversion
603
+                        else {
604
+                            SLog::Write(LOGLEVEL_DEBUG, "Grommunio->SendMail(): no charset conversion done for html forwarded message");
605
+                            $fwbodyHtml = w2u($fwbodyHtml);
606
+                        }
607
+
608
+                        $mapiprops[$sendMailProps["html"]] = $bodyHtml . "<br><br>" . $fwbodyHtml;
609
+                    }
610
+                }
611
+            }
612
+            else {
613
+                // no fwmessage could be opened and we need it because we do not replace mime
614
+                if (!isset($sm->replacemime) || $sm->replacemime == false) {
615
+                    throw new StatusException(sprintf("Grommunio->SendMail(): Could not open message id '%s' in folder id '%s' to be replied/forwarded: 0x%X", $sm->source->itemid, $sm->source->folderid, mapi_last_hresult()), SYNC_COMMONSTATUS_ITEMNOTFOUND);
616
+                }
617
+            }
618
+        }
619
+
620
+        mapi_setprops($mapimessage, $mapiprops);
621
+        mapi_savechanges($mapimessage);
622
+        mapi_message_submitmessage($mapimessage);
623
+        $hr = mapi_last_hresult();
624
+
625
+        if ($hr) {
626
+            switch ($hr) {
627
+                case MAPI_E_STORE_FULL:
628
+                    $code = SYNC_COMMONSTATUS_MAILBOXQUOTAEXCEEDED;
629
+                    break;
630
+
631
+                default:
632
+                    $code = SYNC_COMMONSTATUS_MAILSUBMISSIONFAILED;
633
+                    break;
634
+            }
635
+
636
+            throw new StatusException(sprintf("Grommunio->SendMail(): Error saving/submitting the message to the Outbox: 0x%X", $hr), $code);
637
+        }
638
+
639
+        SLog::Write(LOGLEVEL_DEBUG, "Grommunio->SendMail(): email submitted");
640
+
641
+        return true;
642
+    }
643
+
644
+    /**
645
+     * Returns all available data of a single message.
646
+     *
647
+     * @param string            $folderid
648
+     * @param string            $id
649
+     * @param ContentParameters $contentparameters flag
650
+     *
651
+     * @throws StatusException
652
+     *
653
+     * @return object(SyncObject)
654
+     */
655
+    public function Fetch($folderid, $id, $contentparameters) {
656
+        // SEARCH fetches with folderid == false and PR_ENTRYID as ID
657
+        if (!$folderid) {
658
+            $entryid = hex2bin($id);
659
+            $sk = $id;
660
+        }
661
+        else {
662
+            // id might be in the new longid format, so we have to split it here
663
+            list($fsk, $sk) = Utils::SplitMessageId($id);
664
+            // get the entry id of the message
665
+            $entryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($folderid), hex2bin($sk));
666
+        }
667
+        if (!$entryid) {
668
+            throw new StatusException(sprintf("Grommunio->Fetch('%s','%s'): Error getting entryid: 0x%X", $folderid, $sk, mapi_last_hresult()), SYNC_STATUS_OBJECTNOTFOUND);
669
+        }
670
+
671
+        // open the message
672
+        $message = mapi_msgstore_openentry($this->store, $entryid);
673
+        if (!$message) {
674
+            throw new StatusException(sprintf("Grommunio->Fetch('%s','%s'): Error, unable to open message: 0x%X", $folderid, $sk, mapi_last_hresult()), SYNC_STATUS_OBJECTNOTFOUND);
675
+        }
676
+
677
+        // convert the mapi message into a SyncObject and return it
678
+        $mapiprovider = new MAPIProvider($this->session, $this->store);
679
+
680
+        // override truncation
681
+        $contentparameters->SetTruncation(SYNC_TRUNCATION_ALL);
682
+        // TODO check for body preferences
683
+        return $mapiprovider->GetMessage($message, $contentparameters);
684
+    }
685
+
686
+    /**
687
+     * Returns the waste basket.
688
+     *
689
+     * @return string
690
+     */
691
+    public function GetWasteBasket() {
692
+        if ($this->wastebasket) {
693
+            return $this->wastebasket;
694
+        }
695
+
696
+        $storeprops = mapi_getprops($this->defaultstore, [PR_IPM_WASTEBASKET_ENTRYID]);
697
+        if (isset($storeprops[PR_IPM_WASTEBASKET_ENTRYID])) {
698
+            $wastebasket = mapi_msgstore_openentry($this->defaultstore, $storeprops[PR_IPM_WASTEBASKET_ENTRYID]);
699
+            $wastebasketprops = mapi_getprops($wastebasket, [PR_SOURCE_KEY]);
700
+            if (isset($wastebasketprops[PR_SOURCE_KEY])) {
701
+                $this->wastebasket = bin2hex($wastebasketprops[PR_SOURCE_KEY]);
702
+                SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetWasteBasket(): Got waste basket with id '%s'", $this->wastebasket));
703
+
704
+                return $this->wastebasket;
705
+            }
706
+        }
707
+
708
+        return false;
709
+    }
710
+
711
+    /**
712
+     * Returns the content of the named attachment as stream.
713
+     *
714
+     * @param string $attname
715
+     *
716
+     * @throws StatusException
717
+     *
718
+     * @return SyncItemOperationsAttachment
719
+     */
720
+    public function GetAttachmentData($attname) {
721
+        SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetAttachmentData('%s')", $attname));
722
+
723
+        if (!strpos($attname, ":")) {
724
+            throw new StatusException(sprintf("Grommunio->GetAttachmentData('%s'): Error, attachment requested for non-existing item", $attname), SYNC_ITEMOPERATIONSSTATUS_INVALIDATT);
725
+        }
726
+
727
+        list($id, $attachnum, $parentEntryid) = explode(":", $attname);
728
+        if (isset($parentEntryid)) {
729
+            $this->Setup(GSync::GetAdditionalSyncFolderStore($parentEntryid));
730
+        }
731
+
732
+        $entryid = hex2bin($id);
733
+        $message = mapi_msgstore_openentry($this->store, $entryid);
734
+        if (!$message) {
735
+            throw new StatusException(sprintf("Grommunio->GetAttachmentData('%s'): Error, unable to open item for attachment data for id '%s' with: 0x%X", $attname, $id, mapi_last_hresult()), SYNC_ITEMOPERATIONSSTATUS_INVALIDATT);
736
+        }
737
+
738
+        MAPIUtils::ParseSmime($this->session, $this->defaultstore, $this->getAddressbook(), $message);
739
+        $attach = mapi_message_openattach($message, $attachnum);
740
+        if (!$attach) {
741
+            throw new StatusException(sprintf("Grommunio->GetAttachmentData('%s'): Error, unable to open attachment number '%s' with: 0x%X", $attname, $attachnum, mapi_last_hresult()), SYNC_ITEMOPERATIONSSTATUS_INVALIDATT);
742
+        }
743
+
744
+        // get necessary attachment props
745
+        $attprops = mapi_getprops($attach, [PR_ATTACH_MIME_TAG, PR_ATTACH_MIME_TAG_W, PR_ATTACH_METHOD]);
746
+        $attachment = new SyncItemOperationsAttachment();
747
+        // check if it's an embedded message and open it in such a case
748
+        if (isset($attprops[PR_ATTACH_METHOD]) && $attprops[PR_ATTACH_METHOD] == ATTACH_EMBEDDED_MSG) {
749
+            $embMessage = mapi_attach_openobj($attach);
750
+            $addrbook = $this->getAddressbook();
751
+            $stream = mapi_inetmapi_imtoinet($this->session, $addrbook, $embMessage, ['use_tnef' => -1]);
752
+            // set the default contenttype for this kind of messages
753
+            $attachment->contenttype = "message/rfc822";
754
+        }
755
+        else {
756
+            $stream = mapi_openproperty($attach, PR_ATTACH_DATA_BIN, IID_IStream, 0, 0);
757
+        }
758
+
759
+        if (!$stream) {
760
+            throw new StatusException(sprintf("Grommunio->GetAttachmentData('%s'): Error, unable to open attachment data stream: 0x%X", $attname, mapi_last_hresult()), SYNC_ITEMOPERATIONSSTATUS_INVALIDATT);
761
+        }
762
+
763
+        // put the mapi stream into a wrapper to get a standard stream
764
+        $attachment->data = MAPIStreamWrapper::Open($stream);
765
+        if (isset($attprops[PR_ATTACH_MIME_TAG])) {
766
+            $attachment->contenttype = $attprops[PR_ATTACH_MIME_TAG];
767
+        }
768
+        elseif (isset($attprops[PR_ATTACH_MIME_TAG_W])) {
769
+            $attachment->contenttype = $attprops[PR_ATTACH_MIME_TAG_W];
770
+        }
771
+        // TODO default contenttype
772
+        return $attachment;
773
+    }
774
+
775
+    /**
776
+     * Deletes all contents of the specified folder.
777
+     * This is generally used to empty the trash (wastebasked), but could also be used on any
778
+     * other folder.
779
+     *
780
+     * @param string $folderid
781
+     * @param bool   $includeSubfolders (opt) also delete sub folders, default true
782
+     *
783
+     * @throws StatusException
784
+     *
785
+     * @return bool
786
+     */
787
+    public function EmptyFolder($folderid, $includeSubfolders = true) {
788
+        $folderentryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($folderid));
789
+        if (!$folderentryid) {
790
+            throw new StatusException(sprintf("Grommunio->EmptyFolder('%s','%s'): Error, unable to open folder (no entry id)", $folderid, Utils::PrintAsString($includeSubfolders)), SYNC_ITEMOPERATIONSSTATUS_SERVERERROR);
791
+        }
792
+        $folder = mapi_msgstore_openentry($this->store, $folderentryid);
793
+
794
+        if (!$folder) {
795
+            throw new StatusException(sprintf("Grommunio->EmptyFolder('%s','%s'): Error, unable to open parent folder (open entry)", $folderid, Utils::PrintAsString($includeSubfolders)), SYNC_ITEMOPERATIONSSTATUS_SERVERERROR);
796
+        }
797
+
798
+        $flags = 0;
799
+        if ($includeSubfolders) {
800
+            $flags = DEL_ASSOCIATED;
801
+        }
802
+
803
+        SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->EmptyFolder('%s','%s'): emptying folder", $folderid, Utils::PrintAsString($includeSubfolders)));
804
+
805
+        // empty folder!
806
+        mapi_folder_emptyfolder($folder, $flags);
807
+        if (mapi_last_hresult()) {
808
+            throw new StatusException(sprintf("Grommunio->EmptyFolder('%s','%s'): Error, mapi_folder_emptyfolder() failed: 0x%X", $folderid, Utils::PrintAsString($includeSubfolders), mapi_last_hresult()), SYNC_ITEMOPERATIONSSTATUS_SERVERERROR);
809
+        }
810
+
811
+        return true;
812
+    }
813
+
814
+    /**
815
+     * Processes a response to a meeting request.
816
+     * CalendarID is a reference and has to be set if a new calendar item is created.
817
+     *
818
+     * @param string $requestid id of the object containing the request
819
+     * @param string $folderid  id of the parent folder of $requestid
820
+     * @param string $response
821
+     *
822
+     * @throws StatusException
823
+     *
824
+     * @return string id of the created/updated calendar obj
825
+     */
826
+    public function MeetingResponse($requestid, $folderid, $response) {
827
+        // Use standard meeting response code to process meeting request
828
+        list($fid, $requestid) = Utils::SplitMessageId($requestid);
829
+        $reqentryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($folderid), hex2bin($requestid));
830
+        if (!$reqentryid) {
831
+            throw new StatusException(sprintf("Grommunio->MeetingResponse('%s', '%s', '%s'): Error, unable to entryid of the message 0x%X", $requestid, $folderid, $response, mapi_last_hresult()), SYNC_MEETRESPSTATUS_INVALIDMEETREQ);
832
+        }
833
+
834
+        $mapimessage = mapi_msgstore_openentry($this->store, $reqentryid);
835
+        if (!$mapimessage) {
836
+            throw new StatusException(sprintf("Grommunio->MeetingResponse('%s','%s', '%s'): Error, unable to open request message for response 0x%X", $requestid, $folderid, $response, mapi_last_hresult()), SYNC_MEETRESPSTATUS_INVALIDMEETREQ);
837
+        }
838
+
839
+        // ios sends calendar item in MeetingResponse
840
+        // @see https://jira.z-hub.io/browse/ZP-1524
841
+        $folderClass = GSync::GetDeviceManager()->GetFolderClassFromCacheByID($fid);
842
+        // find the corresponding meeting request
843
+        if ($folderClass != 'Email') {
844
+            $props = MAPIMapping::GetMeetingRequestProperties();
845
+            $props = getPropIdsFromStrings($this->store, $props);
846
+
847
+            $messageprops = mapi_getprops($mapimessage, [$props["goidtag"]]);
848
+            $goid = $messageprops[$props["goidtag"]];
849
+
850
+            $mapiprovider = new MAPIProvider($this->session, $this->store);
851
+            $inboxprops = $mapiprovider->GetInboxProps();
852
+            $folder = mapi_msgstore_openentry($this->store, $inboxprops[PR_ENTRYID]);
853
+
854
+            // Find the item by restricting all items to the correct ID
855
+            $restrict = [RES_AND, [
856
+                [RES_PROPERTY,
857
+                    [
858
+                        RELOP => RELOP_EQ,
859
+                        ULPROPTAG => $props["goidtag"],
860
+                        VALUE => $goid,
861
+                    ],
862
+                ],
863
+            ]];
864
+
865
+            $inboxcontents = mapi_folder_getcontentstable($folder);
866
+
867
+            $rows = mapi_table_queryallrows($inboxcontents, [PR_ENTRYID], $restrict);
868
+            if (empty($rows)) {
869
+                throw new StatusException(sprintf("Grommunio->MeetingResponse('%s','%s', '%s'): Error, meeting request not found in the inbox", $requestid, $folderid, $response), SYNC_MEETRESPSTATUS_INVALIDMEETREQ);
870
+            }
871
+            SLog::Write(LOGLEVEL_DEBUG, "Grommunio->MeetingResponse found meeting request in the inbox");
872
+            $mapimessage = mapi_msgstore_openentry($this->store, $rows[0][PR_ENTRYID]);
873
+            $reqentryid = $rows[0][PR_ENTRYID];
874
+        }
875
+
876
+        $meetingrequest = new Meetingrequest($this->store, $mapimessage, $this->session);
877
+
878
+        if (!$meetingrequest->isMeetingRequest()) {
879
+            throw new StatusException(sprintf("Grommunio->MeetingResponse('%s','%s', '%s'): Error, attempt to respond to non-meeting request", $requestid, $folderid, $response), SYNC_MEETRESPSTATUS_INVALIDMEETREQ);
880
+        }
881
+
882
+        if ($meetingrequest->isLocalOrganiser()) {
883
+            throw new StatusException(sprintf("Grommunio->MeetingResponse('%s','%s', '%s'): Error, attempt to response to meeting request that we organized", $requestid, $folderid, $response), SYNC_MEETRESPSTATUS_INVALIDMEETREQ);
884
+        }
885
+
886
+        // Process the meeting response. We don't have to send the actual meeting response
887
+        // e-mail, because the device will send it itself. This seems not to be the case
888
+        // anymore for the ios devices since at least version 12.4. grommunio-sync will send the
889
+        // accepted email in such a case.
890
+        // @see https://jira.z-hub.io/browse/ZP-1524
891
+        $sendresponse = false;
892
+        $deviceType = strtolower(Request::GetDeviceType());
893
+        if ($deviceType == 'iphone' || $deviceType == 'ipad' || $deviceType == 'ipod') {
894
+            $matches = [];
895
+            if (preg_match("/^Apple-.*?\\/(\\d{4})\\./", Request::GetUserAgent(), $matches) && isset($matches[1]) && $matches[1] >= 1607) {
896
+                SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->MeetingResponse: iOS device %s->%s", Request::GetDeviceType(), Request::GetUserAgent()));
897
+                $sendresponse = true;
898
+            }
899
+        }
900
+
901
+        switch ($response) {
902
+            case 1:     // accept
903
+            default:
904
+                $entryid = $meetingrequest->doAccept(false, $sendresponse, false, false, false, false, true); // last true is the $userAction
905
+                break;
906
+
907
+            case 2:        // tentative
908
+                $entryid = $meetingrequest->doAccept(true, $sendresponse, false, false, false, false, true); // last true is the $userAction
909
+                break;
910
+
911
+            case 3:        // decline
912
+                $meetingrequest->doDecline(false);
913
+                break;
914
+        }
915
+
916
+        // F/B will be updated on logoff
917
+
918
+        // We have to return the ID of the new calendar item, so do that here
919
+        $calendarid = "";
920
+        $calFolderId = "";
921
+        if (isset($entryid)) {
922
+            $newitem = mapi_msgstore_openentry($this->store, $entryid);
923
+            // new item might be in a delegator's store. ActiveSync does not support accepting them.
924
+            if (!$newitem) {
925
+                throw new StatusException(sprintf("Grommunio->MeetingResponse('%s','%s', '%s'): Object with entryid '%s' was not found in user's store (0x%X). It might be in a delegator's store.", $requestid, $folderid, $response, bin2hex($entryid), mapi_last_hresult()), SYNC_MEETRESPSTATUS_SERVERERROR, null, LOGLEVEL_WARN);
926
+            }
927
+
928
+            $newprops = mapi_getprops($newitem, [PR_SOURCE_KEY, PR_PARENT_SOURCE_KEY]);
929
+            $calendarid = bin2hex($newprops[PR_SOURCE_KEY]);
930
+            $calFolderId = bin2hex($newprops[PR_PARENT_SOURCE_KEY]);
931
+        }
932
+
933
+        // on recurring items, the MeetingRequest class responds with a wrong entryid
934
+        if ($requestid == $calendarid) {
935
+            SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->MeetingResponse('%s','%s', '%s'): returned calendar id is the same as the requestid - re-searching", $requestid, $folderid, $response));
936
+
937
+            if (empty($props)) {
938
+                $props = MAPIMapping::GetMeetingRequestProperties();
939
+                $props = getPropIdsFromStrings($this->store, $props);
940
+
941
+                $messageprops = mapi_getprops($mapimessage, [$props["goidtag"]]);
942
+                $goid = $messageprops[$props["goidtag"]];
943
+            }
944
+
945
+            $items = $meetingrequest->findCalendarItems($goid);
946
+
947
+            if (is_array($items)) {
948
+                $newitem = mapi_msgstore_openentry($this->store, $items[0]);
949
+                $newprops = mapi_getprops($newitem, [PR_SOURCE_KEY, PR_PARENT_SOURCE_KEY]);
950
+                $calendarid = bin2hex($newprops[PR_SOURCE_KEY]);
951
+                $calFolderId = bin2hex($newprops[PR_PARENT_SOURCE_KEY]);
952
+                SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->MeetingResponse('%s','%s', '%s'): found other calendar entryid", $requestid, $folderid, $response));
953
+            }
954
+
955
+            if ($requestid == $calendarid) {
956
+                throw new StatusException(sprintf("Grommunio->MeetingResponse('%s','%s', '%s'): Error finding the accepted meeting response in the calendar", $requestid, $folderid, $response), SYNC_MEETRESPSTATUS_INVALIDMEETREQ);
957
+            }
958
+        }
959
+
960
+        // delete meeting request from Inbox
961
+        if ($folderClass == 'Email') {
962
+            $folderentryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($folderid));
963
+            $folder = mapi_msgstore_openentry($this->store, $folderentryid);
964
+        }
965
+        mapi_folder_deletemessages($folder, [$reqentryid], 0);
966
+
967
+        $prefix = '';
968
+        // prepend the short folderid of the target calendar: if available and short ids are used
969
+        if ($calFolderId) {
970
+            $shortFolderId = GSync::GetDeviceManager()->GetFolderIdForBackendId($calFolderId);
971
+            if ($calFolderId != $shortFolderId) {
972
+                $prefix = $shortFolderId . ':';
973
+            }
974
+        }
975
+
976
+        return $prefix . $calendarid;
977
+    }
978
+
979
+    /**
980
+     * Indicates if the backend has a ChangesSink.
981
+     * A sink is an active notification mechanism which does not need polling.
982
+     * Since Zarafa 7.0.5 such a sink is available.
983
+     * The grommunio backend uses this method to initialize the sink with mapi.
984
+     *
985
+     * @return bool
986
+     */
987
+    public function HasChangesSink() {
988
+        if (!$this->notifications) {
989
+            SLog::Write(LOGLEVEL_DEBUG, "Grommunio->HasChangesSink(): sink is not available");
990
+
991
+            return false;
992
+        }
993
+
994
+        $this->changesSink = @mapi_sink_create();
995
+
996
+        if (!$this->changesSink || mapi_last_hresult()) {
997
+            SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->HasChangesSink(): sink could not be created with  0x%X", mapi_last_hresult()));
998
+
999
+            return false;
1000
+        }
1001
+
1002
+        $this->changesSinkHierarchyHash = $this->getHierarchyHash();
1003
+        SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->HasChangesSink(): created - HierarchyHash: %s", $this->changesSinkHierarchyHash));
1004
+
1005
+        // advise the main store and also to check if the connection supports it
1006
+        return $this->adviseStoreToSink($this->defaultstore);
1007
+    }
1008
+
1009
+    /**
1010
+     * The folder should be considered by the sink.
1011
+     * Folders which were not initialized should not result in a notification
1012
+     * of IBackend->ChangesSink().
1013
+     *
1014
+     * @param string $folderid
1015
+     *
1016
+     * @return bool false if entryid can not be found for that folder
1017
+     */
1018
+    public function ChangesSinkInitialize($folderid) {
1019
+        SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->ChangesSinkInitialize(): folderid '%s'", $folderid));
1020
+
1021
+        $entryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($folderid));
1022
+        if (!$entryid) {
1023
+            return false;
1024
+        }
1025
+
1026
+        // add entryid to the monitored folders
1027
+        $this->changesSinkFolders[$entryid] = $folderid;
1028
+
1029
+        // advise the current store to the sink
1030
+        return $this->adviseStoreToSink($this->store);
1031
+    }
1032
+
1033
+    /**
1034
+     * The actual ChangesSink.
1035
+     * For max. the $timeout value this method should block and if no changes
1036
+     * are available return an empty array.
1037
+     * If changes are available a list of folderids is expected.
1038
+     *
1039
+     * @param int $timeout max. amount of seconds to block
1040
+     *
1041
+     * @return array
1042
+     */
1043
+    public function ChangesSink($timeout = 30) {
1044
+        // clear the folder stats cache
1045
+        unset($this->folderStatCache);
1046
+
1047
+        $notifications = [];
1048
+        $hierarchyNotifications = [];
1049
+        $sinkresult = @mapi_sink_timedwait($this->changesSink, $timeout * 1000);
1050
+
1051
+        if (!is_array($sinkresult)) {
1052
+            throw new StatusException("Grommunio->ChangesSink(): Sink returned invalid notification, aborting", SyncCollections::OBSOLETE_CONNECTION);
1053
+        }
1054
+
1055
+        // reverse array so that the changes on folders are before changes on messages and
1056
+        // it's possible to filter such notifications
1057
+        $sinkresult = array_reverse($sinkresult, true);
1058
+        foreach ($sinkresult as $sinknotif) {
1059
+            // add a notification on a folder
1060
+            if ($sinknotif['objtype'] == MAPI_FOLDER) {
1061
+                $hierarchyNotifications[$sinknotif['entryid']] = IBackend::HIERARCHYNOTIFICATION;
1062
+            }
1063
+            // change on a message, remove hierarchy notification
1064
+            if (isset($sinknotif['parentid']) && $sinknotif['objtype'] == MAPI_MESSAGE && isset($notifications[$sinknotif['parentid']])) {
1065
+                unset($hierarchyNotifications[$sinknotif['parentid']]);
1066
+            }
1067
+
1068
+            // TODO check if adding $sinknotif['objtype'] = MAPI_MESSAGE wouldn't break anything
1069
+            // check if something in the monitored folders changed
1070
+            if (isset($sinknotif['parentid']) && array_key_exists($sinknotif['parentid'], $this->changesSinkFolders)) {
1071
+                $notifications[] = $this->changesSinkFolders[$sinknotif['parentid']];
1072
+            }
1073
+            // deletes and moves
1074
+            if (isset($sinknotif['oldparentid']) && array_key_exists($sinknotif['oldparentid'], $this->changesSinkFolders)) {
1075
+                $notifications[] = $this->changesSinkFolders[$sinknotif['oldparentid']];
1076
+            }
1077
+        }
1078
+
1079
+        // validate hierarchy notifications by comparing the hierarchy hashes (too many false positives otherwise)
1080
+        if (!empty($hierarchyNotifications)) {
1081
+            $hash = $this->getHierarchyHash();
1082
+            if ($hash !== $this->changesSinkHierarchyHash) {
1083
+                SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->ChangesSink() Hierarchy notification, pending validation. New hierarchyHash: %s", $hash));
1084
+                $notifications[] = IBackend::HIERARCHYNOTIFICATION;
1085
+                $this->changesSinkHierarchyHash = $hash;
1086
+            }
1087
+        }
1088
+
1089
+        return $notifications;
1090
+    }
1091
+
1092
+    /**
1093
+     * Applies settings to and gets information from the device.
1094
+     *
1095
+     * @param SyncObject $settings (SyncOOF, SyncUserInformation, SyncRightsManagementTemplates possible)
1096
+     *
1097
+     * @return SyncObject $settings
1098
+     */
1099
+    public function Settings($settings) {
1100
+        if ($settings instanceof SyncOOF) {
1101
+            $this->settingsOOF($settings);
1102
+        }
1103
+
1104
+        if ($settings instanceof SyncUserInformation) {
1105
+            $this->settingsUserInformation($settings);
1106
+        }
1107
+
1108
+        if ($settings instanceof SyncRightsManagementTemplates) {
1109
+            $this->settingsRightsManagementTemplates($settings);
1110
+        }
1111
+
1112
+        return $settings;
1113
+    }
1114
+
1115
+    /**
1116
+     * Resolves recipients.
1117
+     *
1118
+     * @param SyncObject $resolveRecipients
1119
+     *
1120
+     * @return SyncObject $resolveRecipients
1121
+     */
1122
+    public function ResolveRecipients($resolveRecipients) {
1123
+        if ($resolveRecipients instanceof SyncResolveRecipients) {
1124
+            $resolveRecipients->status = SYNC_RESOLVERECIPSSTATUS_SUCCESS;
1125
+            $resolveRecipients->response = [];
1126
+            $resolveRecipientsOptions = new SyncResolveRecipientsOptions();
1127
+            $maxAmbiguousRecipients = self::MAXAMBIGUOUSRECIPIENTS;
1128
+
1129
+            if (isset($resolveRecipients->options)) {
1130
+                $resolveRecipientsOptions = $resolveRecipients->options;
1131
+                // only limit ambiguous recipients if the client requests it.
1132
+
1133
+                if (isset($resolveRecipientsOptions->maxambiguousrecipients) &&
1134
+                        $resolveRecipientsOptions->maxambiguousrecipients >= 0 &&
1135
+                        $resolveRecipientsOptions->maxambiguousrecipients <= self::MAXAMBIGUOUSRECIPIENTS) {
1136
+                    $maxAmbiguousRecipients = $resolveRecipientsOptions->maxambiguousrecipients;
1137
+                    SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->ResolveRecipients(): The client requested %d max ambiguous recipients to resolve.", $maxAmbiguousRecipients));
1138
+                }
1139
+            }
1140
+
1141
+            foreach ($resolveRecipients->to as $i => $to) {
1142
+                $response = new SyncResolveRecipientsResponse();
1143
+                $response->to = $to;
1144
+                $response->status = SYNC_RESOLVERECIPSSTATUS_SUCCESS;
1145
+
1146
+                // do not expand distlists here
1147
+                $recipient = $this->resolveRecipient($to, $maxAmbiguousRecipients, false);
1148
+                if (is_array($recipient) && !empty($recipient)) {
1149
+                    $response->recipientcount = 0;
1150
+                    foreach ($recipient as $entry) {
1151
+                        if ($entry instanceof SyncResolveRecipient) {
1152
+                            // certificates are already set. Unset them if they weren't required.
1153
+                            if (!isset($resolveRecipientsOptions->certificateretrieval)) {
1154
+                                unset($entry->certificates);
1155
+                            }
1156
+                            if (isset($resolveRecipientsOptions->availability)) {
1157
+                                if (!isset($resolveRecipientsOptions->starttime)) {
1158
+                                    // TODO error, the request must include a valid StartTime element value
1159
+                                }
1160
+                                $entry->availability = $this->getAvailability($to, $entry, $resolveRecipientsOptions);
1161
+                            }
1162
+                            if (isset($resolveRecipientsOptions->picture)) {
1163
+                                // TODO implement picture retrieval of the recipient
1164
+                            }
1165
+                            ++$response->recipientcount;
1166
+                            $response->recipient[] = $entry;
1167
+                        }
1168
+                        elseif (is_int($recipient)) {
1169
+                            $response->status = $recipient;
1170
+                        }
1171
+                    }
1172
+                }
1173
+
1174
+                $resolveRecipients->response[$i] = $response;
1175
+            }
1176
+
1177
+            return $resolveRecipients;
1178
+        }
1179
+
1180
+        SLog::Write(LOGLEVEL_WARN, "Grommunio->ResolveRecipients(): Not a valid SyncResolveRecipients object.");
1181
+        // return a SyncResolveRecipients object so that sync doesn't fail
1182
+        $r = new SyncResolveRecipients();
1183
+        $r->status = SYNC_RESOLVERECIPSSTATUS_PROTOCOLERROR;
1184
+
1185
+        return $r;
1186
+    }
1187
+
1188
+    /*----------------------------------------------------------------------------------------------------------
1189 1189
 	 * Implementation of the ISearchProvider interface
1190 1190
 	 */
1191 1191
 
1192
-	/**
1193
-	 * Indicates if a search type is supported by this SearchProvider
1194
-	 * Currently only the type ISearchProvider::SEARCH_GAL (Global Address List) is implemented.
1195
-	 *
1196
-	 * @param string $searchtype
1197
-	 *
1198
-	 * @return bool
1199
-	 */
1200
-	public function SupportsType($searchtype) {
1201
-		return ($searchtype == ISearchProvider::SEARCH_GAL) || ($searchtype == ISearchProvider::SEARCH_MAILBOX);
1202
-	}
1203
-
1204
-	/**
1205
-	 * Searches the GAB of Grommunio
1206
-	 * Can be overwritten globally by configuring a SearchBackend.
1207
-	 *
1208
-	 * @param string                       $searchquery   string to be searched for
1209
-	 * @param string                       $searchrange   specified searchrange
1210
-	 * @param SyncResolveRecipientsPicture $searchpicture limitations for picture
1211
-	 *
1212
-	 * @throws StatusException
1213
-	 *
1214
-	 * @return array search results
1215
-	 */
1216
-	public function GetGALSearchResults($searchquery, $searchrange, $searchpicture) {
1217
-		// only return users whose displayName or the username starts with $name
1218
-		// TODO: use PR_ANR for this restriction instead of PR_DISPLAY_NAME and PR_ACCOUNT
1219
-		$addrbook = $this->getAddressbook();
1220
-		// FIXME: create a function to get the adressbook contentstable
1221
-		if ($addrbook) {
1222
-			$ab_entryid = mapi_ab_getdefaultdir($addrbook);
1223
-		}
1224
-		if ($ab_entryid) {
1225
-			$ab_dir = mapi_ab_openentry($addrbook, $ab_entryid);
1226
-		}
1227
-		if ($ab_dir) {
1228
-			$table = mapi_folder_getcontentstable($ab_dir);
1229
-		}
1230
-
1231
-		if (!$table) {
1232
-			throw new StatusException(sprintf("Grommunio->GetGALSearchResults(): could not open addressbook: 0x%X", mapi_last_hresult()), SYNC_SEARCHSTATUS_STORE_CONNECTIONFAILED);
1233
-		}
1234
-
1235
-		$restriction = MAPIUtils::GetSearchRestriction(u2w($searchquery));
1236
-		mapi_table_restrict($table, $restriction);
1237
-		mapi_table_sort($table, [PR_DISPLAY_NAME => TABLE_SORT_ASCEND]);
1238
-
1239
-		if (mapi_last_hresult()) {
1240
-			throw new StatusException(sprintf("Grommunio->GetGALSearchResults(): could not apply restriction: 0x%X", mapi_last_hresult()), SYNC_SEARCHSTATUS_STORE_TOOCOMPLEX);
1241
-		}
1242
-
1243
-		// range for the search results, default symbian range end is 50, wm 99,
1244
-		// so we'll use that of nokia
1245
-		$rangestart = 0;
1246
-		$rangeend = 50;
1247
-
1248
-		if ($searchrange != '0') {
1249
-			$pos = strpos($searchrange, '-');
1250
-			$rangestart = substr($searchrange, 0, $pos);
1251
-			$rangeend = substr($searchrange, ($pos + 1));
1252
-		}
1253
-		$items = [];
1254
-
1255
-		$querycnt = mapi_table_getrowcount($table);
1256
-		// do not return more results as requested in range
1257
-		$querylimit = (($rangeend + 1) < $querycnt) ? ($rangeend + 1) : $querycnt;
1258
-
1259
-		if ($querycnt > 0) {
1260
-			$abentries = mapi_table_queryrows($table, [PR_ENTRYID, PR_ACCOUNT, PR_DISPLAY_NAME, PR_SMTP_ADDRESS, PR_BUSINESS_TELEPHONE_NUMBER, PR_GIVEN_NAME, PR_SURNAME, PR_MOBILE_TELEPHONE_NUMBER, PR_HOME_TELEPHONE_NUMBER, PR_TITLE, PR_COMPANY_NAME, PR_OFFICE_LOCATION, PR_EMS_AB_THUMBNAIL_PHOTO], $rangestart, $querylimit);
1261
-		}
1262
-
1263
-		for ($i = 0; $i < $querylimit; ++$i) {
1264
-			if (!isset($abentries[$i][PR_SMTP_ADDRESS])) {
1265
-				SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->GetGALSearchResults(): The GAL entry '%s' does not have an email address and will be ignored.", w2u($abentries[$i][PR_DISPLAY_NAME])));
1266
-
1267
-				continue;
1268
-			}
1269
-
1270
-			$items[$i][SYNC_GAL_DISPLAYNAME] = w2u($abentries[$i][PR_DISPLAY_NAME]);
1271
-
1272
-			if (strlen(trim($items[$i][SYNC_GAL_DISPLAYNAME])) == 0) {
1273
-				$items[$i][SYNC_GAL_DISPLAYNAME] = w2u($abentries[$i][PR_ACCOUNT]);
1274
-			}
1275
-
1276
-			$items[$i][SYNC_GAL_ALIAS] = w2u($abentries[$i][PR_ACCOUNT]);
1277
-			// it's not possible not get first and last name of an user
1278
-			// from the gab and user functions, so we just set lastname
1279
-			// to displayname and leave firstname unset
1280
-			// this was changed in Zarafa 6.40, so we try to get first and
1281
-			// last name and fall back to the old behaviour if these values are not set
1282
-			if (isset($abentries[$i][PR_GIVEN_NAME])) {
1283
-				$items[$i][SYNC_GAL_FIRSTNAME] = w2u($abentries[$i][PR_GIVEN_NAME]);
1284
-			}
1285
-			if (isset($abentries[$i][PR_SURNAME])) {
1286
-				$items[$i][SYNC_GAL_LASTNAME] = w2u($abentries[$i][PR_SURNAME]);
1287
-			}
1288
-
1289
-			if (!isset($items[$i][SYNC_GAL_LASTNAME])) {
1290
-				$items[$i][SYNC_GAL_LASTNAME] = $items[$i][SYNC_GAL_DISPLAYNAME];
1291
-			}
1292
-
1293
-			$items[$i][SYNC_GAL_EMAILADDRESS] = w2u($abentries[$i][PR_SMTP_ADDRESS]);
1294
-			// check if an user has an office number or it might produce warnings in the log
1295
-			if (isset($abentries[$i][PR_BUSINESS_TELEPHONE_NUMBER])) {
1296
-				$items[$i][SYNC_GAL_PHONE] = w2u($abentries[$i][PR_BUSINESS_TELEPHONE_NUMBER]);
1297
-			}
1298
-			// check if an user has a mobile number or it might produce warnings in the log
1299
-			if (isset($abentries[$i][PR_MOBILE_TELEPHONE_NUMBER])) {
1300
-				$items[$i][SYNC_GAL_MOBILEPHONE] = w2u($abentries[$i][PR_MOBILE_TELEPHONE_NUMBER]);
1301
-			}
1302
-			// check if an user has a home number or it might produce warnings in the log
1303
-			if (isset($abentries[$i][PR_HOME_TELEPHONE_NUMBER])) {
1304
-				$items[$i][SYNC_GAL_HOMEPHONE] = w2u($abentries[$i][PR_HOME_TELEPHONE_NUMBER]);
1305
-			}
1306
-
1307
-			if (isset($abentries[$i][PR_COMPANY_NAME])) {
1308
-				$items[$i][SYNC_GAL_COMPANY] = w2u($abentries[$i][PR_COMPANY_NAME]);
1309
-			}
1310
-
1311
-			if (isset($abentries[$i][PR_TITLE])) {
1312
-				$items[$i][SYNC_GAL_TITLE] = w2u($abentries[$i][PR_TITLE]);
1313
-			}
1314
-
1315
-			if (isset($abentries[$i][PR_OFFICE_LOCATION])) {
1316
-				$items[$i][SYNC_GAL_OFFICE] = w2u($abentries[$i][PR_OFFICE_LOCATION]);
1317
-			}
1318
-
1319
-			if ($searchpicture !== false && isset($abentries[$i][PR_EMS_AB_THUMBNAIL_PHOTO])) {
1320
-				$items[$i][SYNC_GAL_PICTURE] = StringStreamWrapper::Open($abentries[$i][PR_EMS_AB_THUMBNAIL_PHOTO]);
1321
-			}
1322
-		}
1323
-		$nrResults = count($items);
1324
-		$items['range'] = ($nrResults > 0) ? $rangestart . '-' . ($nrResults - 1) : '0-0';
1325
-		$items['searchtotal'] = $nrResults;
1326
-
1327
-		return $items;
1328
-	}
1329
-
1330
-	/**
1331
-	 * Searches for the emails on the server.
1332
-	 *
1333
-	 * @param ContentParameter $cpo
1334
-	 *
1335
-	 * @return array
1336
-	 */
1337
-	public function GetMailboxSearchResults($cpo) {
1338
-		$searchFolder = $this->getSearchFolder();
1339
-		$searchRestriction = $this->getSearchRestriction($cpo);
1340
-		$searchRange = explode('-', $cpo->GetSearchRange());
1341
-		$searchFolderId = $cpo->GetSearchFolderid();
1342
-		$searchFolders = [];
1343
-		// search only in required folders
1344
-		if (!empty($searchFolderId)) {
1345
-			$searchFolderEntryId = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($searchFolderId));
1346
-			$searchFolders[] = $searchFolderEntryId;
1347
-		}
1348
-		// if no folder was required then search in the entire store
1349
-		else {
1350
-			$tmp = mapi_getprops($this->store, [PR_ENTRYID, PR_DISPLAY_NAME, PR_IPM_SUBTREE_ENTRYID]);
1351
-			$searchFolders[] = $tmp[PR_IPM_SUBTREE_ENTRYID];
1352
-		}
1353
-		$items = [];
1354
-		$flags = 0;
1355
-		// if subfolders are required, do a recursive search
1356
-		if ($cpo->GetSearchDeepTraversal()) {
1357
-			$flags |= SEARCH_RECURSIVE;
1358
-		}
1359
-
1360
-		mapi_folder_setsearchcriteria($searchFolder, $searchRestriction, $searchFolders, $flags);
1361
-
1362
-		$table = mapi_folder_getcontentstable($searchFolder);
1363
-		$searchStart = time();
1364
-		// do the search and wait for all the results available
1365
-		while (time() - $searchStart < SEARCH_WAIT) {
1366
-			$searchcriteria = mapi_folder_getsearchcriteria($searchFolder);
1367
-			if (($searchcriteria["searchstate"] & SEARCH_REBUILD) == 0) {
1368
-				break;
1369
-			} // Search is done
1370
-			sleep(1);
1371
-		}
1372
-
1373
-		// if the search range is set limit the result to it, otherwise return all found messages
1374
-		$rows = (is_array($searchRange) && isset($searchRange[0], $searchRange[1])) ?
1375
-			mapi_table_queryrows($table, [PR_ENTRYID], $searchRange[0], $searchRange[1] - $searchRange[0] + 1) :
1376
-			mapi_table_queryrows($table, [PR_ENTRYID], 0, SEARCH_MAXRESULTS);
1377
-
1378
-		$cnt = count($rows);
1379
-		$items['searchtotal'] = $cnt;
1380
-		$items["range"] = $cpo->GetSearchRange();
1381
-		for ($i = 0; $i < $cnt; ++$i) {
1382
-			$items[$i]['class'] = 'Email';
1383
-			$items[$i]['longid'] = bin2hex($rows[$i][PR_ENTRYID]);
1384
-			// $items[$i]['folderid'] = bin2hex($rows[$i][PR_PARENT_SOURCE_KEY]);
1385
-		}
1386
-
1387
-		return $items;
1388
-	}
1389
-
1390
-	/**
1391
-	 * Terminates a search for a given PID.
1392
-	 *
1393
-	 * @param int $pid
1394
-	 *
1395
-	 * @return bool
1396
-	 */
1397
-	public function TerminateSearch($pid) {
1398
-		SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->TerminateSearch(): terminating search for pid %d", $pid));
1399
-		if (!isset($this->store) || $this->store === false) {
1400
-			SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->TerminateSearch(): The store is not available. It is not possible to remove search folder with pid %d", $pid));
1401
-
1402
-			return false;
1403
-		}
1404
-
1405
-		$storeProps = mapi_getprops($this->store, [PR_STORE_SUPPORT_MASK, PR_FINDER_ENTRYID]);
1406
-		if (($storeProps[PR_STORE_SUPPORT_MASK] & STORE_SEARCH_OK) != STORE_SEARCH_OK) {
1407
-			SLog::Write(LOGLEVEL_WARN, "Grommunio->TerminateSearch(): Store doesn't support search folders. Public store doesn't have FINDER_ROOT folder");
1408
-
1409
-			return false;
1410
-		}
1411
-
1412
-		$finderfolder = mapi_msgstore_openentry($this->store, $storeProps[PR_FINDER_ENTRYID]);
1413
-		if (mapi_last_hresult() != NOERROR) {
1414
-			SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->TerminateSearch(): Unable to open search folder (0x%X)", mapi_last_hresult()));
1415
-
1416
-			return false;
1417
-		}
1418
-
1419
-		$hierarchytable = mapi_folder_gethierarchytable($finderfolder);
1420
-		mapi_table_restrict(
1421
-			$hierarchytable,
1422
-			[RES_CONTENT,
1423
-				[
1424
-					FUZZYLEVEL => FL_PREFIX,
1425
-					ULPROPTAG => PR_DISPLAY_NAME,
1426
-					VALUE => [PR_DISPLAY_NAME => "grommunio-sync Search Folder " . $pid],
1427
-				],
1428
-			],
1429
-			TBL_BATCH
1430
-		);
1431
-
1432
-		$folders = mapi_table_queryallrows($hierarchytable, [PR_ENTRYID, PR_DISPLAY_NAME, PR_LAST_MODIFICATION_TIME]);
1433
-		foreach ($folders as $folder) {
1434
-			mapi_folder_deletefolder($finderfolder, $folder[PR_ENTRYID]);
1435
-		}
1436
-
1437
-		return true;
1438
-	}
1439
-
1440
-	/**
1441
-	 * Disconnects from the current search provider.
1442
-	 *
1443
-	 * @return bool
1444
-	 */
1445
-	public function Disconnect() {
1446
-		return true;
1447
-	}
1448
-
1449
-	/**
1450
-	 * Returns the MAPI store resource for a folderid
1451
-	 * This is not part of IBackend but necessary for the ImportChangesICS->MoveMessage() operation if
1452
-	 * the destination folder is not in the default store
1453
-	 * Note: The current backend store might be changed as IBackend->Setup() is executed.
1454
-	 *
1455
-	 * @param string $store    target store, could contain a "domain\user" value - if empty default store is returned
1456
-	 * @param string $folderid
1457
-	 *
1458
-	 * @return Resource/boolean
1459
-	 */
1460
-	public function GetMAPIStoreForFolderId($store, $folderid) {
1461
-		if ($store == false) {
1462
-			SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetMAPIStoreForFolderId('%s', '%s'): no store specified, returning default store", $store, $folderid));
1463
-
1464
-			return $this->defaultstore;
1465
-		}
1466
-
1467
-		// setup the correct store
1468
-		if ($this->Setup($store, false, $folderid)) {
1469
-			return $this->store;
1470
-		}
1471
-
1472
-		SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->GetMAPIStoreForFolderId('%s', '%s'): store is not available", $store, $folderid));
1473
-
1474
-		return false;
1475
-	}
1476
-
1477
-	/**
1478
-	 * Returns the email address and the display name of the user. Used by autodiscover.
1479
-	 *
1480
-	 * @param string $username The username
1481
-	 *
1482
-	 * @return array
1483
-	 */
1484
-	public function GetUserDetails($username) {
1485
-		SLog::Write(LOGLEVEL_WBXML, sprintf("Grommunio->GetUserDetails for '%s'.", $username));
1486
-		$zarafauserinfo = @nsp_getuserinfo($username);
1487
-		$userDetails['emailaddress'] = (isset($zarafauserinfo['primary_email']) && $zarafauserinfo['primary_email']) ? $zarafauserinfo['primary_email'] : false;
1488
-		$userDetails['fullname'] = (isset($zarafauserinfo['fullname']) && $zarafauserinfo['fullname']) ? $zarafauserinfo['fullname'] : false;
1489
-
1490
-		return $userDetails;
1491
-	}
1492
-
1493
-	/**
1494
-	 * Returns the username of the currently active user.
1495
-	 *
1496
-	 * @return string
1497
-	 */
1498
-	public function GetCurrentUsername() {
1499
-		return $this->storeName;
1500
-	}
1501
-
1502
-	/**
1503
-	 * Returns the impersonated user name.
1504
-	 *
1505
-	 * @return string or false if no user is impersonated
1506
-	 */
1507
-	public function GetImpersonatedUser() {
1508
-		return $this->impersonateUser;
1509
-	}
1510
-
1511
-	/**
1512
-	 * Returns the authenticated user name.
1513
-	 *
1514
-	 * @return string
1515
-	 */
1516
-	public function GetMainUser() {
1517
-		return $this->mainUser;
1518
-	}
1519
-
1520
-	/**
1521
-	 * Indicates if the Backend supports folder statistics.
1522
-	 *
1523
-	 * @return bool
1524
-	 */
1525
-	public function HasFolderStats() {
1526
-		return true;
1527
-	}
1528
-
1529
-	/**
1530
-	 * Returns a status indication of the folder.
1531
-	 * If there are changes in the folder, the returned value must change.
1532
-	 * The returned values are compared with '===' to determine if a folder needs synchronization or not.
1533
-	 *
1534
-	 * @param string $store    the store where the folder resides
1535
-	 * @param string $folderid the folder id
1536
-	 *
1537
-	 * @return string
1538
-	 */
1539
-	public function GetFolderStat($store, $folderid) {
1540
-		list($user, $domain) = Utils::SplitDomainUser($store);
1541
-		if ($user === false) {
1542
-			$user = $this->mainUser;
1543
-			if ($this->impersonateUser) {
1544
-				$user = $this->impersonateUser;
1545
-			}
1546
-		}
1547
-
1548
-		if (!isset($this->folderStatCache[$user])) {
1549
-			$this->folderStatCache[$user] = [];
1550
-		}
1551
-
1552
-		// if there is nothing in the cache for a store, load the data for all folders of it
1553
-		if (empty($this->folderStatCache[$user])) {
1554
-			// get the store
1555
-			$userstore = $this->openMessageStore($user);
1556
-			$rootfolder = mapi_msgstore_openentry($userstore);
1557
-			$hierarchy = mapi_folder_gethierarchytable($rootfolder, CONVENIENT_DEPTH);
1558
-			$rows = mapi_table_queryallrows($hierarchy, [PR_SOURCE_KEY, PR_LOCAL_COMMIT_TIME_MAX, PR_CONTENT_COUNT, PR_CONTENT_UNREAD, PR_DELETED_MSG_COUNT]);
1559
-
1560
-			if (count($rows) == 0) {
1561
-				SLog::Write(LOGLEVEL_INFO, sprintf("Grommunio->GetFolderStat(): could not access folder statistics for user '%s'. Probably missing 'read' permissions on the root folder! Folders of this store will be synchronized ONCE per hour only!", $user));
1562
-			}
1563
-
1564
-			foreach ($rows as $folder) {
1565
-				$commit_time = isset($folder[PR_LOCAL_COMMIT_TIME_MAX]) ? $folder[PR_LOCAL_COMMIT_TIME_MAX] : "0000000000";
1566
-				$content_count = isset($folder[PR_CONTENT_COUNT]) ? $folder[PR_CONTENT_COUNT] : -1;
1567
-				$content_unread = isset($folder[PR_CONTENT_UNREAD]) ? $folder[PR_CONTENT_UNREAD] : -1;
1568
-				$content_deleted = isset($folder[PR_DELETED_MSG_COUNT]) ? $folder[PR_DELETED_MSG_COUNT] : -1;
1569
-
1570
-				$this->folderStatCache[$user][bin2hex($folder[PR_SOURCE_KEY])] = $commit_time . "/" . $content_count . "/" . $content_unread . "/" . $content_deleted;
1571
-			}
1572
-			SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetFolderStat() fetched status information of %d folders for store '%s'", count($this->folderStatCache[$user]), $user));
1573
-		}
1574
-
1575
-		if (isset($this->folderStatCache[$user][$folderid])) {
1576
-			return $this->folderStatCache[$user][$folderid];
1577
-		}
1578
-
1579
-		// a timestamp that changes once per hour is returned in case there is no data found for this folder. It will be synchronized only once per hour.
1580
-		return gmdate("Y-m-d-H");
1581
-	}
1582
-
1583
-	/**
1584
-	 * Returns information about the user's store:
1585
-	 * number of folders, store size, full name, email address.
1586
-	 *
1587
-	 * @return UserStoreInfo
1588
-	 */
1589
-	public function GetUserStoreInfo() {
1590
-		$userStoreInfo = new UserStoreInfo();
1591
-
1592
-		$rootfolder = mapi_msgstore_openentry($this->store);
1593
-		$hierarchy = mapi_folder_gethierarchytable($rootfolder, CONVENIENT_DEPTH);
1594
-		// Do not take hidden and system folders into account
1595
-		// TODO make this restriction generic and use for hierarchy?
1596
-		$restrict = [
1597
-			RES_AND,
1598
-			[
1599
-				[
1600
-					RES_PROPERTY,
1601
-					[
1602
-						RELOP => RELOP_NE,
1603
-						ULPROPTAG => PR_ATTR_HIDDEN,
1604
-						VALUE => true, ],
1605
-				],
1606
-				[
1607
-					RES_PROPERTY,
1608
-					[
1609
-						RELOP => RELOP_EQ,
1610
-						ULPROPTAG => PR_FOLDER_TYPE,
1611
-						VALUE => FOLDER_GENERIC, ],
1612
-				],
1613
-				[
1614
-					RES_EXIST,
1615
-					[ULPROPTAG => PR_CONTAINER_CLASS],
1616
-				],
1617
-			], ];
1618
-		mapi_table_restrict($hierarchy, $restrict);
1619
-		$foldercount = mapi_table_getrowcount($hierarchy);
1620
-
1621
-		$storeProps = mapi_getprops($this->store, [PR_MESSAGE_SIZE_EXTENDED]);
1622
-		$storesize = isset($storeProps[PR_MESSAGE_SIZE_EXTENDED]) ? $storeProps[PR_MESSAGE_SIZE_EXTENDED] : 0;
1623
-
1624
-		$userDetails = $this->GetUserDetails($this->impersonateUser ?: $this->mainUser);
1625
-		$userStoreInfo->SetData($foldercount, $storesize, $userDetails['fullname'], $userDetails['emailaddress']);
1626
-		SLog::Write(LOGLEVEL_DEBUG, sprintf(
1627
-			"Grommunio->GetUserStoreInfo(): user %s (%s) store size is %d bytes and contains %d folders",
1628
-			Utils::PrintAsString($userDetails['fullname']),
1629
-			Utils::PrintAsString($userDetails['emailaddress']),
1630
-			$storesize,
1631
-			$foldercount
1632
-		));
1633
-
1634
-		return $userStoreInfo;
1635
-	}
1636
-
1637
-	/*----------------------------------------------------------------------------------------------------------
1192
+    /**
1193
+     * Indicates if a search type is supported by this SearchProvider
1194
+     * Currently only the type ISearchProvider::SEARCH_GAL (Global Address List) is implemented.
1195
+     *
1196
+     * @param string $searchtype
1197
+     *
1198
+     * @return bool
1199
+     */
1200
+    public function SupportsType($searchtype) {
1201
+        return ($searchtype == ISearchProvider::SEARCH_GAL) || ($searchtype == ISearchProvider::SEARCH_MAILBOX);
1202
+    }
1203
+
1204
+    /**
1205
+     * Searches the GAB of Grommunio
1206
+     * Can be overwritten globally by configuring a SearchBackend.
1207
+     *
1208
+     * @param string                       $searchquery   string to be searched for
1209
+     * @param string                       $searchrange   specified searchrange
1210
+     * @param SyncResolveRecipientsPicture $searchpicture limitations for picture
1211
+     *
1212
+     * @throws StatusException
1213
+     *
1214
+     * @return array search results
1215
+     */
1216
+    public function GetGALSearchResults($searchquery, $searchrange, $searchpicture) {
1217
+        // only return users whose displayName or the username starts with $name
1218
+        // TODO: use PR_ANR for this restriction instead of PR_DISPLAY_NAME and PR_ACCOUNT
1219
+        $addrbook = $this->getAddressbook();
1220
+        // FIXME: create a function to get the adressbook contentstable
1221
+        if ($addrbook) {
1222
+            $ab_entryid = mapi_ab_getdefaultdir($addrbook);
1223
+        }
1224
+        if ($ab_entryid) {
1225
+            $ab_dir = mapi_ab_openentry($addrbook, $ab_entryid);
1226
+        }
1227
+        if ($ab_dir) {
1228
+            $table = mapi_folder_getcontentstable($ab_dir);
1229
+        }
1230
+
1231
+        if (!$table) {
1232
+            throw new StatusException(sprintf("Grommunio->GetGALSearchResults(): could not open addressbook: 0x%X", mapi_last_hresult()), SYNC_SEARCHSTATUS_STORE_CONNECTIONFAILED);
1233
+        }
1234
+
1235
+        $restriction = MAPIUtils::GetSearchRestriction(u2w($searchquery));
1236
+        mapi_table_restrict($table, $restriction);
1237
+        mapi_table_sort($table, [PR_DISPLAY_NAME => TABLE_SORT_ASCEND]);
1238
+
1239
+        if (mapi_last_hresult()) {
1240
+            throw new StatusException(sprintf("Grommunio->GetGALSearchResults(): could not apply restriction: 0x%X", mapi_last_hresult()), SYNC_SEARCHSTATUS_STORE_TOOCOMPLEX);
1241
+        }
1242
+
1243
+        // range for the search results, default symbian range end is 50, wm 99,
1244
+        // so we'll use that of nokia
1245
+        $rangestart = 0;
1246
+        $rangeend = 50;
1247
+
1248
+        if ($searchrange != '0') {
1249
+            $pos = strpos($searchrange, '-');
1250
+            $rangestart = substr($searchrange, 0, $pos);
1251
+            $rangeend = substr($searchrange, ($pos + 1));
1252
+        }
1253
+        $items = [];
1254
+
1255
+        $querycnt = mapi_table_getrowcount($table);
1256
+        // do not return more results as requested in range
1257
+        $querylimit = (($rangeend + 1) < $querycnt) ? ($rangeend + 1) : $querycnt;
1258
+
1259
+        if ($querycnt > 0) {
1260
+            $abentries = mapi_table_queryrows($table, [PR_ENTRYID, PR_ACCOUNT, PR_DISPLAY_NAME, PR_SMTP_ADDRESS, PR_BUSINESS_TELEPHONE_NUMBER, PR_GIVEN_NAME, PR_SURNAME, PR_MOBILE_TELEPHONE_NUMBER, PR_HOME_TELEPHONE_NUMBER, PR_TITLE, PR_COMPANY_NAME, PR_OFFICE_LOCATION, PR_EMS_AB_THUMBNAIL_PHOTO], $rangestart, $querylimit);
1261
+        }
1262
+
1263
+        for ($i = 0; $i < $querylimit; ++$i) {
1264
+            if (!isset($abentries[$i][PR_SMTP_ADDRESS])) {
1265
+                SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->GetGALSearchResults(): The GAL entry '%s' does not have an email address and will be ignored.", w2u($abentries[$i][PR_DISPLAY_NAME])));
1266
+
1267
+                continue;
1268
+            }
1269
+
1270
+            $items[$i][SYNC_GAL_DISPLAYNAME] = w2u($abentries[$i][PR_DISPLAY_NAME]);
1271
+
1272
+            if (strlen(trim($items[$i][SYNC_GAL_DISPLAYNAME])) == 0) {
1273
+                $items[$i][SYNC_GAL_DISPLAYNAME] = w2u($abentries[$i][PR_ACCOUNT]);
1274
+            }
1275
+
1276
+            $items[$i][SYNC_GAL_ALIAS] = w2u($abentries[$i][PR_ACCOUNT]);
1277
+            // it's not possible not get first and last name of an user
1278
+            // from the gab and user functions, so we just set lastname
1279
+            // to displayname and leave firstname unset
1280
+            // this was changed in Zarafa 6.40, so we try to get first and
1281
+            // last name and fall back to the old behaviour if these values are not set
1282
+            if (isset($abentries[$i][PR_GIVEN_NAME])) {
1283
+                $items[$i][SYNC_GAL_FIRSTNAME] = w2u($abentries[$i][PR_GIVEN_NAME]);
1284
+            }
1285
+            if (isset($abentries[$i][PR_SURNAME])) {
1286
+                $items[$i][SYNC_GAL_LASTNAME] = w2u($abentries[$i][PR_SURNAME]);
1287
+            }
1288
+
1289
+            if (!isset($items[$i][SYNC_GAL_LASTNAME])) {
1290
+                $items[$i][SYNC_GAL_LASTNAME] = $items[$i][SYNC_GAL_DISPLAYNAME];
1291
+            }
1292
+
1293
+            $items[$i][SYNC_GAL_EMAILADDRESS] = w2u($abentries[$i][PR_SMTP_ADDRESS]);
1294
+            // check if an user has an office number or it might produce warnings in the log
1295
+            if (isset($abentries[$i][PR_BUSINESS_TELEPHONE_NUMBER])) {
1296
+                $items[$i][SYNC_GAL_PHONE] = w2u($abentries[$i][PR_BUSINESS_TELEPHONE_NUMBER]);
1297
+            }
1298
+            // check if an user has a mobile number or it might produce warnings in the log
1299
+            if (isset($abentries[$i][PR_MOBILE_TELEPHONE_NUMBER])) {
1300
+                $items[$i][SYNC_GAL_MOBILEPHONE] = w2u($abentries[$i][PR_MOBILE_TELEPHONE_NUMBER]);
1301
+            }
1302
+            // check if an user has a home number or it might produce warnings in the log
1303
+            if (isset($abentries[$i][PR_HOME_TELEPHONE_NUMBER])) {
1304
+                $items[$i][SYNC_GAL_HOMEPHONE] = w2u($abentries[$i][PR_HOME_TELEPHONE_NUMBER]);
1305
+            }
1306
+
1307
+            if (isset($abentries[$i][PR_COMPANY_NAME])) {
1308
+                $items[$i][SYNC_GAL_COMPANY] = w2u($abentries[$i][PR_COMPANY_NAME]);
1309
+            }
1310
+
1311
+            if (isset($abentries[$i][PR_TITLE])) {
1312
+                $items[$i][SYNC_GAL_TITLE] = w2u($abentries[$i][PR_TITLE]);
1313
+            }
1314
+
1315
+            if (isset($abentries[$i][PR_OFFICE_LOCATION])) {
1316
+                $items[$i][SYNC_GAL_OFFICE] = w2u($abentries[$i][PR_OFFICE_LOCATION]);
1317
+            }
1318
+
1319
+            if ($searchpicture !== false && isset($abentries[$i][PR_EMS_AB_THUMBNAIL_PHOTO])) {
1320
+                $items[$i][SYNC_GAL_PICTURE] = StringStreamWrapper::Open($abentries[$i][PR_EMS_AB_THUMBNAIL_PHOTO]);
1321
+            }
1322
+        }
1323
+        $nrResults = count($items);
1324
+        $items['range'] = ($nrResults > 0) ? $rangestart . '-' . ($nrResults - 1) : '0-0';
1325
+        $items['searchtotal'] = $nrResults;
1326
+
1327
+        return $items;
1328
+    }
1329
+
1330
+    /**
1331
+     * Searches for the emails on the server.
1332
+     *
1333
+     * @param ContentParameter $cpo
1334
+     *
1335
+     * @return array
1336
+     */
1337
+    public function GetMailboxSearchResults($cpo) {
1338
+        $searchFolder = $this->getSearchFolder();
1339
+        $searchRestriction = $this->getSearchRestriction($cpo);
1340
+        $searchRange = explode('-', $cpo->GetSearchRange());
1341
+        $searchFolderId = $cpo->GetSearchFolderid();
1342
+        $searchFolders = [];
1343
+        // search only in required folders
1344
+        if (!empty($searchFolderId)) {
1345
+            $searchFolderEntryId = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($searchFolderId));
1346
+            $searchFolders[] = $searchFolderEntryId;
1347
+        }
1348
+        // if no folder was required then search in the entire store
1349
+        else {
1350
+            $tmp = mapi_getprops($this->store, [PR_ENTRYID, PR_DISPLAY_NAME, PR_IPM_SUBTREE_ENTRYID]);
1351
+            $searchFolders[] = $tmp[PR_IPM_SUBTREE_ENTRYID];
1352
+        }
1353
+        $items = [];
1354
+        $flags = 0;
1355
+        // if subfolders are required, do a recursive search
1356
+        if ($cpo->GetSearchDeepTraversal()) {
1357
+            $flags |= SEARCH_RECURSIVE;
1358
+        }
1359
+
1360
+        mapi_folder_setsearchcriteria($searchFolder, $searchRestriction, $searchFolders, $flags);
1361
+
1362
+        $table = mapi_folder_getcontentstable($searchFolder);
1363
+        $searchStart = time();
1364
+        // do the search and wait for all the results available
1365
+        while (time() - $searchStart < SEARCH_WAIT) {
1366
+            $searchcriteria = mapi_folder_getsearchcriteria($searchFolder);
1367
+            if (($searchcriteria["searchstate"] & SEARCH_REBUILD) == 0) {
1368
+                break;
1369
+            } // Search is done
1370
+            sleep(1);
1371
+        }
1372
+
1373
+        // if the search range is set limit the result to it, otherwise return all found messages
1374
+        $rows = (is_array($searchRange) && isset($searchRange[0], $searchRange[1])) ?
1375
+            mapi_table_queryrows($table, [PR_ENTRYID], $searchRange[0], $searchRange[1] - $searchRange[0] + 1) :
1376
+            mapi_table_queryrows($table, [PR_ENTRYID], 0, SEARCH_MAXRESULTS);
1377
+
1378
+        $cnt = count($rows);
1379
+        $items['searchtotal'] = $cnt;
1380
+        $items["range"] = $cpo->GetSearchRange();
1381
+        for ($i = 0; $i < $cnt; ++$i) {
1382
+            $items[$i]['class'] = 'Email';
1383
+            $items[$i]['longid'] = bin2hex($rows[$i][PR_ENTRYID]);
1384
+            // $items[$i]['folderid'] = bin2hex($rows[$i][PR_PARENT_SOURCE_KEY]);
1385
+        }
1386
+
1387
+        return $items;
1388
+    }
1389
+
1390
+    /**
1391
+     * Terminates a search for a given PID.
1392
+     *
1393
+     * @param int $pid
1394
+     *
1395
+     * @return bool
1396
+     */
1397
+    public function TerminateSearch($pid) {
1398
+        SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->TerminateSearch(): terminating search for pid %d", $pid));
1399
+        if (!isset($this->store) || $this->store === false) {
1400
+            SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->TerminateSearch(): The store is not available. It is not possible to remove search folder with pid %d", $pid));
1401
+
1402
+            return false;
1403
+        }
1404
+
1405
+        $storeProps = mapi_getprops($this->store, [PR_STORE_SUPPORT_MASK, PR_FINDER_ENTRYID]);
1406
+        if (($storeProps[PR_STORE_SUPPORT_MASK] & STORE_SEARCH_OK) != STORE_SEARCH_OK) {
1407
+            SLog::Write(LOGLEVEL_WARN, "Grommunio->TerminateSearch(): Store doesn't support search folders. Public store doesn't have FINDER_ROOT folder");
1408
+
1409
+            return false;
1410
+        }
1411
+
1412
+        $finderfolder = mapi_msgstore_openentry($this->store, $storeProps[PR_FINDER_ENTRYID]);
1413
+        if (mapi_last_hresult() != NOERROR) {
1414
+            SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->TerminateSearch(): Unable to open search folder (0x%X)", mapi_last_hresult()));
1415
+
1416
+            return false;
1417
+        }
1418
+
1419
+        $hierarchytable = mapi_folder_gethierarchytable($finderfolder);
1420
+        mapi_table_restrict(
1421
+            $hierarchytable,
1422
+            [RES_CONTENT,
1423
+                [
1424
+                    FUZZYLEVEL => FL_PREFIX,
1425
+                    ULPROPTAG => PR_DISPLAY_NAME,
1426
+                    VALUE => [PR_DISPLAY_NAME => "grommunio-sync Search Folder " . $pid],
1427
+                ],
1428
+            ],
1429
+            TBL_BATCH
1430
+        );
1431
+
1432
+        $folders = mapi_table_queryallrows($hierarchytable, [PR_ENTRYID, PR_DISPLAY_NAME, PR_LAST_MODIFICATION_TIME]);
1433
+        foreach ($folders as $folder) {
1434
+            mapi_folder_deletefolder($finderfolder, $folder[PR_ENTRYID]);
1435
+        }
1436
+
1437
+        return true;
1438
+    }
1439
+
1440
+    /**
1441
+     * Disconnects from the current search provider.
1442
+     *
1443
+     * @return bool
1444
+     */
1445
+    public function Disconnect() {
1446
+        return true;
1447
+    }
1448
+
1449
+    /**
1450
+     * Returns the MAPI store resource for a folderid
1451
+     * This is not part of IBackend but necessary for the ImportChangesICS->MoveMessage() operation if
1452
+     * the destination folder is not in the default store
1453
+     * Note: The current backend store might be changed as IBackend->Setup() is executed.
1454
+     *
1455
+     * @param string $store    target store, could contain a "domain\user" value - if empty default store is returned
1456
+     * @param string $folderid
1457
+     *
1458
+     * @return Resource/boolean
1459
+     */
1460
+    public function GetMAPIStoreForFolderId($store, $folderid) {
1461
+        if ($store == false) {
1462
+            SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetMAPIStoreForFolderId('%s', '%s'): no store specified, returning default store", $store, $folderid));
1463
+
1464
+            return $this->defaultstore;
1465
+        }
1466
+
1467
+        // setup the correct store
1468
+        if ($this->Setup($store, false, $folderid)) {
1469
+            return $this->store;
1470
+        }
1471
+
1472
+        SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->GetMAPIStoreForFolderId('%s', '%s'): store is not available", $store, $folderid));
1473
+
1474
+        return false;
1475
+    }
1476
+
1477
+    /**
1478
+     * Returns the email address and the display name of the user. Used by autodiscover.
1479
+     *
1480
+     * @param string $username The username
1481
+     *
1482
+     * @return array
1483
+     */
1484
+    public function GetUserDetails($username) {
1485
+        SLog::Write(LOGLEVEL_WBXML, sprintf("Grommunio->GetUserDetails for '%s'.", $username));
1486
+        $zarafauserinfo = @nsp_getuserinfo($username);
1487
+        $userDetails['emailaddress'] = (isset($zarafauserinfo['primary_email']) && $zarafauserinfo['primary_email']) ? $zarafauserinfo['primary_email'] : false;
1488
+        $userDetails['fullname'] = (isset($zarafauserinfo['fullname']) && $zarafauserinfo['fullname']) ? $zarafauserinfo['fullname'] : false;
1489
+
1490
+        return $userDetails;
1491
+    }
1492
+
1493
+    /**
1494
+     * Returns the username of the currently active user.
1495
+     *
1496
+     * @return string
1497
+     */
1498
+    public function GetCurrentUsername() {
1499
+        return $this->storeName;
1500
+    }
1501
+
1502
+    /**
1503
+     * Returns the impersonated user name.
1504
+     *
1505
+     * @return string or false if no user is impersonated
1506
+     */
1507
+    public function GetImpersonatedUser() {
1508
+        return $this->impersonateUser;
1509
+    }
1510
+
1511
+    /**
1512
+     * Returns the authenticated user name.
1513
+     *
1514
+     * @return string
1515
+     */
1516
+    public function GetMainUser() {
1517
+        return $this->mainUser;
1518
+    }
1519
+
1520
+    /**
1521
+     * Indicates if the Backend supports folder statistics.
1522
+     *
1523
+     * @return bool
1524
+     */
1525
+    public function HasFolderStats() {
1526
+        return true;
1527
+    }
1528
+
1529
+    /**
1530
+     * Returns a status indication of the folder.
1531
+     * If there are changes in the folder, the returned value must change.
1532
+     * The returned values are compared with '===' to determine if a folder needs synchronization or not.
1533
+     *
1534
+     * @param string $store    the store where the folder resides
1535
+     * @param string $folderid the folder id
1536
+     *
1537
+     * @return string
1538
+     */
1539
+    public function GetFolderStat($store, $folderid) {
1540
+        list($user, $domain) = Utils::SplitDomainUser($store);
1541
+        if ($user === false) {
1542
+            $user = $this->mainUser;
1543
+            if ($this->impersonateUser) {
1544
+                $user = $this->impersonateUser;
1545
+            }
1546
+        }
1547
+
1548
+        if (!isset($this->folderStatCache[$user])) {
1549
+            $this->folderStatCache[$user] = [];
1550
+        }
1551
+
1552
+        // if there is nothing in the cache for a store, load the data for all folders of it
1553
+        if (empty($this->folderStatCache[$user])) {
1554
+            // get the store
1555
+            $userstore = $this->openMessageStore($user);
1556
+            $rootfolder = mapi_msgstore_openentry($userstore);
1557
+            $hierarchy = mapi_folder_gethierarchytable($rootfolder, CONVENIENT_DEPTH);
1558
+            $rows = mapi_table_queryallrows($hierarchy, [PR_SOURCE_KEY, PR_LOCAL_COMMIT_TIME_MAX, PR_CONTENT_COUNT, PR_CONTENT_UNREAD, PR_DELETED_MSG_COUNT]);
1559
+
1560
+            if (count($rows) == 0) {
1561
+                SLog::Write(LOGLEVEL_INFO, sprintf("Grommunio->GetFolderStat(): could not access folder statistics for user '%s'. Probably missing 'read' permissions on the root folder! Folders of this store will be synchronized ONCE per hour only!", $user));
1562
+            }
1563
+
1564
+            foreach ($rows as $folder) {
1565
+                $commit_time = isset($folder[PR_LOCAL_COMMIT_TIME_MAX]) ? $folder[PR_LOCAL_COMMIT_TIME_MAX] : "0000000000";
1566
+                $content_count = isset($folder[PR_CONTENT_COUNT]) ? $folder[PR_CONTENT_COUNT] : -1;
1567
+                $content_unread = isset($folder[PR_CONTENT_UNREAD]) ? $folder[PR_CONTENT_UNREAD] : -1;
1568
+                $content_deleted = isset($folder[PR_DELETED_MSG_COUNT]) ? $folder[PR_DELETED_MSG_COUNT] : -1;
1569
+
1570
+                $this->folderStatCache[$user][bin2hex($folder[PR_SOURCE_KEY])] = $commit_time . "/" . $content_count . "/" . $content_unread . "/" . $content_deleted;
1571
+            }
1572
+            SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetFolderStat() fetched status information of %d folders for store '%s'", count($this->folderStatCache[$user]), $user));
1573
+        }
1574
+
1575
+        if (isset($this->folderStatCache[$user][$folderid])) {
1576
+            return $this->folderStatCache[$user][$folderid];
1577
+        }
1578
+
1579
+        // a timestamp that changes once per hour is returned in case there is no data found for this folder. It will be synchronized only once per hour.
1580
+        return gmdate("Y-m-d-H");
1581
+    }
1582
+
1583
+    /**
1584
+     * Returns information about the user's store:
1585
+     * number of folders, store size, full name, email address.
1586
+     *
1587
+     * @return UserStoreInfo
1588
+     */
1589
+    public function GetUserStoreInfo() {
1590
+        $userStoreInfo = new UserStoreInfo();
1591
+
1592
+        $rootfolder = mapi_msgstore_openentry($this->store);
1593
+        $hierarchy = mapi_folder_gethierarchytable($rootfolder, CONVENIENT_DEPTH);
1594
+        // Do not take hidden and system folders into account
1595
+        // TODO make this restriction generic and use for hierarchy?
1596
+        $restrict = [
1597
+            RES_AND,
1598
+            [
1599
+                [
1600
+                    RES_PROPERTY,
1601
+                    [
1602
+                        RELOP => RELOP_NE,
1603
+                        ULPROPTAG => PR_ATTR_HIDDEN,
1604
+                        VALUE => true, ],
1605
+                ],
1606
+                [
1607
+                    RES_PROPERTY,
1608
+                    [
1609
+                        RELOP => RELOP_EQ,
1610
+                        ULPROPTAG => PR_FOLDER_TYPE,
1611
+                        VALUE => FOLDER_GENERIC, ],
1612
+                ],
1613
+                [
1614
+                    RES_EXIST,
1615
+                    [ULPROPTAG => PR_CONTAINER_CLASS],
1616
+                ],
1617
+            ], ];
1618
+        mapi_table_restrict($hierarchy, $restrict);
1619
+        $foldercount = mapi_table_getrowcount($hierarchy);
1620
+
1621
+        $storeProps = mapi_getprops($this->store, [PR_MESSAGE_SIZE_EXTENDED]);
1622
+        $storesize = isset($storeProps[PR_MESSAGE_SIZE_EXTENDED]) ? $storeProps[PR_MESSAGE_SIZE_EXTENDED] : 0;
1623
+
1624
+        $userDetails = $this->GetUserDetails($this->impersonateUser ?: $this->mainUser);
1625
+        $userStoreInfo->SetData($foldercount, $storesize, $userDetails['fullname'], $userDetails['emailaddress']);
1626
+        SLog::Write(LOGLEVEL_DEBUG, sprintf(
1627
+            "Grommunio->GetUserStoreInfo(): user %s (%s) store size is %d bytes and contains %d folders",
1628
+            Utils::PrintAsString($userDetails['fullname']),
1629
+            Utils::PrintAsString($userDetails['emailaddress']),
1630
+            $storesize,
1631
+            $foldercount
1632
+        ));
1633
+
1634
+        return $userStoreInfo;
1635
+    }
1636
+
1637
+    /*----------------------------------------------------------------------------------------------------------
1638 1638
 	 * Implementation of the IStateMachine interface
1639 1639
 	 */
1640 1640
 
1641
-	/**
1642
-	 * Gets a hash value indicating the latest dataset of the named
1643
-	 * state with a specified key and counter.
1644
-	 * If the state is changed between two calls of this method
1645
-	 * the returned hash should be different.
1646
-	 *
1647
-	 * @param string $devid   the device id
1648
-	 * @param string $type    the state type
1649
-	 * @param string $key     (opt)
1650
-	 * @param string $counter (opt)
1651
-	 *
1652
-	 * @throws StateNotFoundException
1653
-	 *
1654
-	 * @return string
1655
-	 */
1656
-	public function GetStateHash($devid, $type, $key = false, $counter = false) {
1657
-		try {
1658
-			$stateMessage = $this->getStateMessage($devid, $type, $key, $counter);
1659
-			$stateMessageProps = mapi_getprops($stateMessage, [PR_LAST_MODIFICATION_TIME]);
1660
-			if (isset($stateMessageProps[PR_LAST_MODIFICATION_TIME])) {
1661
-				return $stateMessageProps[PR_LAST_MODIFICATION_TIME];
1662
-			}
1663
-		}
1664
-		catch (StateNotFoundException $e) {
1665
-		}
1666
-
1667
-		return "0";
1668
-	}
1669
-
1670
-	/**
1671
-	 * Gets a state for a specified key and counter.
1672
-	 * This method should call IStateMachine->CleanStates()
1673
-	 * to remove older states (same key, previous counters).
1674
-	 *
1675
-	 * @param string $devid       the device id
1676
-	 * @param string $type        the state type
1677
-	 * @param string $key         (opt)
1678
-	 * @param string $counter     (opt)
1679
-	 * @param string $cleanstates (opt)
1680
-	 *
1681
-	 * @throws StateNotFoundException, StateInvalidException, UnavailableException
1682
-	 *
1683
-	 * @return mixed
1684
-	 */
1685
-	public function GetState($devid, $type, $key = false, $counter = false, $cleanstates = true) {
1686
-		if ($counter && $cleanstates) {
1687
-			$this->CleanStates($devid, $type, $key, $counter);
1688
-			// also clean Failsave state for previous counter
1689
-			if ($key == false) {
1690
-				$this->CleanStates($devid, $type, IStateMachine::FAILSAVE, $counter);
1691
-			}
1692
-		}
1693
-		$stateMessage = $this->getStateMessage($devid, $type, $key, $counter);
1694
-		$state = base64_decode(MAPIUtils::readPropStream($stateMessage, PR_BODY));
1695
-
1696
-		if ($state && $state[0] === '{') {
1697
-			$jsonDec = json_decode($state);
1698
-			if (isset($jsonDec->gsSyncStateClass)) {
1699
-				SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetState(): top class '%s'", $jsonDec->gsSyncStateClass));
1700
-				$gsObj = new $jsonDec->gsSyncStateClass();
1701
-				$gsObj->jsonDeserialize($jsonDec);
1702
-				$gsObj->postUnserialize();
1703
-			}
1704
-		}
1705
-
1706
-		return isset($gsObj) && is_object($gsObj) ? $gsObj : $state;
1707
-	}
1708
-
1709
-	/**
1710
-	 * Writes ta state to for a key and counter.
1711
-	 *
1712
-	 * @param mixed  $state
1713
-	 * @param string $devid   the device id
1714
-	 * @param string $type    the state type
1715
-	 * @param string $key     (opt)
1716
-	 * @param int    $counter (opt)
1717
-	 *
1718
-	 * @throws StateInvalidException, UnavailableException
1719
-	 *
1720
-	 * @return bool
1721
-	 */
1722
-	public function SetState($state, $devid, $type, $key = false, $counter = false) {
1723
-		return $this->setStateMessage($state, $devid, $type, $key, $counter);
1724
-	}
1725
-
1726
-	/**
1727
-	 * Cleans up all older states.
1728
-	 * If called with a $counter, all states previous state counter can be removed.
1729
-	 * If additionally the $thisCounterOnly flag is true, only that specific counter will be removed.
1730
-	 * If called without $counter, all keys (independently from the counter) can be removed.
1731
-	 *
1732
-	 * @param string $devid           the device id
1733
-	 * @param string $type            the state type
1734
-	 * @param string $key
1735
-	 * @param string $counter         (opt)
1736
-	 * @param string $thisCounterOnly (opt) if provided, the exact counter only will be removed
1737
-	 *
1738
-	 * @throws StateInvalidException
1739
-	 *
1740
-	 * @return
1741
-	 */
1742
-	public function CleanStates($devid, $type, $key, $counter = false, $thisCounterOnly = false) {
1743
-		if (!$this->stateFolder) {
1744
-			$this->getStateFolder($devid);
1745
-			if (!$this->stateFolder) {
1746
-				throw new StateNotFoundException(sprintf(
1747
-					"Grommunio->getStateMessage(): Could not locate the state folder for device '%s'",
1748
-					$devid
1749
-				));
1750
-			}
1751
-		}
1752
-		$messageName = rtrim((($key !== false) ? $key . "-" : "") . (($type !== "") ? $type : ""), "-");
1753
-		$restriction = $this->getStateMessageRestriction($messageName, $counter, $thisCounterOnly);
1754
-		$stateFolderContents = mapi_folder_getcontentstable($this->stateFolder, MAPI_ASSOCIATED);
1755
-		if ($stateFolderContents) {
1756
-			mapi_table_restrict($stateFolderContents, $restriction);
1757
-			$rowCnt = mapi_table_getrowcount($stateFolderContents);
1758
-			SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->CleanStates(): Found %d states to clean (%s)", $rowCnt, $messageName));
1759
-			if ($rowCnt > 0) {
1760
-				$rows = mapi_table_queryallrows($stateFolderContents, [PR_ENTRYID]);
1761
-				$entryids = [];
1762
-				foreach ($rows as $row) {
1763
-					$entryids[] = $row[PR_ENTRYID];
1764
-				}
1765
-				mapi_folder_deletemessages($this->stateFolder, $entryids, DELETE_HARD_DELETE);
1766
-			}
1767
-		}
1768
-	}
1769
-
1770
-	/**
1771
-	 * Links a user to a device.
1772
-	 *
1773
-	 * @param string $username
1774
-	 * @param string $devid
1775
-	 *
1776
-	 * @return bool indicating if the user was added or not (existed already)
1777
-	 */
1778
-	public function LinkUserDevice($username, $devid) {
1779
-		$device = [$devid => time()];
1780
-		$this->setDeviceUserData($this->type, $device, $username, -1, $subkey = -1, $doCas = "merge");
1781
-
1782
-		return false;
1783
-	}
1784
-
1785
-	/**
1786
-	 * Unlinks a device from a user.
1787
-	 *
1788
-	 * @param string $username
1789
-	 * @param string $devid
1790
-	 *
1791
-	 * @return bool
1792
-	 */
1793
-	public function UnLinkUserDevice($username, $devid) {
1794
-		// TODO: Implement
1795
-		return false;
1796
-	}
1797
-
1798
-	/**
1799
-	 * Returns the current version of the state files
1800
-	 * grommunio:  This is not relevant atm. IStateMachine::STATEVERSION_02 will match GSync::GetLatestStateVersion().
1801
-	 *          If it might be required to update states in the future, this could be implemented on a store level,
1802
-	 *          where states are then migrated "on-the-fly"
1803
-	 *          or
1804
-	 *          in a global settings where all states in all stores are migrated once.
1805
-	 *
1806
-	 * @return int
1807
-	 */
1808
-	public function GetStateVersion() {
1809
-		return IStateMachine::STATEVERSION_02;
1810
-	}
1811
-
1812
-	/**
1813
-	 * Sets the current version of the state files.
1814
-	 *
1815
-	 * @param int $version the new supported version
1816
-	 *
1817
-	 * @return bool
1818
-	 */
1819
-	public function SetStateVersion($version) {
1820
-		return true;
1821
-	}
1822
-
1823
-	/**
1824
-	 * Returns MAPIFolder object which contains the state information.
1825
-	 * Creates this folder if it is not available yet.
1826
-	 *
1827
-	 * @param string $devid the device id
1828
-	 *
1829
-	 * @return MAPIFolder
1830
-	 */
1831
-	private function getStateFolder($devid) {
1832
-		// Options request doesn't send device id
1833
-		if (strlen($devid) == 0) {
1834
-			return false;
1835
-		}
1836
-		// Try to get the state folder id from redis
1837
-		if (!$this->stateFolder) {
1838
-			$folderentryid = $this->getDeviceUserData($this->userDeviceData, $devid, $this->mainUser, "statefolder");
1839
-			if ($folderentryid) {
1840
-				$this->stateFolder = mapi_msgstore_openentry($this->store, hex2bin($folderentryid));
1841
-			}
1842
-		}
1843
-
1844
-		// fallback code
1845
-		if (!$this->stateFolder) {
1846
-			SLog::Write(LOGLEVEL_INFO, sprintf("Grommunio->getStateFolder(): state folder not set. Use fallback"));
1847
-			$rootfolder = mapi_msgstore_openentry($this->store);
1848
-			$hierarchy = mapi_folder_gethierarchytable($rootfolder, CONVENIENT_DEPTH);
1849
-			$restriction = $this->getStateFolderRestriction($devid);
1850
-			// restrict the hierarchy to the grommunio-sync search folder only
1851
-			mapi_table_restrict($hierarchy, $restriction);
1852
-			$rowCnt = mapi_table_getrowcount($hierarchy);
1853
-			SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->getStateFolder(): found %d device state folders", $rowCnt));
1854
-			if ($rowCnt == 1) {
1855
-				$hierarchyRows = mapi_table_queryrows($hierarchy, [PR_ENTRYID], 0, 1);
1856
-				$this->stateFolder = mapi_msgstore_openentry($this->store, $hierarchyRows[0][PR_ENTRYID]);
1857
-				SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->getStateFolder(): %s", bin2hex($hierarchyRows[0][PR_ENTRYID])));
1858
-				// put found id in redis
1859
-				if ($devid) {
1860
-					$this->setDeviceUserData($this->userDeviceData, bin2hex($hierarchyRows[0][PR_ENTRYID]), $devid, $this->mainUser, "statefolder");
1861
-				}
1862
-			}
1863
-			elseif ($rowCnt == 0) {
1864
-				// legacy code: create the hidden state folder and the device subfolder
1865
-				// this should happen when the user configures the device (autodiscover or first sync if no autodiscover)
1866
-
1867
-				$hierarchy = mapi_folder_gethierarchytable($rootfolder, CONVENIENT_DEPTH);
1868
-				$restriction = $this->getStateFolderRestriction(STORE_STATE_FOLDER);
1869
-				mapi_table_restrict($hierarchy, $restriction);
1870
-				$rowCnt = mapi_table_getrowcount($hierarchy);
1871
-				SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->getStateFolder(): found %d store state folders", $rowCnt));
1872
-				if ($rowCnt == 1) {
1873
-					$hierarchyRows = mapi_table_queryrows($hierarchy, [PR_ENTRYID], 0, 1);
1874
-					$stateFolder = mapi_msgstore_openentry($this->store, $hierarchyRows[0][PR_ENTRYID]);
1875
-				}
1876
-				elseif ($rowCnt == 0) {
1877
-					$stateFolder = mapi_folder_createfolder($rootfolder, STORE_STATE_FOLDER, "");
1878
-					mapi_setprops($stateFolder, [PR_ATTR_HIDDEN => true]);
1879
-				}
1880
-
1881
-				// TODO: handle this
1882
-
1883
-				if (isset($stateFolder) && $stateFolder) {
1884
-					$devStateFolder = mapi_folder_createfolder($stateFolder, $devid, "");
1885
-					$devStateFolderProps = mapi_getprops($devStateFolder);
1886
-					$this->stateFolder = mapi_msgstore_openentry($this->store, $devStateFolderProps[PR_ENTRYID]);
1887
-					mapi_setprops($this->stateFolder, [PR_ATTR_HIDDEN => true]);
1888
-					// we don't cache the entryid in redis, because this will happen on the next request anyway
1889
-				}
1890
-
1891
-				// TODO: unable to create state folder - throw exception
1892
-			}
1893
-
1894
-			// This case is rather unlikely that there would be several
1895
-				// hidden folders having PR_DISPLAY_NAME the same as device id.
1896
-
1897
-				// TODO: get the hierarchy table again, get entry id of STORE_STATE_FOLDER
1898
-				// and compare it to the parent id of those folders.
1899
-		}
1900
-
1901
-		return $this->stateFolder;
1902
-	}
1903
-
1904
-	/**
1905
-	 * Returns the associated MAPIMessage which contains the state information.
1906
-	 *
1907
-	 * @param string $devid   the device id
1908
-	 * @param string $type    the state type
1909
-	 * @param string $key     (opt)
1910
-	 * @param string $counter state counter
1911
-	 *
1912
-	 * @throws StateNotFoundException
1913
-	 *
1914
-	 * @return MAPIMessage
1915
-	 */
1916
-	private function getStateMessage($devid, $type, $key, $counter) {
1917
-		if (!$this->stateFolder) {
1918
-			$this->getStateFolder(Request::GetDeviceID());
1919
-			if (!$this->stateFolder) {
1920
-				throw new StateNotFoundException(sprintf(
1921
-					"Grommunio->getStateMessage(): Could not locate the state folder for device '%s'",
1922
-					$devid
1923
-				));
1924
-			}
1925
-		}
1926
-		$messageName = rtrim((($key !== false) ? $key . "-" : "") . (($type !== "") ? $type : ""), "-");
1927
-		$restriction = $this->getStateMessageRestriction($messageName, $counter, true);
1928
-		$stateFolderContents = mapi_folder_getcontentstable($this->stateFolder, MAPI_ASSOCIATED);
1929
-		if ($stateFolderContents) {
1930
-			mapi_table_restrict($stateFolderContents, $restriction);
1931
-			$rowCnt = mapi_table_getrowcount($stateFolderContents);
1932
-			if ($rowCnt == 1) {
1933
-				$stateFolderRows = mapi_table_queryrows($stateFolderContents, [PR_ENTRYID], 0, 1);
1934
-
1935
-				return mapi_msgstore_openentry($this->store, $stateFolderRows[0][PR_ENTRYID]);
1936
-			}
1937
-			if ($rowCnt > 1) {
1938
-				SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->getStateMessage(): Found several (%d) states for '%s'", $rowCnt, $messageName));
1939
-			}
1940
-		}
1941
-
1942
-		throw new StateNotFoundException(sprintf(
1943
-			"Grommunio->getStateMessage(): Could not locate the state message '%s-%s'",
1944
-			$messageName,
1945
-			Utils::PrintAsString($counter)
1946
-		));
1947
-	}
1948
-
1949
-	/**
1950
-	 * Writes ta state to for a key and counter.
1951
-	 *
1952
-	 * @param mixed  $state
1953
-	 * @param string $devid   the device id
1954
-	 * @param string $type    the state type
1955
-	 * @param string $key     (opt)
1956
-	 * @param int    $counter (opt)
1957
-	 *
1958
-	 * @throws StateInvalidException, UnavailableException
1959
-	 *
1960
-	 * @return bool
1961
-	 */
1962
-	private function setStateMessage($state, $devid, $type, $key = false, $counter = false) {
1963
-		if (!$this->stateFolder) {
1964
-			throw new StateNotFoundException(sprintf("Grommunio->setStateMessage(): Could not locate the state folder for device '%s'", $devid));
1965
-		}
1966
-
1967
-		try {
1968
-			$stateMessage = $this->getStateMessage($devid, $type, $key, $counter);
1969
-		}
1970
-		catch (StateNotFoundException $e) {
1971
-			// if message is not available, try to create a new one
1972
-			$stateMessage = mapi_folder_createmessage($this->stateFolder, MAPI_ASSOCIATED);
1973
-			SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->setStateMessage(): mapi_folder_createmessage 0x%08X", mapi_last_hresult()));
1974
-
1975
-			$messageName = rtrim((($key !== false) ? $key . "-" : "") . (($type !== "") ? $type : ""), "-");
1976
-			SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->setStateMessage(): creating new state message '%s-%d'", $messageName, is_int($counter) ? $counter : 0));
1977
-			mapi_setprops($stateMessage, [PR_DISPLAY_NAME => $messageName, PR_MESSAGE_CLASS => 'IPM.Note.GrommunioState']);
1978
-		}
1979
-		if (isset($stateMessage)) {
1980
-			$jsonEncodedState = is_object($state) || is_array($state) ? json_encode($state, JSON_INVALID_UTF8_IGNORE | JSON_UNESCAPED_UNICODE) : $state;
1981
-
1982
-			$encodedState = base64_encode($jsonEncodedState);
1983
-			$encodedStateLength = strlen($encodedState);
1984
-			mapi_setprops($stateMessage, [PR_LAST_VERB_EXECUTED => is_int($counter) ? $counter : 0]);
1985
-			$stream = mapi_openproperty($stateMessage, PR_BODY, IID_IStream, STGM_DIRECT, MAPI_CREATE | MAPI_MODIFY);
1986
-			mapi_stream_setsize($stream, $encodedStateLength);
1987
-			mapi_stream_write($stream, $encodedState);
1988
-			mapi_stream_commit($stream);
1989
-			mapi_savechanges($stateMessage);
1990
-
1991
-			return $encodedStateLength;
1992
-		}
1993
-
1994
-		return false;
1995
-	}
1996
-
1997
-	/**
1998
-	 * Returns the restriction for the state folder name.
1999
-	 *
2000
-	 * @param string $folderName the state folder name
2001
-	 *
2002
-	 * @return array
2003
-	 */
2004
-	private function getStateFolderRestriction($folderName) {
2005
-		return [RES_AND, [
2006
-			[RES_PROPERTY,
2007
-				[RELOP => RELOP_EQ,
2008
-					ULPROPTAG => PR_DISPLAY_NAME,
2009
-					VALUE => $folderName,
2010
-				],
2011
-			],
2012
-			[RES_PROPERTY,
2013
-				[RELOP => RELOP_EQ,
2014
-					ULPROPTAG => PR_ATTR_HIDDEN,
2015
-					VALUE => true,
2016
-				],
2017
-			],
2018
-		]];
2019
-	}
2020
-
2021
-	/**
2022
-	 * Returns the restriction for the associated message in the state folder.
2023
-	 *
2024
-	 * @param string $messageName     the message name
2025
-	 * @param string $counter         counter
2026
-	 * @param string $thisCounterOnly (opt) if provided, restrict to the exact counter
2027
-	 *
2028
-	 * @return array
2029
-	 */
2030
-	private function getStateMessageRestriction($messageName, $counter, $thisCounterOnly = false) {
2031
-		return [RES_AND, [
2032
-			[RES_PROPERTY,
2033
-				[RELOP => RELOP_EQ,
2034
-					ULPROPTAG => PR_DISPLAY_NAME,
2035
-					VALUE => $messageName,
2036
-				],
2037
-			],
2038
-			[RES_PROPERTY,
2039
-				[RELOP => RELOP_EQ,
2040
-					ULPROPTAG => PR_MESSAGE_CLASS,
2041
-					VALUE => 'IPM.Note.GrommunioState',
2042
-				],
2043
-			],
2044
-			[RES_PROPERTY,
2045
-				[RELOP => $thisCounterOnly ? RELOP_EQ : RELOP_LT,
2046
-					ULPROPTAG => PR_LAST_VERB_EXECUTED,
2047
-					VALUE => $counter,
2048
-				],
2049
-			],
2050
-		]];
2051
-	}
2052
-
2053
-	/*----------------------------------------------------------------------------------------------------------
1641
+    /**
1642
+     * Gets a hash value indicating the latest dataset of the named
1643
+     * state with a specified key and counter.
1644
+     * If the state is changed between two calls of this method
1645
+     * the returned hash should be different.
1646
+     *
1647
+     * @param string $devid   the device id
1648
+     * @param string $type    the state type
1649
+     * @param string $key     (opt)
1650
+     * @param string $counter (opt)
1651
+     *
1652
+     * @throws StateNotFoundException
1653
+     *
1654
+     * @return string
1655
+     */
1656
+    public function GetStateHash($devid, $type, $key = false, $counter = false) {
1657
+        try {
1658
+            $stateMessage = $this->getStateMessage($devid, $type, $key, $counter);
1659
+            $stateMessageProps = mapi_getprops($stateMessage, [PR_LAST_MODIFICATION_TIME]);
1660
+            if (isset($stateMessageProps[PR_LAST_MODIFICATION_TIME])) {
1661
+                return $stateMessageProps[PR_LAST_MODIFICATION_TIME];
1662
+            }
1663
+        }
1664
+        catch (StateNotFoundException $e) {
1665
+        }
1666
+
1667
+        return "0";
1668
+    }
1669
+
1670
+    /**
1671
+     * Gets a state for a specified key and counter.
1672
+     * This method should call IStateMachine->CleanStates()
1673
+     * to remove older states (same key, previous counters).
1674
+     *
1675
+     * @param string $devid       the device id
1676
+     * @param string $type        the state type
1677
+     * @param string $key         (opt)
1678
+     * @param string $counter     (opt)
1679
+     * @param string $cleanstates (opt)
1680
+     *
1681
+     * @throws StateNotFoundException, StateInvalidException, UnavailableException
1682
+     *
1683
+     * @return mixed
1684
+     */
1685
+    public function GetState($devid, $type, $key = false, $counter = false, $cleanstates = true) {
1686
+        if ($counter && $cleanstates) {
1687
+            $this->CleanStates($devid, $type, $key, $counter);
1688
+            // also clean Failsave state for previous counter
1689
+            if ($key == false) {
1690
+                $this->CleanStates($devid, $type, IStateMachine::FAILSAVE, $counter);
1691
+            }
1692
+        }
1693
+        $stateMessage = $this->getStateMessage($devid, $type, $key, $counter);
1694
+        $state = base64_decode(MAPIUtils::readPropStream($stateMessage, PR_BODY));
1695
+
1696
+        if ($state && $state[0] === '{') {
1697
+            $jsonDec = json_decode($state);
1698
+            if (isset($jsonDec->gsSyncStateClass)) {
1699
+                SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetState(): top class '%s'", $jsonDec->gsSyncStateClass));
1700
+                $gsObj = new $jsonDec->gsSyncStateClass();
1701
+                $gsObj->jsonDeserialize($jsonDec);
1702
+                $gsObj->postUnserialize();
1703
+            }
1704
+        }
1705
+
1706
+        return isset($gsObj) && is_object($gsObj) ? $gsObj : $state;
1707
+    }
1708
+
1709
+    /**
1710
+     * Writes ta state to for a key and counter.
1711
+     *
1712
+     * @param mixed  $state
1713
+     * @param string $devid   the device id
1714
+     * @param string $type    the state type
1715
+     * @param string $key     (opt)
1716
+     * @param int    $counter (opt)
1717
+     *
1718
+     * @throws StateInvalidException, UnavailableException
1719
+     *
1720
+     * @return bool
1721
+     */
1722
+    public function SetState($state, $devid, $type, $key = false, $counter = false) {
1723
+        return $this->setStateMessage($state, $devid, $type, $key, $counter);
1724
+    }
1725
+
1726
+    /**
1727
+     * Cleans up all older states.
1728
+     * If called with a $counter, all states previous state counter can be removed.
1729
+     * If additionally the $thisCounterOnly flag is true, only that specific counter will be removed.
1730
+     * If called without $counter, all keys (independently from the counter) can be removed.
1731
+     *
1732
+     * @param string $devid           the device id
1733
+     * @param string $type            the state type
1734
+     * @param string $key
1735
+     * @param string $counter         (opt)
1736
+     * @param string $thisCounterOnly (opt) if provided, the exact counter only will be removed
1737
+     *
1738
+     * @throws StateInvalidException
1739
+     *
1740
+     * @return
1741
+     */
1742
+    public function CleanStates($devid, $type, $key, $counter = false, $thisCounterOnly = false) {
1743
+        if (!$this->stateFolder) {
1744
+            $this->getStateFolder($devid);
1745
+            if (!$this->stateFolder) {
1746
+                throw new StateNotFoundException(sprintf(
1747
+                    "Grommunio->getStateMessage(): Could not locate the state folder for device '%s'",
1748
+                    $devid
1749
+                ));
1750
+            }
1751
+        }
1752
+        $messageName = rtrim((($key !== false) ? $key . "-" : "") . (($type !== "") ? $type : ""), "-");
1753
+        $restriction = $this->getStateMessageRestriction($messageName, $counter, $thisCounterOnly);
1754
+        $stateFolderContents = mapi_folder_getcontentstable($this->stateFolder, MAPI_ASSOCIATED);
1755
+        if ($stateFolderContents) {
1756
+            mapi_table_restrict($stateFolderContents, $restriction);
1757
+            $rowCnt = mapi_table_getrowcount($stateFolderContents);
1758
+            SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->CleanStates(): Found %d states to clean (%s)", $rowCnt, $messageName));
1759
+            if ($rowCnt > 0) {
1760
+                $rows = mapi_table_queryallrows($stateFolderContents, [PR_ENTRYID]);
1761
+                $entryids = [];
1762
+                foreach ($rows as $row) {
1763
+                    $entryids[] = $row[PR_ENTRYID];
1764
+                }
1765
+                mapi_folder_deletemessages($this->stateFolder, $entryids, DELETE_HARD_DELETE);
1766
+            }
1767
+        }
1768
+    }
1769
+
1770
+    /**
1771
+     * Links a user to a device.
1772
+     *
1773
+     * @param string $username
1774
+     * @param string $devid
1775
+     *
1776
+     * @return bool indicating if the user was added or not (existed already)
1777
+     */
1778
+    public function LinkUserDevice($username, $devid) {
1779
+        $device = [$devid => time()];
1780
+        $this->setDeviceUserData($this->type, $device, $username, -1, $subkey = -1, $doCas = "merge");
1781
+
1782
+        return false;
1783
+    }
1784
+
1785
+    /**
1786
+     * Unlinks a device from a user.
1787
+     *
1788
+     * @param string $username
1789
+     * @param string $devid
1790
+     *
1791
+     * @return bool
1792
+     */
1793
+    public function UnLinkUserDevice($username, $devid) {
1794
+        // TODO: Implement
1795
+        return false;
1796
+    }
1797
+
1798
+    /**
1799
+     * Returns the current version of the state files
1800
+     * grommunio:  This is not relevant atm. IStateMachine::STATEVERSION_02 will match GSync::GetLatestStateVersion().
1801
+     *          If it might be required to update states in the future, this could be implemented on a store level,
1802
+     *          where states are then migrated "on-the-fly"
1803
+     *          or
1804
+     *          in a global settings where all states in all stores are migrated once.
1805
+     *
1806
+     * @return int
1807
+     */
1808
+    public function GetStateVersion() {
1809
+        return IStateMachine::STATEVERSION_02;
1810
+    }
1811
+
1812
+    /**
1813
+     * Sets the current version of the state files.
1814
+     *
1815
+     * @param int $version the new supported version
1816
+     *
1817
+     * @return bool
1818
+     */
1819
+    public function SetStateVersion($version) {
1820
+        return true;
1821
+    }
1822
+
1823
+    /**
1824
+     * Returns MAPIFolder object which contains the state information.
1825
+     * Creates this folder if it is not available yet.
1826
+     *
1827
+     * @param string $devid the device id
1828
+     *
1829
+     * @return MAPIFolder
1830
+     */
1831
+    private function getStateFolder($devid) {
1832
+        // Options request doesn't send device id
1833
+        if (strlen($devid) == 0) {
1834
+            return false;
1835
+        }
1836
+        // Try to get the state folder id from redis
1837
+        if (!$this->stateFolder) {
1838
+            $folderentryid = $this->getDeviceUserData($this->userDeviceData, $devid, $this->mainUser, "statefolder");
1839
+            if ($folderentryid) {
1840
+                $this->stateFolder = mapi_msgstore_openentry($this->store, hex2bin($folderentryid));
1841
+            }
1842
+        }
1843
+
1844
+        // fallback code
1845
+        if (!$this->stateFolder) {
1846
+            SLog::Write(LOGLEVEL_INFO, sprintf("Grommunio->getStateFolder(): state folder not set. Use fallback"));
1847
+            $rootfolder = mapi_msgstore_openentry($this->store);
1848
+            $hierarchy = mapi_folder_gethierarchytable($rootfolder, CONVENIENT_DEPTH);
1849
+            $restriction = $this->getStateFolderRestriction($devid);
1850
+            // restrict the hierarchy to the grommunio-sync search folder only
1851
+            mapi_table_restrict($hierarchy, $restriction);
1852
+            $rowCnt = mapi_table_getrowcount($hierarchy);
1853
+            SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->getStateFolder(): found %d device state folders", $rowCnt));
1854
+            if ($rowCnt == 1) {
1855
+                $hierarchyRows = mapi_table_queryrows($hierarchy, [PR_ENTRYID], 0, 1);
1856
+                $this->stateFolder = mapi_msgstore_openentry($this->store, $hierarchyRows[0][PR_ENTRYID]);
1857
+                SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->getStateFolder(): %s", bin2hex($hierarchyRows[0][PR_ENTRYID])));
1858
+                // put found id in redis
1859
+                if ($devid) {
1860
+                    $this->setDeviceUserData($this->userDeviceData, bin2hex($hierarchyRows[0][PR_ENTRYID]), $devid, $this->mainUser, "statefolder");
1861
+                }
1862
+            }
1863
+            elseif ($rowCnt == 0) {
1864
+                // legacy code: create the hidden state folder and the device subfolder
1865
+                // this should happen when the user configures the device (autodiscover or first sync if no autodiscover)
1866
+
1867
+                $hierarchy = mapi_folder_gethierarchytable($rootfolder, CONVENIENT_DEPTH);
1868
+                $restriction = $this->getStateFolderRestriction(STORE_STATE_FOLDER);
1869
+                mapi_table_restrict($hierarchy, $restriction);
1870
+                $rowCnt = mapi_table_getrowcount($hierarchy);
1871
+                SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->getStateFolder(): found %d store state folders", $rowCnt));
1872
+                if ($rowCnt == 1) {
1873
+                    $hierarchyRows = mapi_table_queryrows($hierarchy, [PR_ENTRYID], 0, 1);
1874
+                    $stateFolder = mapi_msgstore_openentry($this->store, $hierarchyRows[0][PR_ENTRYID]);
1875
+                }
1876
+                elseif ($rowCnt == 0) {
1877
+                    $stateFolder = mapi_folder_createfolder($rootfolder, STORE_STATE_FOLDER, "");
1878
+                    mapi_setprops($stateFolder, [PR_ATTR_HIDDEN => true]);
1879
+                }
1880
+
1881
+                // TODO: handle this
1882
+
1883
+                if (isset($stateFolder) && $stateFolder) {
1884
+                    $devStateFolder = mapi_folder_createfolder($stateFolder, $devid, "");
1885
+                    $devStateFolderProps = mapi_getprops($devStateFolder);
1886
+                    $this->stateFolder = mapi_msgstore_openentry($this->store, $devStateFolderProps[PR_ENTRYID]);
1887
+                    mapi_setprops($this->stateFolder, [PR_ATTR_HIDDEN => true]);
1888
+                    // we don't cache the entryid in redis, because this will happen on the next request anyway
1889
+                }
1890
+
1891
+                // TODO: unable to create state folder - throw exception
1892
+            }
1893
+
1894
+            // This case is rather unlikely that there would be several
1895
+                // hidden folders having PR_DISPLAY_NAME the same as device id.
1896
+
1897
+                // TODO: get the hierarchy table again, get entry id of STORE_STATE_FOLDER
1898
+                // and compare it to the parent id of those folders.
1899
+        }
1900
+
1901
+        return $this->stateFolder;
1902
+    }
1903
+
1904
+    /**
1905
+     * Returns the associated MAPIMessage which contains the state information.
1906
+     *
1907
+     * @param string $devid   the device id
1908
+     * @param string $type    the state type
1909
+     * @param string $key     (opt)
1910
+     * @param string $counter state counter
1911
+     *
1912
+     * @throws StateNotFoundException
1913
+     *
1914
+     * @return MAPIMessage
1915
+     */
1916
+    private function getStateMessage($devid, $type, $key, $counter) {
1917
+        if (!$this->stateFolder) {
1918
+            $this->getStateFolder(Request::GetDeviceID());
1919
+            if (!$this->stateFolder) {
1920
+                throw new StateNotFoundException(sprintf(
1921
+                    "Grommunio->getStateMessage(): Could not locate the state folder for device '%s'",
1922
+                    $devid
1923
+                ));
1924
+            }
1925
+        }
1926
+        $messageName = rtrim((($key !== false) ? $key . "-" : "") . (($type !== "") ? $type : ""), "-");
1927
+        $restriction = $this->getStateMessageRestriction($messageName, $counter, true);
1928
+        $stateFolderContents = mapi_folder_getcontentstable($this->stateFolder, MAPI_ASSOCIATED);
1929
+        if ($stateFolderContents) {
1930
+            mapi_table_restrict($stateFolderContents, $restriction);
1931
+            $rowCnt = mapi_table_getrowcount($stateFolderContents);
1932
+            if ($rowCnt == 1) {
1933
+                $stateFolderRows = mapi_table_queryrows($stateFolderContents, [PR_ENTRYID], 0, 1);
1934
+
1935
+                return mapi_msgstore_openentry($this->store, $stateFolderRows[0][PR_ENTRYID]);
1936
+            }
1937
+            if ($rowCnt > 1) {
1938
+                SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->getStateMessage(): Found several (%d) states for '%s'", $rowCnt, $messageName));
1939
+            }
1940
+        }
1941
+
1942
+        throw new StateNotFoundException(sprintf(
1943
+            "Grommunio->getStateMessage(): Could not locate the state message '%s-%s'",
1944
+            $messageName,
1945
+            Utils::PrintAsString($counter)
1946
+        ));
1947
+    }
1948
+
1949
+    /**
1950
+     * Writes ta state to for a key and counter.
1951
+     *
1952
+     * @param mixed  $state
1953
+     * @param string $devid   the device id
1954
+     * @param string $type    the state type
1955
+     * @param string $key     (opt)
1956
+     * @param int    $counter (opt)
1957
+     *
1958
+     * @throws StateInvalidException, UnavailableException
1959
+     *
1960
+     * @return bool
1961
+     */
1962
+    private function setStateMessage($state, $devid, $type, $key = false, $counter = false) {
1963
+        if (!$this->stateFolder) {
1964
+            throw new StateNotFoundException(sprintf("Grommunio->setStateMessage(): Could not locate the state folder for device '%s'", $devid));
1965
+        }
1966
+
1967
+        try {
1968
+            $stateMessage = $this->getStateMessage($devid, $type, $key, $counter);
1969
+        }
1970
+        catch (StateNotFoundException $e) {
1971
+            // if message is not available, try to create a new one
1972
+            $stateMessage = mapi_folder_createmessage($this->stateFolder, MAPI_ASSOCIATED);
1973
+            SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->setStateMessage(): mapi_folder_createmessage 0x%08X", mapi_last_hresult()));
1974
+
1975
+            $messageName = rtrim((($key !== false) ? $key . "-" : "") . (($type !== "") ? $type : ""), "-");
1976
+            SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->setStateMessage(): creating new state message '%s-%d'", $messageName, is_int($counter) ? $counter : 0));
1977
+            mapi_setprops($stateMessage, [PR_DISPLAY_NAME => $messageName, PR_MESSAGE_CLASS => 'IPM.Note.GrommunioState']);
1978
+        }
1979
+        if (isset($stateMessage)) {
1980
+            $jsonEncodedState = is_object($state) || is_array($state) ? json_encode($state, JSON_INVALID_UTF8_IGNORE | JSON_UNESCAPED_UNICODE) : $state;
1981
+
1982
+            $encodedState = base64_encode($jsonEncodedState);
1983
+            $encodedStateLength = strlen($encodedState);
1984
+            mapi_setprops($stateMessage, [PR_LAST_VERB_EXECUTED => is_int($counter) ? $counter : 0]);
1985
+            $stream = mapi_openproperty($stateMessage, PR_BODY, IID_IStream, STGM_DIRECT, MAPI_CREATE | MAPI_MODIFY);
1986
+            mapi_stream_setsize($stream, $encodedStateLength);
1987
+            mapi_stream_write($stream, $encodedState);
1988
+            mapi_stream_commit($stream);
1989
+            mapi_savechanges($stateMessage);
1990
+
1991
+            return $encodedStateLength;
1992
+        }
1993
+
1994
+        return false;
1995
+    }
1996
+
1997
+    /**
1998
+     * Returns the restriction for the state folder name.
1999
+     *
2000
+     * @param string $folderName the state folder name
2001
+     *
2002
+     * @return array
2003
+     */
2004
+    private function getStateFolderRestriction($folderName) {
2005
+        return [RES_AND, [
2006
+            [RES_PROPERTY,
2007
+                [RELOP => RELOP_EQ,
2008
+                    ULPROPTAG => PR_DISPLAY_NAME,
2009
+                    VALUE => $folderName,
2010
+                ],
2011
+            ],
2012
+            [RES_PROPERTY,
2013
+                [RELOP => RELOP_EQ,
2014
+                    ULPROPTAG => PR_ATTR_HIDDEN,
2015
+                    VALUE => true,
2016
+                ],
2017
+            ],
2018
+        ]];
2019
+    }
2020
+
2021
+    /**
2022
+     * Returns the restriction for the associated message in the state folder.
2023
+     *
2024
+     * @param string $messageName     the message name
2025
+     * @param string $counter         counter
2026
+     * @param string $thisCounterOnly (opt) if provided, restrict to the exact counter
2027
+     *
2028
+     * @return array
2029
+     */
2030
+    private function getStateMessageRestriction($messageName, $counter, $thisCounterOnly = false) {
2031
+        return [RES_AND, [
2032
+            [RES_PROPERTY,
2033
+                [RELOP => RELOP_EQ,
2034
+                    ULPROPTAG => PR_DISPLAY_NAME,
2035
+                    VALUE => $messageName,
2036
+                ],
2037
+            ],
2038
+            [RES_PROPERTY,
2039
+                [RELOP => RELOP_EQ,
2040
+                    ULPROPTAG => PR_MESSAGE_CLASS,
2041
+                    VALUE => 'IPM.Note.GrommunioState',
2042
+                ],
2043
+            ],
2044
+            [RES_PROPERTY,
2045
+                [RELOP => $thisCounterOnly ? RELOP_EQ : RELOP_LT,
2046
+                    ULPROPTAG => PR_LAST_VERB_EXECUTED,
2047
+                    VALUE => $counter,
2048
+                ],
2049
+            ],
2050
+        ]];
2051
+    }
2052
+
2053
+    /*----------------------------------------------------------------------------------------------------------
2054 2054
 	 * Private methods
2055 2055
 	 */
2056 2056
 
2057
-	/**
2058
-	 * Returns a hash representing changes in the hierarchy of the main user.
2059
-	 * It changes if a folder is added, renamed or deleted.
2060
-	 *
2061
-	 * @return string
2062
-	 */
2063
-	private function getHierarchyHash() {
2064
-		$rootfolder = mapi_msgstore_openentry($this->defaultstore);
2065
-		$hierarchy = mapi_folder_gethierarchytable($rootfolder, CONVENIENT_DEPTH);
2066
-
2067
-		return md5(serialize(mapi_table_queryallrows($hierarchy, [PR_DISPLAY_NAME, PR_PARENT_ENTRYID])));
2068
-	}
2069
-
2070
-	/**
2071
-	 * Advises a store to the changes sink.
2072
-	 *
2073
-	 * @param mapistore $store store to be advised
2074
-	 *
2075
-	 * @return bool
2076
-	 */
2077
-	private function adviseStoreToSink($store) {
2078
-		// check if we already advised the store
2079
-		if (!in_array($store, $this->changesSinkStores)) {
2080
-			mapi_msgstore_advise($store, null, fnevObjectModified | fnevObjectCreated | fnevObjectMoved | fnevObjectDeleted, $this->changesSink);
2081
-
2082
-			if (mapi_last_hresult()) {
2083
-				SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->adviseStoreToSink(): failed to advised store '%s' with code 0x%X. Polling will be performed.", $store, mapi_last_hresult()));
2084
-
2085
-				return false;
2086
-			}
2087
-
2088
-			SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->adviseStoreToSink(): advised store '%s'", $store));
2089
-			$this->changesSinkStores[] = $store;
2090
-		}
2091
-
2092
-		return true;
2093
-	}
2094
-
2095
-	/**
2096
-	 * Open the store marked with PR_DEFAULT_STORE = TRUE
2097
-	 * if $return_public is set, the public store is opened.
2098
-	 *
2099
-	 * @param string $user User which store should be opened
2100
-	 *
2101
-	 * @return bool
2102
-	 */
2103
-	private function openMessageStore($user) {
2104
-		// During PING requests the operations store has to be switched constantly
2105
-		// the cache prevents the same store opened several times
2106
-		if (isset($this->storeCache[$user])) {
2107
-			return $this->storeCache[$user];
2108
-		}
2109
-
2110
-		$entryid = false;
2111
-		$return_public = false;
2112
-
2113
-		if (strtoupper($user) == 'SYSTEM') {
2114
-			$return_public = true;
2115
-		}
2116
-
2117
-		// loop through the storestable if authenticated user of public folder
2118
-		if ($user == $this->mainUser || $return_public === true) {
2119
-			// Find the default store
2120
-			$storestables = mapi_getmsgstorestable($this->session);
2121
-			$result = mapi_last_hresult();
2122
-
2123
-			if ($result == NOERROR) {
2124
-				$rows = mapi_table_queryallrows($storestables, [PR_ENTRYID, PR_DEFAULT_STORE, PR_MDB_PROVIDER]);
2125
-
2126
-				foreach ($rows as $row) {
2127
-					if (!$return_public && isset($row[PR_DEFAULT_STORE]) && $row[PR_DEFAULT_STORE] == true) {
2128
-						$entryid = $row[PR_ENTRYID];
2129
-
2130
-						break;
2131
-					}
2132
-					if ($return_public && isset($row[PR_MDB_PROVIDER]) && $row[PR_MDB_PROVIDER] == ZARAFA_STORE_PUBLIC_GUID) {
2133
-						$entryid = $row[PR_ENTRYID];
2134
-
2135
-						break;
2136
-					}
2137
-				}
2138
-			}
2139
-		}
2140
-		else {
2141
-			$entryid = @mapi_msgstore_createentryid($this->defaultstore, $user);
2142
-		}
2143
-
2144
-		if ($entryid) {
2145
-			$store = @mapi_openmsgstore($this->session, $entryid);
2146
-
2147
-			if (!$store) {
2148
-				SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->openMessageStore('%s'): Could not open store", $user));
2149
-
2150
-				return false;
2151
-			}
2152
-
2153
-			// add this store to the cache
2154
-			if (!isset($this->storeCache[$user])) {
2155
-				$this->storeCache[$user] = $store;
2156
-			}
2157
-
2158
-			SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->openMessageStore('%s'): Found '%s' store: '%s'", $user, (($return_public) ? 'PUBLIC' : 'DEFAULT'), $store));
2159
-
2160
-			return $store;
2161
-		}
2162
-
2163
-		SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->openMessageStore('%s'): No store found for this user", $user));
2164
-
2165
-		return false;
2166
-	}
2167
-
2168
-	/**
2169
-	 * Checks if the logged in user has secretary permissions on a folder.
2170
-	 *
2171
-	 * @param resource $store
2172
-	 * @param string   $folderid
2173
-	 * @param mixed    $entryid
2174
-	 *
2175
-	 * @return bool
2176
-	 */
2177
-	public function HasSecretaryACLs($store, $folderid, $entryid = false) {
2178
-		if (!$entryid) {
2179
-			$entryid = mapi_msgstore_entryidfromsourcekey($store, hex2bin($folderid));
2180
-			if (!$entryid) {
2181
-				SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->HasSecretaryACLs(): error, no entryid resolved for %s on store %s", $folderid, $store));
2182
-
2183
-				return false;
2184
-			}
2185
-		}
2186
-
2187
-		$folder = mapi_msgstore_openentry($store, $entryid);
2188
-		if (!$folder) {
2189
-			SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->HasSecretaryACLs(): error, could not open folder with entryid %s on store %s", bin2hex($entryid), $store));
2190
-
2191
-			return false;
2192
-		}
2193
-
2194
-		$props = mapi_getprops($folder, [PR_RIGHTS]);
2195
-		if (isset($props[PR_RIGHTS]) &&
2196
-			($props[PR_RIGHTS] & ecRightsReadAny) &&
2197
-			($props[PR_RIGHTS] & ecRightsCreate) &&
2198
-			($props[PR_RIGHTS] & ecRightsEditOwned) &&
2199
-			($props[PR_RIGHTS] & ecRightsDeleteOwned) &&
2200
-			($props[PR_RIGHTS] & ecRightsEditAny) &&
2201
-			($props[PR_RIGHTS] & ecRightsDeleteAny) &&
2202
-			($props[PR_RIGHTS] & ecRightsFolderVisible)) {
2203
-			return true;
2204
-		}
2205
-
2206
-		return false;
2207
-	}
2208
-
2209
-	/**
2210
-	 * The meta function for out of office settings.
2211
-	 *
2212
-	 * @param SyncObject $oof
2213
-	 */
2214
-	private function settingsOOF(&$oof) {
2215
-		// if oof state is set it must be set of oof and get otherwise
2216
-		if (isset($oof->oofstate)) {
2217
-			$this->settingsOofSet($oof);
2218
-		}
2219
-		else {
2220
-			$this->settingsOofGet($oof);
2221
-		}
2222
-	}
2223
-
2224
-	/**
2225
-	 * Gets the out of office settings.
2226
-	 *
2227
-	 * @param SyncObject $oof
2228
-	 */
2229
-	private function settingsOofGet(&$oof) {
2230
-		$oofprops = mapi_getprops($this->defaultstore, [PR_EC_OUTOFOFFICE, PR_EC_OUTOFOFFICE_MSG, PR_EC_OUTOFOFFICE_SUBJECT, PR_EC_OUTOFOFFICE_FROM, PR_EC_OUTOFOFFICE_UNTIL]);
2231
-		$oof->oofstate = SYNC_SETTINGSOOF_DISABLED;
2232
-		$oof->Status = SYNC_SETTINGSSTATUS_SUCCESS;
2233
-		if ($oofprops != false) {
2234
-			$oof->oofstate = isset($oofprops[PR_EC_OUTOFOFFICE]) ? ($oofprops[PR_EC_OUTOFOFFICE] ? SYNC_SETTINGSOOF_GLOBAL : SYNC_SETTINGSOOF_DISABLED) : SYNC_SETTINGSOOF_DISABLED;
2235
-			// TODO external and external unknown
2236
-			$oofmessage = new SyncOOFMessage();
2237
-			$oofmessage->appliesToInternal = "";
2238
-			$oofmessage->enabled = $oof->oofstate;
2239
-			$oofmessage->replymessage = (isset($oofprops[PR_EC_OUTOFOFFICE_MSG])) ? w2u($oofprops[PR_EC_OUTOFOFFICE_MSG]) : "";
2240
-			$oofmessage->bodytype = $oof->bodytype;
2241
-			unset($oofmessage->appliesToExternal, $oofmessage->appliesToExternalUnknown);
2242
-			$oof->oofmessage[] = $oofmessage;
2243
-
2244
-			// check whether time based out of office is set
2245
-			if ($oof->oofstate == SYNC_SETTINGSOOF_GLOBAL && isset($oofprops[PR_EC_OUTOFOFFICE_FROM], $oofprops[PR_EC_OUTOFOFFICE_UNTIL])) {
2246
-				$now = time();
2247
-				if ($now > $oofprops[PR_EC_OUTOFOFFICE_FROM] && $now > $oofprops[PR_EC_OUTOFOFFICE_UNTIL]) {
2248
-					// Out of office is set but the date is in the past. Set the state to disabled.
2249
-					// @see https://jira.z-hub.io/browse/ZP-1188 for details
2250
-					$oof->oofstate = SYNC_SETTINGSOOF_DISABLED;
2251
-					@mapi_setprops($this->defaultstore, [PR_EC_OUTOFOFFICE => false]);
2252
-					@mapi_deleteprops($this->defaultstore, [PR_EC_OUTOFOFFICE_FROM, PR_EC_OUTOFOFFICE_UNTIL]);
2253
-					SLog::Write(LOGLEVEL_INFO, "Grommunio->settingsOofGet(): Out of office is set but the from and until are in the past. Disabling out of office.");
2254
-				}
2255
-				elseif ($oofprops[PR_EC_OUTOFOFFICE_FROM] < $oofprops[PR_EC_OUTOFOFFICE_UNTIL]) {
2256
-					$oof->oofstate = SYNC_SETTINGSOOF_TIMEBASED;
2257
-					$oof->starttime = $oofprops[PR_EC_OUTOFOFFICE_FROM];
2258
-					$oof->endtime = $oofprops[PR_EC_OUTOFOFFICE_UNTIL];
2259
-				}
2260
-				else {
2261
-					SLog::Write(LOGLEVEL_WARN, sprintf(
2262
-						"Grommunio->settingsOofGet(): Time based out of office set but end time ('%s') is before startime ('%s').",
2263
-						date("Y-m-d H:i:s", $oofprops[PR_EC_OUTOFOFFICE_FROM]),
2264
-						date("Y-m-d H:i:s", $oofprops[PR_EC_OUTOFOFFICE_UNTIL])
2265
-					));
2266
-					$oof->Status = SYNC_SETTINGSSTATUS_PROTOCOLLERROR;
2267
-				}
2268
-			}
2269
-			elseif ($oof->oofstate == SYNC_SETTINGSOOF_GLOBAL && (isset($oofprops[PR_EC_OUTOFOFFICE_FROM]) || isset($oofprops[PR_EC_OUTOFOFFICE_UNTIL]))) {
2270
-				SLog::Write(LOGLEVEL_WARN, sprintf(
2271
-					"Grommunio->settingsOofGet(): Time based out of office set but either start time ('%s') or end time ('%s') is missing.",
2272
-					(isset($oofprops[PR_EC_OUTOFOFFICE_FROM]) ? date("Y-m-d H:i:s", $oofprops[PR_EC_OUTOFOFFICE_FROM]) : 'empty'),
2273
-					(isset($oofprops[PR_EC_OUTOFOFFICE_UNTIL]) ? date("Y-m-d H:i:s", $oofprops[PR_EC_OUTOFOFFICE_UNTIL]) : 'empty')
2274
-				));
2275
-				$oof->Status = SYNC_SETTINGSSTATUS_PROTOCOLLERROR;
2276
-			}
2277
-		}
2278
-		else {
2279
-			SLog::Write(LOGLEVEL_WARN, "Grommunio->Unable to get out of office information");
2280
-		}
2281
-
2282
-		// unset body type for oof in order not to stream it
2283
-		unset($oof->bodytype);
2284
-	}
2285
-
2286
-	/**
2287
-	 * Sets the out of office settings.
2288
-	 *
2289
-	 * @param SyncObject $oof
2290
-	 */
2291
-	private function settingsOofSet(&$oof) {
2292
-		$oof->Status = SYNC_SETTINGSSTATUS_SUCCESS;
2293
-		$props = [];
2294
-		if ($oof->oofstate == SYNC_SETTINGSOOF_GLOBAL || $oof->oofstate == SYNC_SETTINGSOOF_TIMEBASED) {
2295
-			$props[PR_EC_OUTOFOFFICE] = true;
2296
-			foreach ($oof->oofmessage as $oofmessage) {
2297
-				if (isset($oofmessage->appliesToInternal)) {
2298
-					$props[PR_EC_OUTOFOFFICE_MSG] = isset($oofmessage->replymessage) ? u2w($oofmessage->replymessage) : "";
2299
-					$props[PR_EC_OUTOFOFFICE_SUBJECT] = "Out of office";
2300
-				}
2301
-			}
2302
-			if ($oof->oofstate == SYNC_SETTINGSOOF_TIMEBASED) {
2303
-				if (isset($oof->starttime, $oof->endtime)) {
2304
-					$props[PR_EC_OUTOFOFFICE_FROM] = $oof->starttime;
2305
-					$props[PR_EC_OUTOFOFFICE_UNTIL] = $oof->endtime;
2306
-				}
2307
-				elseif (isset($oof->starttime) || isset($oof->endtime)) {
2308
-					$oof->Status = SYNC_SETTINGSSTATUS_PROTOCOLLERROR;
2309
-				}
2310
-			}
2311
-			else {
2312
-				$deleteProps = [PR_EC_OUTOFOFFICE_FROM, PR_EC_OUTOFOFFICE_UNTIL];
2313
-			}
2314
-		}
2315
-		elseif ($oof->oofstate == SYNC_SETTINGSOOF_DISABLED) {
2316
-			$props[PR_EC_OUTOFOFFICE] = false;
2317
-			$deleteProps = [PR_EC_OUTOFOFFICE_FROM, PR_EC_OUTOFOFFICE_UNTIL];
2318
-		}
2319
-
2320
-		if (!empty($props)) {
2321
-			@mapi_setprops($this->defaultstore, $props);
2322
-			$result = mapi_last_hresult();
2323
-			if ($result != NOERROR) {
2324
-				SLog::Write(LOGLEVEL_ERROR, sprintf("Grommunio->settingsOofSet(): Setting oof information failed (%X)", $result));
2325
-
2326
-				return false;
2327
-			}
2328
-		}
2329
-
2330
-		if (!empty($deleteProps)) {
2331
-			@mapi_deleteprops($this->defaultstore, $deleteProps);
2332
-		}
2333
-
2334
-		return true;
2335
-	}
2336
-
2337
-	/**
2338
-	 * Gets the user's email address from server.
2339
-	 *
2340
-	 * @param SyncObject $userinformation
2341
-	 */
2342
-	private function settingsUserInformation(&$userinformation) {
2343
-		if (!isset($this->defaultstore) || !isset($this->mainUser)) {
2344
-			SLog::Write(LOGLEVEL_ERROR, "Grommunio->settingsUserInformation(): The store or user are not available for getting user information");
2345
-
2346
-			return false;
2347
-		}
2348
-		$user = nsp_getuserinfo($this->mainUser);
2349
-		if ($user != false) {
2350
-			$userinformation->Status = SYNC_SETTINGSSTATUS_USERINFO_SUCCESS;
2351
-			if (Request::GetProtocolVersion() >= 14.1) {
2352
-				$account = new SyncAccount();
2353
-				$emailaddresses = new SyncEmailAddresses();
2354
-				$emailaddresses->smtpaddress[] = $user["primary_email"];
2355
-				$emailaddresses->primarysmtpaddress = $user["primary_email"];
2356
-				$account->emailaddresses = $emailaddresses;
2357
-				$userinformation->accounts[] = $account;
2358
-			}
2359
-			else {
2360
-				$userinformation->emailaddresses[] = $user["primary_email"];
2361
-			}
2362
-
2363
-			return true;
2364
-		}
2365
-		SLog::Write(LOGLEVEL_ERROR, sprintf("Grommunio->settingsUserInformation(): Getting user information failed: nsp_getuserinfo(%X)", mapi_last_hresult()));
2366
-
2367
-		return false;
2368
-	}
2369
-
2370
-	/**
2371
-	 * Gets the rights management templates from the server.
2372
-	 *
2373
-	 * @param SyncObject $rmTemplates
2374
-	 */
2375
-	private function settingsRightsManagementTemplates(&$rmTemplates) {
2376
-		/* Currently there is no information rights management feature in
2057
+    /**
2058
+     * Returns a hash representing changes in the hierarchy of the main user.
2059
+     * It changes if a folder is added, renamed or deleted.
2060
+     *
2061
+     * @return string
2062
+     */
2063
+    private function getHierarchyHash() {
2064
+        $rootfolder = mapi_msgstore_openentry($this->defaultstore);
2065
+        $hierarchy = mapi_folder_gethierarchytable($rootfolder, CONVENIENT_DEPTH);
2066
+
2067
+        return md5(serialize(mapi_table_queryallrows($hierarchy, [PR_DISPLAY_NAME, PR_PARENT_ENTRYID])));
2068
+    }
2069
+
2070
+    /**
2071
+     * Advises a store to the changes sink.
2072
+     *
2073
+     * @param mapistore $store store to be advised
2074
+     *
2075
+     * @return bool
2076
+     */
2077
+    private function adviseStoreToSink($store) {
2078
+        // check if we already advised the store
2079
+        if (!in_array($store, $this->changesSinkStores)) {
2080
+            mapi_msgstore_advise($store, null, fnevObjectModified | fnevObjectCreated | fnevObjectMoved | fnevObjectDeleted, $this->changesSink);
2081
+
2082
+            if (mapi_last_hresult()) {
2083
+                SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->adviseStoreToSink(): failed to advised store '%s' with code 0x%X. Polling will be performed.", $store, mapi_last_hresult()));
2084
+
2085
+                return false;
2086
+            }
2087
+
2088
+            SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->adviseStoreToSink(): advised store '%s'", $store));
2089
+            $this->changesSinkStores[] = $store;
2090
+        }
2091
+
2092
+        return true;
2093
+    }
2094
+
2095
+    /**
2096
+     * Open the store marked with PR_DEFAULT_STORE = TRUE
2097
+     * if $return_public is set, the public store is opened.
2098
+     *
2099
+     * @param string $user User which store should be opened
2100
+     *
2101
+     * @return bool
2102
+     */
2103
+    private function openMessageStore($user) {
2104
+        // During PING requests the operations store has to be switched constantly
2105
+        // the cache prevents the same store opened several times
2106
+        if (isset($this->storeCache[$user])) {
2107
+            return $this->storeCache[$user];
2108
+        }
2109
+
2110
+        $entryid = false;
2111
+        $return_public = false;
2112
+
2113
+        if (strtoupper($user) == 'SYSTEM') {
2114
+            $return_public = true;
2115
+        }
2116
+
2117
+        // loop through the storestable if authenticated user of public folder
2118
+        if ($user == $this->mainUser || $return_public === true) {
2119
+            // Find the default store
2120
+            $storestables = mapi_getmsgstorestable($this->session);
2121
+            $result = mapi_last_hresult();
2122
+
2123
+            if ($result == NOERROR) {
2124
+                $rows = mapi_table_queryallrows($storestables, [PR_ENTRYID, PR_DEFAULT_STORE, PR_MDB_PROVIDER]);
2125
+
2126
+                foreach ($rows as $row) {
2127
+                    if (!$return_public && isset($row[PR_DEFAULT_STORE]) && $row[PR_DEFAULT_STORE] == true) {
2128
+                        $entryid = $row[PR_ENTRYID];
2129
+
2130
+                        break;
2131
+                    }
2132
+                    if ($return_public && isset($row[PR_MDB_PROVIDER]) && $row[PR_MDB_PROVIDER] == ZARAFA_STORE_PUBLIC_GUID) {
2133
+                        $entryid = $row[PR_ENTRYID];
2134
+
2135
+                        break;
2136
+                    }
2137
+                }
2138
+            }
2139
+        }
2140
+        else {
2141
+            $entryid = @mapi_msgstore_createentryid($this->defaultstore, $user);
2142
+        }
2143
+
2144
+        if ($entryid) {
2145
+            $store = @mapi_openmsgstore($this->session, $entryid);
2146
+
2147
+            if (!$store) {
2148
+                SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->openMessageStore('%s'): Could not open store", $user));
2149
+
2150
+                return false;
2151
+            }
2152
+
2153
+            // add this store to the cache
2154
+            if (!isset($this->storeCache[$user])) {
2155
+                $this->storeCache[$user] = $store;
2156
+            }
2157
+
2158
+            SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->openMessageStore('%s'): Found '%s' store: '%s'", $user, (($return_public) ? 'PUBLIC' : 'DEFAULT'), $store));
2159
+
2160
+            return $store;
2161
+        }
2162
+
2163
+        SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->openMessageStore('%s'): No store found for this user", $user));
2164
+
2165
+        return false;
2166
+    }
2167
+
2168
+    /**
2169
+     * Checks if the logged in user has secretary permissions on a folder.
2170
+     *
2171
+     * @param resource $store
2172
+     * @param string   $folderid
2173
+     * @param mixed    $entryid
2174
+     *
2175
+     * @return bool
2176
+     */
2177
+    public function HasSecretaryACLs($store, $folderid, $entryid = false) {
2178
+        if (!$entryid) {
2179
+            $entryid = mapi_msgstore_entryidfromsourcekey($store, hex2bin($folderid));
2180
+            if (!$entryid) {
2181
+                SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->HasSecretaryACLs(): error, no entryid resolved for %s on store %s", $folderid, $store));
2182
+
2183
+                return false;
2184
+            }
2185
+        }
2186
+
2187
+        $folder = mapi_msgstore_openentry($store, $entryid);
2188
+        if (!$folder) {
2189
+            SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->HasSecretaryACLs(): error, could not open folder with entryid %s on store %s", bin2hex($entryid), $store));
2190
+
2191
+            return false;
2192
+        }
2193
+
2194
+        $props = mapi_getprops($folder, [PR_RIGHTS]);
2195
+        if (isset($props[PR_RIGHTS]) &&
2196
+            ($props[PR_RIGHTS] & ecRightsReadAny) &&
2197
+            ($props[PR_RIGHTS] & ecRightsCreate) &&
2198
+            ($props[PR_RIGHTS] & ecRightsEditOwned) &&
2199
+            ($props[PR_RIGHTS] & ecRightsDeleteOwned) &&
2200
+            ($props[PR_RIGHTS] & ecRightsEditAny) &&
2201
+            ($props[PR_RIGHTS] & ecRightsDeleteAny) &&
2202
+            ($props[PR_RIGHTS] & ecRightsFolderVisible)) {
2203
+            return true;
2204
+        }
2205
+
2206
+        return false;
2207
+    }
2208
+
2209
+    /**
2210
+     * The meta function for out of office settings.
2211
+     *
2212
+     * @param SyncObject $oof
2213
+     */
2214
+    private function settingsOOF(&$oof) {
2215
+        // if oof state is set it must be set of oof and get otherwise
2216
+        if (isset($oof->oofstate)) {
2217
+            $this->settingsOofSet($oof);
2218
+        }
2219
+        else {
2220
+            $this->settingsOofGet($oof);
2221
+        }
2222
+    }
2223
+
2224
+    /**
2225
+     * Gets the out of office settings.
2226
+     *
2227
+     * @param SyncObject $oof
2228
+     */
2229
+    private function settingsOofGet(&$oof) {
2230
+        $oofprops = mapi_getprops($this->defaultstore, [PR_EC_OUTOFOFFICE, PR_EC_OUTOFOFFICE_MSG, PR_EC_OUTOFOFFICE_SUBJECT, PR_EC_OUTOFOFFICE_FROM, PR_EC_OUTOFOFFICE_UNTIL]);
2231
+        $oof->oofstate = SYNC_SETTINGSOOF_DISABLED;
2232
+        $oof->Status = SYNC_SETTINGSSTATUS_SUCCESS;
2233
+        if ($oofprops != false) {
2234
+            $oof->oofstate = isset($oofprops[PR_EC_OUTOFOFFICE]) ? ($oofprops[PR_EC_OUTOFOFFICE] ? SYNC_SETTINGSOOF_GLOBAL : SYNC_SETTINGSOOF_DISABLED) : SYNC_SETTINGSOOF_DISABLED;
2235
+            // TODO external and external unknown
2236
+            $oofmessage = new SyncOOFMessage();
2237
+            $oofmessage->appliesToInternal = "";
2238
+            $oofmessage->enabled = $oof->oofstate;
2239
+            $oofmessage->replymessage = (isset($oofprops[PR_EC_OUTOFOFFICE_MSG])) ? w2u($oofprops[PR_EC_OUTOFOFFICE_MSG]) : "";
2240
+            $oofmessage->bodytype = $oof->bodytype;
2241
+            unset($oofmessage->appliesToExternal, $oofmessage->appliesToExternalUnknown);
2242
+            $oof->oofmessage[] = $oofmessage;
2243
+
2244
+            // check whether time based out of office is set
2245
+            if ($oof->oofstate == SYNC_SETTINGSOOF_GLOBAL && isset($oofprops[PR_EC_OUTOFOFFICE_FROM], $oofprops[PR_EC_OUTOFOFFICE_UNTIL])) {
2246
+                $now = time();
2247
+                if ($now > $oofprops[PR_EC_OUTOFOFFICE_FROM] && $now > $oofprops[PR_EC_OUTOFOFFICE_UNTIL]) {
2248
+                    // Out of office is set but the date is in the past. Set the state to disabled.
2249
+                    // @see https://jira.z-hub.io/browse/ZP-1188 for details
2250
+                    $oof->oofstate = SYNC_SETTINGSOOF_DISABLED;
2251
+                    @mapi_setprops($this->defaultstore, [PR_EC_OUTOFOFFICE => false]);
2252
+                    @mapi_deleteprops($this->defaultstore, [PR_EC_OUTOFOFFICE_FROM, PR_EC_OUTOFOFFICE_UNTIL]);
2253
+                    SLog::Write(LOGLEVEL_INFO, "Grommunio->settingsOofGet(): Out of office is set but the from and until are in the past. Disabling out of office.");
2254
+                }
2255
+                elseif ($oofprops[PR_EC_OUTOFOFFICE_FROM] < $oofprops[PR_EC_OUTOFOFFICE_UNTIL]) {
2256
+                    $oof->oofstate = SYNC_SETTINGSOOF_TIMEBASED;
2257
+                    $oof->starttime = $oofprops[PR_EC_OUTOFOFFICE_FROM];
2258
+                    $oof->endtime = $oofprops[PR_EC_OUTOFOFFICE_UNTIL];
2259
+                }
2260
+                else {
2261
+                    SLog::Write(LOGLEVEL_WARN, sprintf(
2262
+                        "Grommunio->settingsOofGet(): Time based out of office set but end time ('%s') is before startime ('%s').",
2263
+                        date("Y-m-d H:i:s", $oofprops[PR_EC_OUTOFOFFICE_FROM]),
2264
+                        date("Y-m-d H:i:s", $oofprops[PR_EC_OUTOFOFFICE_UNTIL])
2265
+                    ));
2266
+                    $oof->Status = SYNC_SETTINGSSTATUS_PROTOCOLLERROR;
2267
+                }
2268
+            }
2269
+            elseif ($oof->oofstate == SYNC_SETTINGSOOF_GLOBAL && (isset($oofprops[PR_EC_OUTOFOFFICE_FROM]) || isset($oofprops[PR_EC_OUTOFOFFICE_UNTIL]))) {
2270
+                SLog::Write(LOGLEVEL_WARN, sprintf(
2271
+                    "Grommunio->settingsOofGet(): Time based out of office set but either start time ('%s') or end time ('%s') is missing.",
2272
+                    (isset($oofprops[PR_EC_OUTOFOFFICE_FROM]) ? date("Y-m-d H:i:s", $oofprops[PR_EC_OUTOFOFFICE_FROM]) : 'empty'),
2273
+                    (isset($oofprops[PR_EC_OUTOFOFFICE_UNTIL]) ? date("Y-m-d H:i:s", $oofprops[PR_EC_OUTOFOFFICE_UNTIL]) : 'empty')
2274
+                ));
2275
+                $oof->Status = SYNC_SETTINGSSTATUS_PROTOCOLLERROR;
2276
+            }
2277
+        }
2278
+        else {
2279
+            SLog::Write(LOGLEVEL_WARN, "Grommunio->Unable to get out of office information");
2280
+        }
2281
+
2282
+        // unset body type for oof in order not to stream it
2283
+        unset($oof->bodytype);
2284
+    }
2285
+
2286
+    /**
2287
+     * Sets the out of office settings.
2288
+     *
2289
+     * @param SyncObject $oof
2290
+     */
2291
+    private function settingsOofSet(&$oof) {
2292
+        $oof->Status = SYNC_SETTINGSSTATUS_SUCCESS;
2293
+        $props = [];
2294
+        if ($oof->oofstate == SYNC_SETTINGSOOF_GLOBAL || $oof->oofstate == SYNC_SETTINGSOOF_TIMEBASED) {
2295
+            $props[PR_EC_OUTOFOFFICE] = true;
2296
+            foreach ($oof->oofmessage as $oofmessage) {
2297
+                if (isset($oofmessage->appliesToInternal)) {
2298
+                    $props[PR_EC_OUTOFOFFICE_MSG] = isset($oofmessage->replymessage) ? u2w($oofmessage->replymessage) : "";
2299
+                    $props[PR_EC_OUTOFOFFICE_SUBJECT] = "Out of office";
2300
+                }
2301
+            }
2302
+            if ($oof->oofstate == SYNC_SETTINGSOOF_TIMEBASED) {
2303
+                if (isset($oof->starttime, $oof->endtime)) {
2304
+                    $props[PR_EC_OUTOFOFFICE_FROM] = $oof->starttime;
2305
+                    $props[PR_EC_OUTOFOFFICE_UNTIL] = $oof->endtime;
2306
+                }
2307
+                elseif (isset($oof->starttime) || isset($oof->endtime)) {
2308
+                    $oof->Status = SYNC_SETTINGSSTATUS_PROTOCOLLERROR;
2309
+                }
2310
+            }
2311
+            else {
2312
+                $deleteProps = [PR_EC_OUTOFOFFICE_FROM, PR_EC_OUTOFOFFICE_UNTIL];
2313
+            }
2314
+        }
2315
+        elseif ($oof->oofstate == SYNC_SETTINGSOOF_DISABLED) {
2316
+            $props[PR_EC_OUTOFOFFICE] = false;
2317
+            $deleteProps = [PR_EC_OUTOFOFFICE_FROM, PR_EC_OUTOFOFFICE_UNTIL];
2318
+        }
2319
+
2320
+        if (!empty($props)) {
2321
+            @mapi_setprops($this->defaultstore, $props);
2322
+            $result = mapi_last_hresult();
2323
+            if ($result != NOERROR) {
2324
+                SLog::Write(LOGLEVEL_ERROR, sprintf("Grommunio->settingsOofSet(): Setting oof information failed (%X)", $result));
2325
+
2326
+                return false;
2327
+            }
2328
+        }
2329
+
2330
+        if (!empty($deleteProps)) {
2331
+            @mapi_deleteprops($this->defaultstore, $deleteProps);
2332
+        }
2333
+
2334
+        return true;
2335
+    }
2336
+
2337
+    /**
2338
+     * Gets the user's email address from server.
2339
+     *
2340
+     * @param SyncObject $userinformation
2341
+     */
2342
+    private function settingsUserInformation(&$userinformation) {
2343
+        if (!isset($this->defaultstore) || !isset($this->mainUser)) {
2344
+            SLog::Write(LOGLEVEL_ERROR, "Grommunio->settingsUserInformation(): The store or user are not available for getting user information");
2345
+
2346
+            return false;
2347
+        }
2348
+        $user = nsp_getuserinfo($this->mainUser);
2349
+        if ($user != false) {
2350
+            $userinformation->Status = SYNC_SETTINGSSTATUS_USERINFO_SUCCESS;
2351
+            if (Request::GetProtocolVersion() >= 14.1) {
2352
+                $account = new SyncAccount();
2353
+                $emailaddresses = new SyncEmailAddresses();
2354
+                $emailaddresses->smtpaddress[] = $user["primary_email"];
2355
+                $emailaddresses->primarysmtpaddress = $user["primary_email"];
2356
+                $account->emailaddresses = $emailaddresses;
2357
+                $userinformation->accounts[] = $account;
2358
+            }
2359
+            else {
2360
+                $userinformation->emailaddresses[] = $user["primary_email"];
2361
+            }
2362
+
2363
+            return true;
2364
+        }
2365
+        SLog::Write(LOGLEVEL_ERROR, sprintf("Grommunio->settingsUserInformation(): Getting user information failed: nsp_getuserinfo(%X)", mapi_last_hresult()));
2366
+
2367
+        return false;
2368
+    }
2369
+
2370
+    /**
2371
+     * Gets the rights management templates from the server.
2372
+     *
2373
+     * @param SyncObject $rmTemplates
2374
+     */
2375
+    private function settingsRightsManagementTemplates(&$rmTemplates) {
2376
+        /* Currently there is no information rights management feature in
2377 2377
 		 * the grommunio backend, so just return the status and empty
2378 2378
 		 * SyncRightsManagementTemplates tag.
2379 2379
 		 * Once it's available, it would be something like:
@@ -2384,588 +2384,588 @@  discard block
 block discarded – undo
2384 2384
 		$rmTemplate->description = "What does the template do. E.g. it disables forward and reply.";
2385 2385
 		$rmTemplates->rmtemplates[] = $rmTemplate;
2386 2386
 		 */
2387
-		$rmTemplates->Status = SYNC_COMMONSTATUS_IRMFEATUREDISABLED;
2388
-		$rmTemplates->rmtemplates = [];
2389
-	}
2390
-
2391
-	/**
2392
-	 * Sets the importance and priority of a message from a RFC822 message headers.
2393
-	 *
2394
-	 * @param int   $xPriority
2395
-	 * @param array $mapiprops
2396
-	 * @param mixed $sendMailProps
2397
-	 */
2398
-	private function getImportanceAndPriority($xPriority, &$mapiprops, $sendMailProps) {
2399
-		switch ($xPriority) {
2400
-			case 1:
2401
-			case 2:
2402
-				$priority = PRIO_URGENT;
2403
-				$importance = IMPORTANCE_HIGH;
2404
-				break;
2405
-
2406
-			case 4:
2407
-			case 5:
2408
-				$priority = PRIO_NONURGENT;
2409
-				$importance = IMPORTANCE_LOW;
2410
-				break;
2411
-
2412
-			case 3:
2413
-			default:
2414
-				$priority = PRIO_NORMAL;
2415
-				$importance = IMPORTANCE_NORMAL;
2416
-				break;
2417
-		}
2418
-		$mapiprops[$sendMailProps["importance"]] = $importance;
2419
-		$mapiprops[$sendMailProps["priority"]] = $priority;
2420
-	}
2421
-
2422
-	/**
2423
-	 * Copies attachments from one message to another.
2424
-	 *
2425
-	 * @param MAPIMessage $toMessage
2426
-	 * @param MAPIMessage $fromMessage
2427
-	 */
2428
-	private function copyAttachments(&$toMessage, $fromMessage) {
2429
-		$attachtable = mapi_message_getattachmenttable($fromMessage);
2430
-		$rows = mapi_table_queryallrows($attachtable, [PR_ATTACH_NUM]);
2431
-
2432
-		foreach ($rows as $row) {
2433
-			if (isset($row[PR_ATTACH_NUM])) {
2434
-				$attach = mapi_message_openattach($fromMessage, $row[PR_ATTACH_NUM]);
2435
-				$newattach = mapi_message_createattach($toMessage);
2436
-				mapi_copyto($attach, [], [], $newattach, 0);
2437
-				mapi_savechanges($newattach);
2438
-			}
2439
-		}
2440
-	}
2441
-
2442
-	/**
2443
-	 * Function will create a search folder in FINDER_ROOT folder
2444
-	 * if folder exists then it will open it.
2445
-	 *
2446
-	 * @see createSearchFolder($store, $openIfExists = true) function in the webaccess
2447
-	 *
2448
-	 * @return mapiFolderObject $folder created search folder
2449
-	 */
2450
-	private function getSearchFolder() {
2451
-		// create new or open existing search folder
2452
-		$searchFolderRoot = $this->getSearchFoldersRoot($this->store);
2453
-		if ($searchFolderRoot === false) {
2454
-			// error in finding search root folder
2455
-			// or store doesn't support search folders
2456
-			return false;
2457
-		}
2458
-
2459
-		$searchFolder = $this->createSearchFolder($searchFolderRoot);
2460
-
2461
-		if ($searchFolder !== false && mapi_last_hresult() == NOERROR) {
2462
-			return $searchFolder;
2463
-		}
2464
-
2465
-		return false;
2466
-	}
2467
-
2468
-	/**
2469
-	 * Function will open FINDER_ROOT folder in root container
2470
-	 * public folder's don't have FINDER_ROOT folder.
2471
-	 *
2472
-	 * @see getSearchFoldersRoot($store) function in the webaccess
2473
-	 *
2474
-	 * @return mapiFolderObject root folder for search folders
2475
-	 */
2476
-	private function getSearchFoldersRoot() {
2477
-		// check if we can create search folders
2478
-		$storeProps = mapi_getprops($this->store, [PR_STORE_SUPPORT_MASK, PR_FINDER_ENTRYID]);
2479
-		if (($storeProps[PR_STORE_SUPPORT_MASK] & STORE_SEARCH_OK) != STORE_SEARCH_OK) {
2480
-			SLog::Write(LOGLEVEL_WARN, "Grommunio->getSearchFoldersRoot(): Store doesn't support search folders. Public store doesn't have FINDER_ROOT folder");
2481
-
2482
-			return false;
2483
-		}
2484
-
2485
-		// open search folders root
2486
-		$searchRootFolder = mapi_msgstore_openentry($this->store, $storeProps[PR_FINDER_ENTRYID]);
2487
-		if (mapi_last_hresult() != NOERROR) {
2488
-			SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->getSearchFoldersRoot(): Unable to open search folder (0x%X)", mapi_last_hresult()));
2489
-
2490
-			return false;
2491
-		}
2492
-
2493
-		return $searchRootFolder;
2494
-	}
2495
-
2496
-	/**
2497
-	 * Creates a search folder if it not exists or opens an existing one
2498
-	 * and returns it.
2499
-	 *
2500
-	 * @param mapiFolderObject $searchFolderRoot
2501
-	 *
2502
-	 * @return mapiFolderObject
2503
-	 */
2504
-	private function createSearchFolder($searchFolderRoot) {
2505
-		$folderName = "grommunio-sync Search Folder " . @getmypid();
2506
-		$searchFolders = mapi_folder_gethierarchytable($searchFolderRoot);
2507
-		$restriction = [
2508
-			RES_CONTENT,
2509
-			[
2510
-				FUZZYLEVEL => FL_PREFIX,
2511
-				ULPROPTAG => PR_DISPLAY_NAME,
2512
-				VALUE => [PR_DISPLAY_NAME => $folderName],
2513
-			],
2514
-		];
2515
-		// restrict the hierarchy to the grommunio-sync search folder only
2516
-		mapi_table_restrict($searchFolders, $restriction);
2517
-		if (mapi_table_getrowcount($searchFolders)) {
2518
-			$searchFolder = mapi_table_queryrows($searchFolders, [PR_ENTRYID], 0, 1);
2519
-
2520
-			return mapi_msgstore_openentry($this->store, $searchFolder[0][PR_ENTRYID]);
2521
-		}
2522
-
2523
-		return mapi_folder_createfolder($searchFolderRoot, $folderName, null, 0, FOLDER_SEARCH);
2524
-	}
2525
-
2526
-	/**
2527
-	 * Creates a search restriction.
2528
-	 *
2529
-	 * @param ContentParameter $cpo
2530
-	 *
2531
-	 * @return array
2532
-	 */
2533
-	private function getSearchRestriction($cpo) {
2534
-		$searchText = $cpo->GetSearchFreeText();
2535
-
2536
-		$searchGreater = strtotime($cpo->GetSearchValueGreater());
2537
-		$searchLess = strtotime($cpo->GetSearchValueLess());
2538
-
2539
-		if (version_compare(phpversion(), '5.3.4') < 0) {
2540
-			SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->getSearchRestriction(): Your system's PHP version (%s) might not correctly process unicode strings. Search containing such characters might not return correct results. It is recommended to update to at least PHP 5.3.4. See ZP-541 for more information.", phpversion()));
2541
-		}
2542
-		// split the search on whitespache and look for every word
2543
-		$searchText = preg_split("/\\W+/u", $searchText);
2544
-		$searchProps = [PR_BODY, PR_SUBJECT, PR_DISPLAY_TO, PR_DISPLAY_CC, PR_SENDER_NAME, PR_SENDER_EMAIL_ADDRESS, PR_SENT_REPRESENTING_NAME, PR_SENT_REPRESENTING_EMAIL_ADDRESS];
2545
-		$resAnd = [];
2546
-		foreach ($searchText as $term) {
2547
-			$resOr = [];
2548
-
2549
-			foreach ($searchProps as $property) {
2550
-				array_push(
2551
-					$resOr,
2552
-					[RES_CONTENT,
2553
-						[
2554
-							FUZZYLEVEL => FL_SUBSTRING | FL_IGNORECASE,
2555
-							ULPROPTAG => $property,
2556
-							VALUE => u2w($term),
2557
-						],
2558
-					]
2559
-				);
2560
-			}
2561
-			array_push($resAnd, [RES_OR, $resOr]);
2562
-		}
2563
-
2564
-		// add time range restrictions
2565
-		if ($searchGreater) {
2566
-			array_push($resAnd, [RES_PROPERTY, [RELOP => RELOP_GE, ULPROPTAG => PR_MESSAGE_DELIVERY_TIME, VALUE => [PR_MESSAGE_DELIVERY_TIME => $searchGreater]]]); // RES_AND;
2567
-		}
2568
-		if ($searchLess) {
2569
-			array_push($resAnd, [RES_PROPERTY, [RELOP => RELOP_LE, ULPROPTAG => PR_MESSAGE_DELIVERY_TIME, VALUE => [PR_MESSAGE_DELIVERY_TIME => $searchLess]]]);
2570
-		}
2571
-
2572
-		return [RES_AND, $resAnd];
2573
-	}
2574
-
2575
-	/**
2576
-	 * Resolve recipient based on his email address.
2577
-	 *
2578
-	 * @param string $to
2579
-	 * @param int    $maxAmbiguousRecipients
2580
-	 * @param bool   $expandDistlist
2581
-	 *
2582
-	 * @return bool|SyncResolveRecipient
2583
-	 */
2584
-	private function resolveRecipient($to, $maxAmbiguousRecipients, $expandDistlist = true) {
2585
-		$recipient = $this->resolveRecipientGAL($to, $maxAmbiguousRecipients, $expandDistlist);
2586
-
2587
-		if ($recipient !== false) {
2588
-			return $recipient;
2589
-		}
2590
-
2591
-		$recipient = $this->resolveRecipientContact($to, $maxAmbiguousRecipients);
2592
-
2593
-		if ($recipient !== false) {
2594
-			return $recipient;
2595
-		}
2596
-
2597
-		return false;
2598
-	}
2599
-
2600
-	/**
2601
-	 * Resolves recipient from the GAL and gets his certificates.
2602
-	 *
2603
-	 * @param string $to
2604
-	 * @param int    $maxAmbiguousRecipients
2605
-	 * @param bool   $expandDistlist
2606
-	 *
2607
-	 * @return array|bool
2608
-	 */
2609
-	private function resolveRecipientGAL($to, $maxAmbiguousRecipients, $expandDistlist = true) {
2610
-		SLog::Write(LOGLEVEL_WBXML, sprintf("Grommunio->resolveRecipientGAL(): Resolving recipient '%s' in GAL", $to));
2611
-		$addrbook = $this->getAddressbook();
2612
-		// FIXME: create a function to get the adressbook contentstable
2613
-		$ab_entryid = mapi_ab_getdefaultdir($addrbook);
2614
-		if ($ab_entryid) {
2615
-			$ab_dir = mapi_ab_openentry($addrbook, $ab_entryid);
2616
-		}
2617
-		if ($ab_dir) {
2618
-			$table = mapi_folder_getcontentstable($ab_dir);
2619
-		}
2620
-
2621
-		if (!$table) {
2622
-			SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->resolveRecipientGAL(): Unable to open addressbook:0x%X", mapi_last_hresult()));
2623
-
2624
-			return false;
2625
-		}
2626
-
2627
-		$restriction = MAPIUtils::GetSearchRestriction(u2w($to));
2628
-		mapi_table_restrict($table, $restriction);
2629
-
2630
-		$querycnt = mapi_table_getrowcount($table);
2631
-		if ($querycnt > 0) {
2632
-			$recipientGal = [];
2633
-			$rowsToQuery = $maxAmbiguousRecipients;
2634
-			// some devices request 0 ambiguous recipients
2635
-			if ($querycnt == 1 && $maxAmbiguousRecipients == 0) {
2636
-				$rowsToQuery = 1;
2637
-			}
2638
-			elseif ($querycnt > 1 && $maxAmbiguousRecipients == 0) {
2639
-				SLog::Write(LOGLEVEL_INFO, sprintf("Grommunio->resolveRecipientGAL(): GAL search found %d recipients but the device hasn't requested ambiguous recipients", $querycnt));
2640
-
2641
-				return $recipientGal;
2642
-			}
2643
-			elseif ($querycnt > 1 && $maxAmbiguousRecipients == 1) {
2644
-				$rowsToQuery = $querycnt;
2645
-			}
2646
-			// get the certificate every time because caching the certificate is less expensive than opening addressbook entry again
2647
-			$abentries = mapi_table_queryrows($table, [PR_ENTRYID, PR_DISPLAY_NAME, PR_EMS_AB_TAGGED_X509_CERT, PR_OBJECT_TYPE, PR_SMTP_ADDRESS], 0, $rowsToQuery);
2648
-			for ($i = 0, $nrEntries = count($abentries); $i < $nrEntries; ++$i) {
2649
-				if (strcasecmp($abentries[$i][PR_SMTP_ADDRESS], $to) !== 0 && $maxAmbiguousRecipients == 1) {
2650
-					SLog::Write(LOGLEVEL_INFO, sprintf("Grommunio->resolveRecipientGAL(): maxAmbiguousRecipients is 1 and found non-matching user (to '%s' found: '%s')", $to, $abentries[$i][PR_SMTP_ADDRESS]));
2651
-
2652
-					continue;
2653
-				}
2654
-				if ($abentries[$i][PR_OBJECT_TYPE] == MAPI_DISTLIST) {
2655
-					// check whether to expand dist list
2656
-					if ($expandDistlist) {
2657
-						SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->resolveRecipientGAL(): '%s' is a dist list. Expand it to members.", $to));
2658
-						$distList = mapi_ab_openentry($addrbook, $abentries[$i][PR_ENTRYID]);
2659
-						$distListContent = mapi_folder_getcontentstable($distList);
2660
-						$distListMembers = mapi_table_queryallrows($distListContent, [PR_ENTRYID, PR_DISPLAY_NAME, PR_EMS_AB_TAGGED_X509_CERT]);
2661
-						for ($j = 0, $nrDistListMembers = mapi_table_getrowcount($distListContent); $j < $nrDistListMembers; ++$j) {
2662
-							SLog::Write(LOGLEVEL_WBXML, sprintf("Grommunio->resolveRecipientGAL(): distlist's '%s' member: '%s'", $to, $distListMembers[$j][PR_DISPLAY_NAME]));
2663
-							$recipientGal[] = $this->createResolveRecipient(SYNC_RESOLVERECIPIENTS_TYPE_GAL, $to, $distListMembers[$j], $nrDistListMembers);
2664
-						}
2665
-					}
2666
-					else {
2667
-						SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->resolveRecipientGAL(): '%s' is a dist list, but return it as is.", $to));
2668
-						$recipientGal[] = $this->createResolveRecipient(SYNC_RESOLVERECIPIENTS_TYPE_GAL, $abentries[$i][PR_SMTP_ADDRESS], $abentries[$i]);
2669
-					}
2670
-				}
2671
-				elseif ($abentries[$i][PR_OBJECT_TYPE] == MAPI_MAILUSER) {
2672
-					$recipientGal[] = $this->createResolveRecipient(SYNC_RESOLVERECIPIENTS_TYPE_GAL, $abentries[$i][PR_SMTP_ADDRESS], $abentries[$i]);
2673
-				}
2674
-			}
2675
-
2676
-			SLog::Write(LOGLEVEL_WBXML, "Grommunio->resolveRecipientGAL(): Found a recipient in GAL");
2677
-
2678
-			return $recipientGal;
2679
-		}
2680
-
2681
-		SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->resolveRecipientGAL(): No recipient found for: '%s' in GAL", $to));
2682
-
2683
-		return SYNC_RESOLVERECIPSSTATUS_RESPONSE_UNRESOLVEDRECIP;
2684
-
2685
-		return false;
2686
-	}
2687
-
2688
-	/**
2689
-	 * Resolves recipient from the contact list and gets his certificates.
2690
-	 *
2691
-	 * @param string $to
2692
-	 * @param int    $maxAmbiguousRecipients
2693
-	 *
2694
-	 * @return array|bool
2695
-	 */
2696
-	private function resolveRecipientContact($to, $maxAmbiguousRecipients) {
2697
-		SLog::Write(LOGLEVEL_WBXML, sprintf("Grommunio->resolveRecipientContact(): Resolving recipient '%s' in user's contacts", $to));
2698
-		// go through all contact folders of the user and
2699
-		// check if there's a contact with the given email address
2700
-		$root = mapi_msgstore_openentry($this->defaultstore);
2701
-		if (!$root) {
2702
-			SLog::Write(LOGLEVEL_ERROR, sprintf("Grommunio->resolveRecipientContact(): Unable to open default store: 0x%X", mapi_last_hresult()));
2703
-		}
2704
-		$rootprops = mapi_getprops($root, [PR_IPM_CONTACT_ENTRYID]);
2705
-		$contacts = $this->getContactsFromFolder($this->defaultstore, $rootprops[PR_IPM_CONTACT_ENTRYID], $to);
2706
-		$recipients = [];
2707
-
2708
-		if ($contacts !== false) {
2709
-			SLog::Write(LOGLEVEL_WBXML, sprintf("Grommunio->resolveRecipientContact(): Found %d contacts in main contacts folder.", count($contacts)));
2710
-			// create resolve recipient object
2711
-			foreach ($contacts as $contact) {
2712
-				$recipients[] = $this->createResolveRecipient(SYNC_RESOLVERECIPIENTS_TYPE_CONTACT, $to, $contact);
2713
-			}
2714
-		}
2715
-
2716
-		$contactfolder = mapi_msgstore_openentry($this->defaultstore, $rootprops[PR_IPM_CONTACT_ENTRYID]);
2717
-		$subfolders = MAPIUtils::GetSubfoldersForType($contactfolder, "IPF.Contact");
2718
-		if ($subfolders !== false) {
2719
-			foreach ($subfolders as $folder) {
2720
-				$contacts = $this->getContactsFromFolder($this->defaultstore, $folder[PR_ENTRYID], $to);
2721
-				if ($contacts !== false) {
2722
-					SLog::Write(LOGLEVEL_WBXML, sprintf("Grommunio->resolveRecipientContact(): Found %d contacts in contacts' subfolder.", count($contacts)));
2723
-					foreach ($contacts as $contact) {
2724
-						$recipients[] = $this->createResolveRecipient(SYNC_RESOLVERECIPIENTS_TYPE_CONTACT, $to, $contact);
2725
-					}
2726
-				}
2727
-			}
2728
-		}
2729
-
2730
-		// search contacts in public folders
2731
-		$storestables = mapi_getmsgstorestable($this->session);
2732
-		$result = mapi_last_hresult();
2733
-
2734
-		if ($result == NOERROR) {
2735
-			$rows = mapi_table_queryallrows($storestables, [PR_ENTRYID, PR_DEFAULT_STORE, PR_MDB_PROVIDER]);
2736
-			foreach ($rows as $row) {
2737
-				if (isset($row[PR_MDB_PROVIDER]) && $row[PR_MDB_PROVIDER] == ZARAFA_STORE_PUBLIC_GUID) {
2738
-					// TODO refactor public store
2739
-					$publicstore = mapi_openmsgstore($this->session, $row[PR_ENTRYID]);
2740
-					$publicfolder = mapi_msgstore_openentry($publicstore);
2741
-
2742
-					$subfolders = MAPIUtils::GetSubfoldersForType($publicfolder, "IPF.Contact");
2743
-					if ($subfolders !== false) {
2744
-						foreach ($subfolders as $folder) {
2745
-							$contacts = $this->getContactsFromFolder($publicstore, $folder[PR_ENTRYID], $to);
2746
-							if ($contacts !== false) {
2747
-								SLog::Write(LOGLEVEL_WBXML, sprintf("Grommunio->resolveRecipientContact(): Found %d contacts in public contacts folder.", count($contacts)));
2748
-								foreach ($contacts as $contact) {
2749
-									$recipients[] = $this->createResolveRecipient(SYNC_RESOLVERECIPIENTS_TYPE_CONTACT, $to, $contact);
2750
-								}
2751
-							}
2752
-						}
2753
-					}
2754
-
2755
-					break;
2756
-				}
2757
-			}
2758
-		}
2759
-		else {
2760
-			SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->resolveRecipientContact(): Unable to open public store: 0x%X", $result));
2761
-		}
2762
-
2763
-		if (empty($recipients)) {
2764
-			$contactProperties = [];
2765
-			$contactProperties[PR_DISPLAY_NAME] = $to;
2766
-			$contactProperties[PR_USER_X509_CERTIFICATE] = false;
2767
-
2768
-			$recipients[] = $this->createResolveRecipient(SYNC_RESOLVERECIPIENTS_TYPE_CONTACT, $to, $contactProperties);
2769
-		}
2770
-
2771
-		return $recipients;
2772
-	}
2773
-
2774
-	/**
2775
-	 * Creates SyncResolveRecipientsCertificates object for ResolveRecipients.
2776
-	 *
2777
-	 * @param binary $certificates
2778
-	 * @param int    $recipientCount
2779
-	 *
2780
-	 * @return SyncResolveRecipientsCertificates
2781
-	 */
2782
-	private function getCertificates($certificates, $recipientCount = 0) {
2783
-		$cert = new SyncResolveRecipientsCertificates();
2784
-		if ($certificates === false) {
2785
-			$cert->status = SYNC_RESOLVERECIPSSTATUS_CERTIFICATES_NOVALIDCERT;
2786
-
2787
-			return $cert;
2788
-		}
2789
-		$cert->status = SYNC_RESOLVERECIPSSTATUS_SUCCESS;
2790
-		$cert->certificatecount = count($certificates);
2791
-		$cert->recipientcount = $recipientCount;
2792
-		$cert->certificate = [];
2793
-		foreach ($certificates as $certificate) {
2794
-			$cert->certificate[] = base64_encode($certificate);
2795
-		}
2796
-
2797
-		return $cert;
2798
-	}
2799
-
2800
-	/**
2801
-	 * Creates SyncResolveRecipient object for ResolveRecipientsResponse.
2802
-	 *
2803
-	 * @param int    $type
2804
-	 * @param string $email
2805
-	 * @param array  $recipientProperties
2806
-	 * @param int    $recipientCount
2807
-	 *
2808
-	 * @return SyncResolveRecipient
2809
-	 */
2810
-	private function createResolveRecipient($type, $email, $recipientProperties, $recipientCount = 0) {
2811
-		$recipient = new SyncResolveRecipient();
2812
-		$recipient->type = $type;
2813
-		$recipient->displayname = u2w($recipientProperties[PR_DISPLAY_NAME]);
2814
-		$recipient->emailaddress = $email;
2815
-
2816
-		if ($type == SYNC_RESOLVERECIPIENTS_TYPE_GAL) {
2817
-			$certificateProp = PR_EMS_AB_TAGGED_X509_CERT;
2818
-		}
2819
-		elseif ($type == SYNC_RESOLVERECIPIENTS_TYPE_CONTACT) {
2820
-			$certificateProp = PR_USER_X509_CERTIFICATE;
2821
-		}
2822
-		else {
2823
-			$certificateProp = null;
2824
-		}
2825
-
2826
-		if (isset($recipientProperties[$certificateProp]) && is_array($recipientProperties[$certificateProp]) && !empty($recipientProperties[$certificateProp])) {
2827
-			$certificates = $this->getCertificates($recipientProperties[$certificateProp], $recipientCount);
2828
-		}
2829
-		else {
2830
-			$certificates = $this->getCertificates(false);
2831
-			SLog::Write(LOGLEVEL_INFO, sprintf("Grommunio->createResolveRecipient(): No certificate found for '%s' (requested email address: '%s')", $recipientProperties[PR_DISPLAY_NAME], $email));
2832
-		}
2833
-		$recipient->certificates = $certificates;
2834
-
2835
-		if (isset($recipientProperties[PR_ENTRYID])) {
2836
-			$recipient->id = $recipientProperties[PR_ENTRYID];
2837
-		}
2838
-
2839
-		return $recipient;
2840
-	}
2841
-
2842
-	/**
2843
-	 * Gets the availability of a user for the given time window.
2844
-	 *
2845
-	 * @param string                       $to
2846
-	 * @param SyncResolveRecipient         $resolveRecipient
2847
-	 * @param SyncResolveRecipientsOptions $resolveRecipientsOptions
2848
-	 *
2849
-	 * @return SyncResolveRecipientsAvailability
2850
-	 */
2851
-	private function getAvailability($to, $resolveRecipient, $resolveRecipientsOptions) {
2852
-		$availability = new SyncResolveRecipientsAvailability();
2853
-		$availability->status = SYNC_RESOLVERECIPSSTATUS_AVAILABILITY_SUCCESS;
2854
-
2855
-		if (!isset($resolveRecipient->id)) {
2856
-			// TODO this shouldn't happen but try to get the recipient in such a case
2857
-		}
2858
-
2859
-		$start = strtotime($resolveRecipientsOptions->availability->starttime);
2860
-		$end = strtotime($resolveRecipientsOptions->availability->endtime);
2861
-		// Each digit in the MergedFreeBusy indicates the free/busy status for the user for every 30 minute interval.
2862
-		$timeslots = intval(ceil(($end - $start) / self::HALFHOURSECONDS));
2863
-
2864
-		if ($timeslots > self::MAXFREEBUSYSLOTS) {
2865
-			throw new StatusException("Grommunio->getAvailability(): the requested free busy range is too large.", SYNC_RESOLVERECIPSSTATUS_PROTOCOLERROR);
2866
-		}
2867
-
2868
-		$mergedFreeBusy = str_pad(fbNoData, $timeslots, fbNoData);
2869
-
2870
-		$retval = mapi_getuseravailability($this->session, $resolveRecipient->id, $start, $end);
2871
-		SLog::Write(LOGLEVEL_INFO, sprintf("Grommunio->getAvailability(): free busy '%s'", print_r($retval, 1)));
2872
-
2873
-		if (!empty($retval)) {
2874
-			$freebusy = json_decode($retval, true);
2875
-			// freebusy is available, assume that the user is free
2876
-			$mergedFreeBusy = str_pad(fbFree, $timeslots, fbFree);
2877
-			foreach ($freebusy['events'] as $event) {
2878
-				// calculate which timeslot of mergedFreeBusy should be replaced.
2879
-				$startSlot = intval(floor(($event['StartTime'] - $start) / self::HALFHOURSECONDS));
2880
-				$endSlot = intval(floor(($event['EndTime'] - $start) / self::HALFHOURSECONDS));
2881
-				// if event started at a multiple of half an hour from requested freebusy time and
2882
-				// its duration is also a multiple of half an hour
2883
-				// then it's necessary to reduce endSlot by one
2884
-				if ((($event['StartTime'] - $start) % self::HALFHOURSECONDS == 0) && (($event['EndTime'] - $event['StartTime']) % self::HALFHOURSECONDS == 0)) {
2885
-					--$endSlot;
2886
-				}
2887
-				$fbType = Utils::GetFbStatusFromType($event['BusyType']);
2888
-				for ($i = $startSlot; $i <= $endSlot && $i < $timeslots; ++$i) {
2889
-					// only set the new slot's free busy status if it's higher than the current one
2890
-					if ($fbType > $mergedFreeBusy[$i]) {
2891
-						$mergedFreeBusy[$i] = $fbType;
2892
-					}
2893
-				}
2894
-			}
2895
-		}
2896
-		$availability->mergedfreebusy = $mergedFreeBusy;
2897
-
2898
-		return $availability;
2899
-	}
2900
-
2901
-	/**
2902
-	 * Returns contacts matching given email address from a folder.
2903
-	 *
2904
-	 * @param MAPIStore $store
2905
-	 * @param binary    $folderEntryid
2906
-	 * @param string    $email
2907
-	 *
2908
-	 * @return array|bool
2909
-	 */
2910
-	private function getContactsFromFolder($store, $folderEntryid, $email) {
2911
-		$folder = mapi_msgstore_openentry($store, $folderEntryid);
2912
-		$folderContent = mapi_folder_getcontentstable($folder);
2913
-		mapi_table_restrict($folderContent, MAPIUtils::GetEmailAddressRestriction($store, $email));
2914
-		// TODO max limit
2915
-		if (mapi_table_getrowcount($folderContent) > 0) {
2916
-			return mapi_table_queryallrows($folderContent, [PR_DISPLAY_NAME, PR_USER_X509_CERTIFICATE, PR_ENTRYID]);
2917
-		}
2918
-
2919
-		return false;
2920
-	}
2921
-
2922
-	/**
2923
-	 * Get MAPI addressbook object.
2924
-	 *
2925
-	 * @return MAPIAddressbook object to be used with mapi_ab_* or false on failure
2926
-	 */
2927
-	private function getAddressbook() {
2928
-		if (isset($this->addressbook) && $this->addressbook) {
2929
-			return $this->addressbook;
2930
-		}
2931
-		$this->addressbook = mapi_openaddressbook($this->session);
2932
-		$result = mapi_last_hresult();
2933
-		if ($result && $this->addressbook === false) {
2934
-			SLog::Write(LOGLEVEL_ERROR, sprintf("Grommunio->getAddressbook error opening addressbook 0x%X", $result));
2935
-
2936
-			return false;
2937
-		}
2938
-
2939
-		return $this->addressbook;
2940
-	}
2941
-
2942
-	/**
2943
-	 * Checks if the user is not disabled for grommunio-sync.
2944
-	 *
2945
-	 * @throws FatalException if user is disabled for grommunio-sync
2946
-	 *
2947
-	 * @return bool
2948
-	 */
2949
-	private function isGSyncEnabled() {
2950
-		$addressbook = $this->getAddressbook();
2951
-		// this check needs to be performed on the store of the main (authenticated) user
2952
-		$store = $this->storeCache[$this->mainUser];
2953
-		$userEntryid = mapi_getprops($store, [PR_MAILBOX_OWNER_ENTRYID]);
2954
-		$mailuser = mapi_ab_openentry($addressbook, $userEntryid[PR_MAILBOX_OWNER_ENTRYID]);
2955
-		$enabledFeatures = mapi_getprops($mailuser, [PR_EC_DISABLED_FEATURES]);
2956
-		if (isset($enabledFeatures[PR_EC_DISABLED_FEATURES]) && is_array($enabledFeatures[PR_EC_DISABLED_FEATURES])) {
2957
-			$mobileDisabled = in_array(self::MOBILE_ENABLED, $enabledFeatures[PR_EC_DISABLED_FEATURES]);
2958
-			$deviceId = Request::GetDeviceID();
2959
-			// Checks for deviceId present in zarafaDisabledFeatures LDAP array attribute. Check is performed case insensitive.
2960
-			$deviceIdDisabled = (($deviceId !== null) && in_array($deviceId, array_map('strtolower', $enabledFeatures[PR_EC_DISABLED_FEATURES]))) ? true : false;
2961
-			if ($mobileDisabled) {
2962
-				throw new FatalException("User is disabled for grommunio-sync.");
2963
-			}
2964
-			if ($deviceIdDisabled) {
2965
-				throw new FatalException(sprintf("User has deviceId %s disabled for usage with grommunio-sync.", $deviceId));
2966
-			}
2967
-		}
2968
-
2969
-		return true;
2970
-	}
2387
+        $rmTemplates->Status = SYNC_COMMONSTATUS_IRMFEATUREDISABLED;
2388
+        $rmTemplates->rmtemplates = [];
2389
+    }
2390
+
2391
+    /**
2392
+     * Sets the importance and priority of a message from a RFC822 message headers.
2393
+     *
2394
+     * @param int   $xPriority
2395
+     * @param array $mapiprops
2396
+     * @param mixed $sendMailProps
2397
+     */
2398
+    private function getImportanceAndPriority($xPriority, &$mapiprops, $sendMailProps) {
2399
+        switch ($xPriority) {
2400
+            case 1:
2401
+            case 2:
2402
+                $priority = PRIO_URGENT;
2403
+                $importance = IMPORTANCE_HIGH;
2404
+                break;
2405
+
2406
+            case 4:
2407
+            case 5:
2408
+                $priority = PRIO_NONURGENT;
2409
+                $importance = IMPORTANCE_LOW;
2410
+                break;
2411
+
2412
+            case 3:
2413
+            default:
2414
+                $priority = PRIO_NORMAL;
2415
+                $importance = IMPORTANCE_NORMAL;
2416
+                break;
2417
+        }
2418
+        $mapiprops[$sendMailProps["importance"]] = $importance;
2419
+        $mapiprops[$sendMailProps["priority"]] = $priority;
2420
+    }
2421
+
2422
+    /**
2423
+     * Copies attachments from one message to another.
2424
+     *
2425
+     * @param MAPIMessage $toMessage
2426
+     * @param MAPIMessage $fromMessage
2427
+     */
2428
+    private function copyAttachments(&$toMessage, $fromMessage) {
2429
+        $attachtable = mapi_message_getattachmenttable($fromMessage);
2430
+        $rows = mapi_table_queryallrows($attachtable, [PR_ATTACH_NUM]);
2431
+
2432
+        foreach ($rows as $row) {
2433
+            if (isset($row[PR_ATTACH_NUM])) {
2434
+                $attach = mapi_message_openattach($fromMessage, $row[PR_ATTACH_NUM]);
2435
+                $newattach = mapi_message_createattach($toMessage);
2436
+                mapi_copyto($attach, [], [], $newattach, 0);
2437
+                mapi_savechanges($newattach);
2438
+            }
2439
+        }
2440
+    }
2441
+
2442
+    /**
2443
+     * Function will create a search folder in FINDER_ROOT folder
2444
+     * if folder exists then it will open it.
2445
+     *
2446
+     * @see createSearchFolder($store, $openIfExists = true) function in the webaccess
2447
+     *
2448
+     * @return mapiFolderObject $folder created search folder
2449
+     */
2450
+    private function getSearchFolder() {
2451
+        // create new or open existing search folder
2452
+        $searchFolderRoot = $this->getSearchFoldersRoot($this->store);
2453
+        if ($searchFolderRoot === false) {
2454
+            // error in finding search root folder
2455
+            // or store doesn't support search folders
2456
+            return false;
2457
+        }
2458
+
2459
+        $searchFolder = $this->createSearchFolder($searchFolderRoot);
2460
+
2461
+        if ($searchFolder !== false && mapi_last_hresult() == NOERROR) {
2462
+            return $searchFolder;
2463
+        }
2464
+
2465
+        return false;
2466
+    }
2467
+
2468
+    /**
2469
+     * Function will open FINDER_ROOT folder in root container
2470
+     * public folder's don't have FINDER_ROOT folder.
2471
+     *
2472
+     * @see getSearchFoldersRoot($store) function in the webaccess
2473
+     *
2474
+     * @return mapiFolderObject root folder for search folders
2475
+     */
2476
+    private function getSearchFoldersRoot() {
2477
+        // check if we can create search folders
2478
+        $storeProps = mapi_getprops($this->store, [PR_STORE_SUPPORT_MASK, PR_FINDER_ENTRYID]);
2479
+        if (($storeProps[PR_STORE_SUPPORT_MASK] & STORE_SEARCH_OK) != STORE_SEARCH_OK) {
2480
+            SLog::Write(LOGLEVEL_WARN, "Grommunio->getSearchFoldersRoot(): Store doesn't support search folders. Public store doesn't have FINDER_ROOT folder");
2481
+
2482
+            return false;
2483
+        }
2484
+
2485
+        // open search folders root
2486
+        $searchRootFolder = mapi_msgstore_openentry($this->store, $storeProps[PR_FINDER_ENTRYID]);
2487
+        if (mapi_last_hresult() != NOERROR) {
2488
+            SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->getSearchFoldersRoot(): Unable to open search folder (0x%X)", mapi_last_hresult()));
2489
+
2490
+            return false;
2491
+        }
2492
+
2493
+        return $searchRootFolder;
2494
+    }
2495
+
2496
+    /**
2497
+     * Creates a search folder if it not exists or opens an existing one
2498
+     * and returns it.
2499
+     *
2500
+     * @param mapiFolderObject $searchFolderRoot
2501
+     *
2502
+     * @return mapiFolderObject
2503
+     */
2504
+    private function createSearchFolder($searchFolderRoot) {
2505
+        $folderName = "grommunio-sync Search Folder " . @getmypid();
2506
+        $searchFolders = mapi_folder_gethierarchytable($searchFolderRoot);
2507
+        $restriction = [
2508
+            RES_CONTENT,
2509
+            [
2510
+                FUZZYLEVEL => FL_PREFIX,
2511
+                ULPROPTAG => PR_DISPLAY_NAME,
2512
+                VALUE => [PR_DISPLAY_NAME => $folderName],
2513
+            ],
2514
+        ];
2515
+        // restrict the hierarchy to the grommunio-sync search folder only
2516
+        mapi_table_restrict($searchFolders, $restriction);
2517
+        if (mapi_table_getrowcount($searchFolders)) {
2518
+            $searchFolder = mapi_table_queryrows($searchFolders, [PR_ENTRYID], 0, 1);
2519
+
2520
+            return mapi_msgstore_openentry($this->store, $searchFolder[0][PR_ENTRYID]);
2521
+        }
2522
+
2523
+        return mapi_folder_createfolder($searchFolderRoot, $folderName, null, 0, FOLDER_SEARCH);
2524
+    }
2525
+
2526
+    /**
2527
+     * Creates a search restriction.
2528
+     *
2529
+     * @param ContentParameter $cpo
2530
+     *
2531
+     * @return array
2532
+     */
2533
+    private function getSearchRestriction($cpo) {
2534
+        $searchText = $cpo->GetSearchFreeText();
2535
+
2536
+        $searchGreater = strtotime($cpo->GetSearchValueGreater());
2537
+        $searchLess = strtotime($cpo->GetSearchValueLess());
2538
+
2539
+        if (version_compare(phpversion(), '5.3.4') < 0) {
2540
+            SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->getSearchRestriction(): Your system's PHP version (%s) might not correctly process unicode strings. Search containing such characters might not return correct results. It is recommended to update to at least PHP 5.3.4. See ZP-541 for more information.", phpversion()));
2541
+        }
2542
+        // split the search on whitespache and look for every word
2543
+        $searchText = preg_split("/\\W+/u", $searchText);
2544
+        $searchProps = [PR_BODY, PR_SUBJECT, PR_DISPLAY_TO, PR_DISPLAY_CC, PR_SENDER_NAME, PR_SENDER_EMAIL_ADDRESS, PR_SENT_REPRESENTING_NAME, PR_SENT_REPRESENTING_EMAIL_ADDRESS];
2545
+        $resAnd = [];
2546
+        foreach ($searchText as $term) {
2547
+            $resOr = [];
2548
+
2549
+            foreach ($searchProps as $property) {
2550
+                array_push(
2551
+                    $resOr,
2552
+                    [RES_CONTENT,
2553
+                        [
2554
+                            FUZZYLEVEL => FL_SUBSTRING | FL_IGNORECASE,
2555
+                            ULPROPTAG => $property,
2556
+                            VALUE => u2w($term),
2557
+                        ],
2558
+                    ]
2559
+                );
2560
+            }
2561
+            array_push($resAnd, [RES_OR, $resOr]);
2562
+        }
2563
+
2564
+        // add time range restrictions
2565
+        if ($searchGreater) {
2566
+            array_push($resAnd, [RES_PROPERTY, [RELOP => RELOP_GE, ULPROPTAG => PR_MESSAGE_DELIVERY_TIME, VALUE => [PR_MESSAGE_DELIVERY_TIME => $searchGreater]]]); // RES_AND;
2567
+        }
2568
+        if ($searchLess) {
2569
+            array_push($resAnd, [RES_PROPERTY, [RELOP => RELOP_LE, ULPROPTAG => PR_MESSAGE_DELIVERY_TIME, VALUE => [PR_MESSAGE_DELIVERY_TIME => $searchLess]]]);
2570
+        }
2571
+
2572
+        return [RES_AND, $resAnd];
2573
+    }
2574
+
2575
+    /**
2576
+     * Resolve recipient based on his email address.
2577
+     *
2578
+     * @param string $to
2579
+     * @param int    $maxAmbiguousRecipients
2580
+     * @param bool   $expandDistlist
2581
+     *
2582
+     * @return bool|SyncResolveRecipient
2583
+     */
2584
+    private function resolveRecipient($to, $maxAmbiguousRecipients, $expandDistlist = true) {
2585
+        $recipient = $this->resolveRecipientGAL($to, $maxAmbiguousRecipients, $expandDistlist);
2586
+
2587
+        if ($recipient !== false) {
2588
+            return $recipient;
2589
+        }
2590
+
2591
+        $recipient = $this->resolveRecipientContact($to, $maxAmbiguousRecipients);
2592
+
2593
+        if ($recipient !== false) {
2594
+            return $recipient;
2595
+        }
2596
+
2597
+        return false;
2598
+    }
2599
+
2600
+    /**
2601
+     * Resolves recipient from the GAL and gets his certificates.
2602
+     *
2603
+     * @param string $to
2604
+     * @param int    $maxAmbiguousRecipients
2605
+     * @param bool   $expandDistlist
2606
+     *
2607
+     * @return array|bool
2608
+     */
2609
+    private function resolveRecipientGAL($to, $maxAmbiguousRecipients, $expandDistlist = true) {
2610
+        SLog::Write(LOGLEVEL_WBXML, sprintf("Grommunio->resolveRecipientGAL(): Resolving recipient '%s' in GAL", $to));
2611
+        $addrbook = $this->getAddressbook();
2612
+        // FIXME: create a function to get the adressbook contentstable
2613
+        $ab_entryid = mapi_ab_getdefaultdir($addrbook);
2614
+        if ($ab_entryid) {
2615
+            $ab_dir = mapi_ab_openentry($addrbook, $ab_entryid);
2616
+        }
2617
+        if ($ab_dir) {
2618
+            $table = mapi_folder_getcontentstable($ab_dir);
2619
+        }
2620
+
2621
+        if (!$table) {
2622
+            SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->resolveRecipientGAL(): Unable to open addressbook:0x%X", mapi_last_hresult()));
2623
+
2624
+            return false;
2625
+        }
2626
+
2627
+        $restriction = MAPIUtils::GetSearchRestriction(u2w($to));
2628
+        mapi_table_restrict($table, $restriction);
2629
+
2630
+        $querycnt = mapi_table_getrowcount($table);
2631
+        if ($querycnt > 0) {
2632
+            $recipientGal = [];
2633
+            $rowsToQuery = $maxAmbiguousRecipients;
2634
+            // some devices request 0 ambiguous recipients
2635
+            if ($querycnt == 1 && $maxAmbiguousRecipients == 0) {
2636
+                $rowsToQuery = 1;
2637
+            }
2638
+            elseif ($querycnt > 1 && $maxAmbiguousRecipients == 0) {
2639
+                SLog::Write(LOGLEVEL_INFO, sprintf("Grommunio->resolveRecipientGAL(): GAL search found %d recipients but the device hasn't requested ambiguous recipients", $querycnt));
2640
+
2641
+                return $recipientGal;
2642
+            }
2643
+            elseif ($querycnt > 1 && $maxAmbiguousRecipients == 1) {
2644
+                $rowsToQuery = $querycnt;
2645
+            }
2646
+            // get the certificate every time because caching the certificate is less expensive than opening addressbook entry again
2647
+            $abentries = mapi_table_queryrows($table, [PR_ENTRYID, PR_DISPLAY_NAME, PR_EMS_AB_TAGGED_X509_CERT, PR_OBJECT_TYPE, PR_SMTP_ADDRESS], 0, $rowsToQuery);
2648
+            for ($i = 0, $nrEntries = count($abentries); $i < $nrEntries; ++$i) {
2649
+                if (strcasecmp($abentries[$i][PR_SMTP_ADDRESS], $to) !== 0 && $maxAmbiguousRecipients == 1) {
2650
+                    SLog::Write(LOGLEVEL_INFO, sprintf("Grommunio->resolveRecipientGAL(): maxAmbiguousRecipients is 1 and found non-matching user (to '%s' found: '%s')", $to, $abentries[$i][PR_SMTP_ADDRESS]));
2651
+
2652
+                    continue;
2653
+                }
2654
+                if ($abentries[$i][PR_OBJECT_TYPE] == MAPI_DISTLIST) {
2655
+                    // check whether to expand dist list
2656
+                    if ($expandDistlist) {
2657
+                        SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->resolveRecipientGAL(): '%s' is a dist list. Expand it to members.", $to));
2658
+                        $distList = mapi_ab_openentry($addrbook, $abentries[$i][PR_ENTRYID]);
2659
+                        $distListContent = mapi_folder_getcontentstable($distList);
2660
+                        $distListMembers = mapi_table_queryallrows($distListContent, [PR_ENTRYID, PR_DISPLAY_NAME, PR_EMS_AB_TAGGED_X509_CERT]);
2661
+                        for ($j = 0, $nrDistListMembers = mapi_table_getrowcount($distListContent); $j < $nrDistListMembers; ++$j) {
2662
+                            SLog::Write(LOGLEVEL_WBXML, sprintf("Grommunio->resolveRecipientGAL(): distlist's '%s' member: '%s'", $to, $distListMembers[$j][PR_DISPLAY_NAME]));
2663
+                            $recipientGal[] = $this->createResolveRecipient(SYNC_RESOLVERECIPIENTS_TYPE_GAL, $to, $distListMembers[$j], $nrDistListMembers);
2664
+                        }
2665
+                    }
2666
+                    else {
2667
+                        SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->resolveRecipientGAL(): '%s' is a dist list, but return it as is.", $to));
2668
+                        $recipientGal[] = $this->createResolveRecipient(SYNC_RESOLVERECIPIENTS_TYPE_GAL, $abentries[$i][PR_SMTP_ADDRESS], $abentries[$i]);
2669
+                    }
2670
+                }
2671
+                elseif ($abentries[$i][PR_OBJECT_TYPE] == MAPI_MAILUSER) {
2672
+                    $recipientGal[] = $this->createResolveRecipient(SYNC_RESOLVERECIPIENTS_TYPE_GAL, $abentries[$i][PR_SMTP_ADDRESS], $abentries[$i]);
2673
+                }
2674
+            }
2675
+
2676
+            SLog::Write(LOGLEVEL_WBXML, "Grommunio->resolveRecipientGAL(): Found a recipient in GAL");
2677
+
2678
+            return $recipientGal;
2679
+        }
2680
+
2681
+        SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->resolveRecipientGAL(): No recipient found for: '%s' in GAL", $to));
2682
+
2683
+        return SYNC_RESOLVERECIPSSTATUS_RESPONSE_UNRESOLVEDRECIP;
2684
+
2685
+        return false;
2686
+    }
2687
+
2688
+    /**
2689
+     * Resolves recipient from the contact list and gets his certificates.
2690
+     *
2691
+     * @param string $to
2692
+     * @param int    $maxAmbiguousRecipients
2693
+     *
2694
+     * @return array|bool
2695
+     */
2696
+    private function resolveRecipientContact($to, $maxAmbiguousRecipients) {
2697
+        SLog::Write(LOGLEVEL_WBXML, sprintf("Grommunio->resolveRecipientContact(): Resolving recipient '%s' in user's contacts", $to));
2698
+        // go through all contact folders of the user and
2699
+        // check if there's a contact with the given email address
2700
+        $root = mapi_msgstore_openentry($this->defaultstore);
2701
+        if (!$root) {
2702
+            SLog::Write(LOGLEVEL_ERROR, sprintf("Grommunio->resolveRecipientContact(): Unable to open default store: 0x%X", mapi_last_hresult()));
2703
+        }
2704
+        $rootprops = mapi_getprops($root, [PR_IPM_CONTACT_ENTRYID]);
2705
+        $contacts = $this->getContactsFromFolder($this->defaultstore, $rootprops[PR_IPM_CONTACT_ENTRYID], $to);
2706
+        $recipients = [];
2707
+
2708
+        if ($contacts !== false) {
2709
+            SLog::Write(LOGLEVEL_WBXML, sprintf("Grommunio->resolveRecipientContact(): Found %d contacts in main contacts folder.", count($contacts)));
2710
+            // create resolve recipient object
2711
+            foreach ($contacts as $contact) {
2712
+                $recipients[] = $this->createResolveRecipient(SYNC_RESOLVERECIPIENTS_TYPE_CONTACT, $to, $contact);
2713
+            }
2714
+        }
2715
+
2716
+        $contactfolder = mapi_msgstore_openentry($this->defaultstore, $rootprops[PR_IPM_CONTACT_ENTRYID]);
2717
+        $subfolders = MAPIUtils::GetSubfoldersForType($contactfolder, "IPF.Contact");
2718
+        if ($subfolders !== false) {
2719
+            foreach ($subfolders as $folder) {
2720
+                $contacts = $this->getContactsFromFolder($this->defaultstore, $folder[PR_ENTRYID], $to);
2721
+                if ($contacts !== false) {
2722
+                    SLog::Write(LOGLEVEL_WBXML, sprintf("Grommunio->resolveRecipientContact(): Found %d contacts in contacts' subfolder.", count($contacts)));
2723
+                    foreach ($contacts as $contact) {
2724
+                        $recipients[] = $this->createResolveRecipient(SYNC_RESOLVERECIPIENTS_TYPE_CONTACT, $to, $contact);
2725
+                    }
2726
+                }
2727
+            }
2728
+        }
2729
+
2730
+        // search contacts in public folders
2731
+        $storestables = mapi_getmsgstorestable($this->session);
2732
+        $result = mapi_last_hresult();
2733
+
2734
+        if ($result == NOERROR) {
2735
+            $rows = mapi_table_queryallrows($storestables, [PR_ENTRYID, PR_DEFAULT_STORE, PR_MDB_PROVIDER]);
2736
+            foreach ($rows as $row) {
2737
+                if (isset($row[PR_MDB_PROVIDER]) && $row[PR_MDB_PROVIDER] == ZARAFA_STORE_PUBLIC_GUID) {
2738
+                    // TODO refactor public store
2739
+                    $publicstore = mapi_openmsgstore($this->session, $row[PR_ENTRYID]);
2740
+                    $publicfolder = mapi_msgstore_openentry($publicstore);
2741
+
2742
+                    $subfolders = MAPIUtils::GetSubfoldersForType($publicfolder, "IPF.Contact");
2743
+                    if ($subfolders !== false) {
2744
+                        foreach ($subfolders as $folder) {
2745
+                            $contacts = $this->getContactsFromFolder($publicstore, $folder[PR_ENTRYID], $to);
2746
+                            if ($contacts !== false) {
2747
+                                SLog::Write(LOGLEVEL_WBXML, sprintf("Grommunio->resolveRecipientContact(): Found %d contacts in public contacts folder.", count($contacts)));
2748
+                                foreach ($contacts as $contact) {
2749
+                                    $recipients[] = $this->createResolveRecipient(SYNC_RESOLVERECIPIENTS_TYPE_CONTACT, $to, $contact);
2750
+                                }
2751
+                            }
2752
+                        }
2753
+                    }
2754
+
2755
+                    break;
2756
+                }
2757
+            }
2758
+        }
2759
+        else {
2760
+            SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->resolveRecipientContact(): Unable to open public store: 0x%X", $result));
2761
+        }
2762
+
2763
+        if (empty($recipients)) {
2764
+            $contactProperties = [];
2765
+            $contactProperties[PR_DISPLAY_NAME] = $to;
2766
+            $contactProperties[PR_USER_X509_CERTIFICATE] = false;
2767
+
2768
+            $recipients[] = $this->createResolveRecipient(SYNC_RESOLVERECIPIENTS_TYPE_CONTACT, $to, $contactProperties);
2769
+        }
2770
+
2771
+        return $recipients;
2772
+    }
2773
+
2774
+    /**
2775
+     * Creates SyncResolveRecipientsCertificates object for ResolveRecipients.
2776
+     *
2777
+     * @param binary $certificates
2778
+     * @param int    $recipientCount
2779
+     *
2780
+     * @return SyncResolveRecipientsCertificates
2781
+     */
2782
+    private function getCertificates($certificates, $recipientCount = 0) {
2783
+        $cert = new SyncResolveRecipientsCertificates();
2784
+        if ($certificates === false) {
2785
+            $cert->status = SYNC_RESOLVERECIPSSTATUS_CERTIFICATES_NOVALIDCERT;
2786
+
2787
+            return $cert;
2788
+        }
2789
+        $cert->status = SYNC_RESOLVERECIPSSTATUS_SUCCESS;
2790
+        $cert->certificatecount = count($certificates);
2791
+        $cert->recipientcount = $recipientCount;
2792
+        $cert->certificate = [];
2793
+        foreach ($certificates as $certificate) {
2794
+            $cert->certificate[] = base64_encode($certificate);
2795
+        }
2796
+
2797
+        return $cert;
2798
+    }
2799
+
2800
+    /**
2801
+     * Creates SyncResolveRecipient object for ResolveRecipientsResponse.
2802
+     *
2803
+     * @param int    $type
2804
+     * @param string $email
2805
+     * @param array  $recipientProperties
2806
+     * @param int    $recipientCount
2807
+     *
2808
+     * @return SyncResolveRecipient
2809
+     */
2810
+    private function createResolveRecipient($type, $email, $recipientProperties, $recipientCount = 0) {
2811
+        $recipient = new SyncResolveRecipient();
2812
+        $recipient->type = $type;
2813
+        $recipient->displayname = u2w($recipientProperties[PR_DISPLAY_NAME]);
2814
+        $recipient->emailaddress = $email;
2815
+
2816
+        if ($type == SYNC_RESOLVERECIPIENTS_TYPE_GAL) {
2817
+            $certificateProp = PR_EMS_AB_TAGGED_X509_CERT;
2818
+        }
2819
+        elseif ($type == SYNC_RESOLVERECIPIENTS_TYPE_CONTACT) {
2820
+            $certificateProp = PR_USER_X509_CERTIFICATE;
2821
+        }
2822
+        else {
2823
+            $certificateProp = null;
2824
+        }
2825
+
2826
+        if (isset($recipientProperties[$certificateProp]) && is_array($recipientProperties[$certificateProp]) && !empty($recipientProperties[$certificateProp])) {
2827
+            $certificates = $this->getCertificates($recipientProperties[$certificateProp], $recipientCount);
2828
+        }
2829
+        else {
2830
+            $certificates = $this->getCertificates(false);
2831
+            SLog::Write(LOGLEVEL_INFO, sprintf("Grommunio->createResolveRecipient(): No certificate found for '%s' (requested email address: '%s')", $recipientProperties[PR_DISPLAY_NAME], $email));
2832
+        }
2833
+        $recipient->certificates = $certificates;
2834
+
2835
+        if (isset($recipientProperties[PR_ENTRYID])) {
2836
+            $recipient->id = $recipientProperties[PR_ENTRYID];
2837
+        }
2838
+
2839
+        return $recipient;
2840
+    }
2841
+
2842
+    /**
2843
+     * Gets the availability of a user for the given time window.
2844
+     *
2845
+     * @param string                       $to
2846
+     * @param SyncResolveRecipient         $resolveRecipient
2847
+     * @param SyncResolveRecipientsOptions $resolveRecipientsOptions
2848
+     *
2849
+     * @return SyncResolveRecipientsAvailability
2850
+     */
2851
+    private function getAvailability($to, $resolveRecipient, $resolveRecipientsOptions) {
2852
+        $availability = new SyncResolveRecipientsAvailability();
2853
+        $availability->status = SYNC_RESOLVERECIPSSTATUS_AVAILABILITY_SUCCESS;
2854
+
2855
+        if (!isset($resolveRecipient->id)) {
2856
+            // TODO this shouldn't happen but try to get the recipient in such a case
2857
+        }
2858
+
2859
+        $start = strtotime($resolveRecipientsOptions->availability->starttime);
2860
+        $end = strtotime($resolveRecipientsOptions->availability->endtime);
2861
+        // Each digit in the MergedFreeBusy indicates the free/busy status for the user for every 30 minute interval.
2862
+        $timeslots = intval(ceil(($end - $start) / self::HALFHOURSECONDS));
2863
+
2864
+        if ($timeslots > self::MAXFREEBUSYSLOTS) {
2865
+            throw new StatusException("Grommunio->getAvailability(): the requested free busy range is too large.", SYNC_RESOLVERECIPSSTATUS_PROTOCOLERROR);
2866
+        }
2867
+
2868
+        $mergedFreeBusy = str_pad(fbNoData, $timeslots, fbNoData);
2869
+
2870
+        $retval = mapi_getuseravailability($this->session, $resolveRecipient->id, $start, $end);
2871
+        SLog::Write(LOGLEVEL_INFO, sprintf("Grommunio->getAvailability(): free busy '%s'", print_r($retval, 1)));
2872
+
2873
+        if (!empty($retval)) {
2874
+            $freebusy = json_decode($retval, true);
2875
+            // freebusy is available, assume that the user is free
2876
+            $mergedFreeBusy = str_pad(fbFree, $timeslots, fbFree);
2877
+            foreach ($freebusy['events'] as $event) {
2878
+                // calculate which timeslot of mergedFreeBusy should be replaced.
2879
+                $startSlot = intval(floor(($event['StartTime'] - $start) / self::HALFHOURSECONDS));
2880
+                $endSlot = intval(floor(($event['EndTime'] - $start) / self::HALFHOURSECONDS));
2881
+                // if event started at a multiple of half an hour from requested freebusy time and
2882
+                // its duration is also a multiple of half an hour
2883
+                // then it's necessary to reduce endSlot by one
2884
+                if ((($event['StartTime'] - $start) % self::HALFHOURSECONDS == 0) && (($event['EndTime'] - $event['StartTime']) % self::HALFHOURSECONDS == 0)) {
2885
+                    --$endSlot;
2886
+                }
2887
+                $fbType = Utils::GetFbStatusFromType($event['BusyType']);
2888
+                for ($i = $startSlot; $i <= $endSlot && $i < $timeslots; ++$i) {
2889
+                    // only set the new slot's free busy status if it's higher than the current one
2890
+                    if ($fbType > $mergedFreeBusy[$i]) {
2891
+                        $mergedFreeBusy[$i] = $fbType;
2892
+                    }
2893
+                }
2894
+            }
2895
+        }
2896
+        $availability->mergedfreebusy = $mergedFreeBusy;
2897
+
2898
+        return $availability;
2899
+    }
2900
+
2901
+    /**
2902
+     * Returns contacts matching given email address from a folder.
2903
+     *
2904
+     * @param MAPIStore $store
2905
+     * @param binary    $folderEntryid
2906
+     * @param string    $email
2907
+     *
2908
+     * @return array|bool
2909
+     */
2910
+    private function getContactsFromFolder($store, $folderEntryid, $email) {
2911
+        $folder = mapi_msgstore_openentry($store, $folderEntryid);
2912
+        $folderContent = mapi_folder_getcontentstable($folder);
2913
+        mapi_table_restrict($folderContent, MAPIUtils::GetEmailAddressRestriction($store, $email));
2914
+        // TODO max limit
2915
+        if (mapi_table_getrowcount($folderContent) > 0) {
2916
+            return mapi_table_queryallrows($folderContent, [PR_DISPLAY_NAME, PR_USER_X509_CERTIFICATE, PR_ENTRYID]);
2917
+        }
2918
+
2919
+        return false;
2920
+    }
2921
+
2922
+    /**
2923
+     * Get MAPI addressbook object.
2924
+     *
2925
+     * @return MAPIAddressbook object to be used with mapi_ab_* or false on failure
2926
+     */
2927
+    private function getAddressbook() {
2928
+        if (isset($this->addressbook) && $this->addressbook) {
2929
+            return $this->addressbook;
2930
+        }
2931
+        $this->addressbook = mapi_openaddressbook($this->session);
2932
+        $result = mapi_last_hresult();
2933
+        if ($result && $this->addressbook === false) {
2934
+            SLog::Write(LOGLEVEL_ERROR, sprintf("Grommunio->getAddressbook error opening addressbook 0x%X", $result));
2935
+
2936
+            return false;
2937
+        }
2938
+
2939
+        return $this->addressbook;
2940
+    }
2941
+
2942
+    /**
2943
+     * Checks if the user is not disabled for grommunio-sync.
2944
+     *
2945
+     * @throws FatalException if user is disabled for grommunio-sync
2946
+     *
2947
+     * @return bool
2948
+     */
2949
+    private function isGSyncEnabled() {
2950
+        $addressbook = $this->getAddressbook();
2951
+        // this check needs to be performed on the store of the main (authenticated) user
2952
+        $store = $this->storeCache[$this->mainUser];
2953
+        $userEntryid = mapi_getprops($store, [PR_MAILBOX_OWNER_ENTRYID]);
2954
+        $mailuser = mapi_ab_openentry($addressbook, $userEntryid[PR_MAILBOX_OWNER_ENTRYID]);
2955
+        $enabledFeatures = mapi_getprops($mailuser, [PR_EC_DISABLED_FEATURES]);
2956
+        if (isset($enabledFeatures[PR_EC_DISABLED_FEATURES]) && is_array($enabledFeatures[PR_EC_DISABLED_FEATURES])) {
2957
+            $mobileDisabled = in_array(self::MOBILE_ENABLED, $enabledFeatures[PR_EC_DISABLED_FEATURES]);
2958
+            $deviceId = Request::GetDeviceID();
2959
+            // Checks for deviceId present in zarafaDisabledFeatures LDAP array attribute. Check is performed case insensitive.
2960
+            $deviceIdDisabled = (($deviceId !== null) && in_array($deviceId, array_map('strtolower', $enabledFeatures[PR_EC_DISABLED_FEATURES]))) ? true : false;
2961
+            if ($mobileDisabled) {
2962
+                throw new FatalException("User is disabled for grommunio-sync.");
2963
+            }
2964
+            if ($deviceIdDisabled) {
2965
+                throw new FatalException(sprintf("User has deviceId %s disabled for usage with grommunio-sync.", $deviceId));
2966
+            }
2967
+        }
2968
+
2969
+        return true;
2970
+    }
2971 2971
 }
Please login to merge, or discard this patch.
Spacing   +18 added lines, -19 removed lines patch added patch discarded remove patch
@@ -142,7 +142,7 @@  discard block
 block discarded – undo
142 142
 			if (function_exists('mapi_feature') && mapi_feature('LOGONFLAGS')) {
143 143
 				// send grommunio-sync version and user agent to ZCP - ZP-589
144 144
 				if (Utils::CheckMapiExtVersion('7.2.0')) {
145
-					$gsync_version = 'Grommunio-Sync_' . @constant('GROMMUNIOSYNC_VERSION');
145
+					$gsync_version = 'Grommunio-Sync_'.@constant('GROMMUNIOSYNC_VERSION');
146 146
 					$user_agent = ($deviceId) ? GSync::GetDeviceManager()->GetUserAgent() : "unknown";
147 147
 					$this->session = @mapi_logon_zarafa($this->mainUser, $pass, MAPI_SERVER, null, null, 0, $gsync_version, $user_agent);
148 148
 				}
@@ -195,7 +195,7 @@  discard block
 block discarded – undo
195 195
 		$this->store = $this->defaultstore;
196 196
 		$this->storeName = $defaultUser;
197 197
 
198
-		SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->Logon(): User '%s' is authenticated%s", $this->mainUser, ($this->impersonateUser ? " impersonating '" . $this->impersonateUser . "'" : '')));
198
+		SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->Logon(): User '%s' is authenticated%s", $this->mainUser, ($this->impersonateUser ? " impersonating '".$this->impersonateUser."'" : '')));
199 199
 
200 200
 		$this->isGSyncEnabled();
201 201
 
@@ -589,7 +589,7 @@  discard block
 block discarded – undo
589 589
 							$fwbody = w2u($fwbody);
590 590
 						}
591 591
 
592
-						$mapiprops[$sendMailProps["body"]] = $body . "\r\n\r\n" . $fwbody;
592
+						$mapiprops[$sendMailProps["body"]] = $body."\r\n\r\n".$fwbody;
593 593
 					}
594 594
 
595 595
 					if (strlen($bodyHtml) > 0) {
@@ -605,7 +605,7 @@  discard block
 block discarded – undo
605 605
 							$fwbodyHtml = w2u($fwbodyHtml);
606 606
 						}
607 607
 
608
-						$mapiprops[$sendMailProps["html"]] = $bodyHtml . "<br><br>" . $fwbodyHtml;
608
+						$mapiprops[$sendMailProps["html"]] = $bodyHtml."<br><br>".$fwbodyHtml;
609 609
 					}
610 610
 				}
611 611
 			}
@@ -969,11 +969,11 @@  discard block
 block discarded – undo
969 969
 		if ($calFolderId) {
970 970
 			$shortFolderId = GSync::GetDeviceManager()->GetFolderIdForBackendId($calFolderId);
971 971
 			if ($calFolderId != $shortFolderId) {
972
-				$prefix = $shortFolderId . ':';
972
+				$prefix = $shortFolderId.':';
973 973
 			}
974 974
 		}
975 975
 
976
-		return $prefix . $calendarid;
976
+		return $prefix.$calendarid;
977 977
 	}
978 978
 
979 979
 	/**
@@ -1321,7 +1321,7 @@  discard block
 block discarded – undo
1321 1321
 			}
1322 1322
 		}
1323 1323
 		$nrResults = count($items);
1324
-		$items['range'] = ($nrResults > 0) ? $rangestart . '-' . ($nrResults - 1) : '0-0';
1324
+		$items['range'] = ($nrResults > 0) ? $rangestart.'-'.($nrResults - 1) : '0-0';
1325 1325
 		$items['searchtotal'] = $nrResults;
1326 1326
 
1327 1327
 		return $items;
@@ -1372,8 +1372,7 @@  discard block
 block discarded – undo
1372 1372
 
1373 1373
 		// if the search range is set limit the result to it, otherwise return all found messages
1374 1374
 		$rows = (is_array($searchRange) && isset($searchRange[0], $searchRange[1])) ?
1375
-			mapi_table_queryrows($table, [PR_ENTRYID], $searchRange[0], $searchRange[1] - $searchRange[0] + 1) :
1376
-			mapi_table_queryrows($table, [PR_ENTRYID], 0, SEARCH_MAXRESULTS);
1375
+			mapi_table_queryrows($table, [PR_ENTRYID], $searchRange[0], $searchRange[1] - $searchRange[0] + 1) : mapi_table_queryrows($table, [PR_ENTRYID], 0, SEARCH_MAXRESULTS);
1377 1376
 
1378 1377
 		$cnt = count($rows);
1379 1378
 		$items['searchtotal'] = $cnt;
@@ -1423,7 +1422,7 @@  discard block
 block discarded – undo
1423 1422
 				[
1424 1423
 					FUZZYLEVEL => FL_PREFIX,
1425 1424
 					ULPROPTAG => PR_DISPLAY_NAME,
1426
-					VALUE => [PR_DISPLAY_NAME => "grommunio-sync Search Folder " . $pid],
1425
+					VALUE => [PR_DISPLAY_NAME => "grommunio-sync Search Folder ".$pid],
1427 1426
 				],
1428 1427
 			],
1429 1428
 			TBL_BATCH
@@ -1567,7 +1566,7 @@  discard block
 block discarded – undo
1567 1566
 				$content_unread = isset($folder[PR_CONTENT_UNREAD]) ? $folder[PR_CONTENT_UNREAD] : -1;
1568 1567
 				$content_deleted = isset($folder[PR_DELETED_MSG_COUNT]) ? $folder[PR_DELETED_MSG_COUNT] : -1;
1569 1568
 
1570
-				$this->folderStatCache[$user][bin2hex($folder[PR_SOURCE_KEY])] = $commit_time . "/" . $content_count . "/" . $content_unread . "/" . $content_deleted;
1569
+				$this->folderStatCache[$user][bin2hex($folder[PR_SOURCE_KEY])] = $commit_time."/".$content_count."/".$content_unread."/".$content_deleted;
1571 1570
 			}
1572 1571
 			SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetFolderStat() fetched status information of %d folders for store '%s'", count($this->folderStatCache[$user]), $user));
1573 1572
 		}
@@ -1749,7 +1748,7 @@  discard block
 block discarded – undo
1749 1748
 				));
1750 1749
 			}
1751 1750
 		}
1752
-		$messageName = rtrim((($key !== false) ? $key . "-" : "") . (($type !== "") ? $type : ""), "-");
1751
+		$messageName = rtrim((($key !== false) ? $key."-" : "").(($type !== "") ? $type : ""), "-");
1753 1752
 		$restriction = $this->getStateMessageRestriction($messageName, $counter, $thisCounterOnly);
1754 1753
 		$stateFolderContents = mapi_folder_getcontentstable($this->stateFolder, MAPI_ASSOCIATED);
1755 1754
 		if ($stateFolderContents) {
@@ -1923,7 +1922,7 @@  discard block
 block discarded – undo
1923 1922
 				));
1924 1923
 			}
1925 1924
 		}
1926
-		$messageName = rtrim((($key !== false) ? $key . "-" : "") . (($type !== "") ? $type : ""), "-");
1925
+		$messageName = rtrim((($key !== false) ? $key."-" : "").(($type !== "") ? $type : ""), "-");
1927 1926
 		$restriction = $this->getStateMessageRestriction($messageName, $counter, true);
1928 1927
 		$stateFolderContents = mapi_folder_getcontentstable($this->stateFolder, MAPI_ASSOCIATED);
1929 1928
 		if ($stateFolderContents) {
@@ -1972,17 +1971,17 @@  discard block
 block discarded – undo
1972 1971
 			$stateMessage = mapi_folder_createmessage($this->stateFolder, MAPI_ASSOCIATED);
1973 1972
 			SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->setStateMessage(): mapi_folder_createmessage 0x%08X", mapi_last_hresult()));
1974 1973
 
1975
-			$messageName = rtrim((($key !== false) ? $key . "-" : "") . (($type !== "") ? $type : ""), "-");
1974
+			$messageName = rtrim((($key !== false) ? $key."-" : "").(($type !== "") ? $type : ""), "-");
1976 1975
 			SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->setStateMessage(): creating new state message '%s-%d'", $messageName, is_int($counter) ? $counter : 0));
1977 1976
 			mapi_setprops($stateMessage, [PR_DISPLAY_NAME => $messageName, PR_MESSAGE_CLASS => 'IPM.Note.GrommunioState']);
1978 1977
 		}
1979 1978
 		if (isset($stateMessage)) {
1980
-			$jsonEncodedState = is_object($state) || is_array($state) ? json_encode($state, JSON_INVALID_UTF8_IGNORE | JSON_UNESCAPED_UNICODE) : $state;
1979
+			$jsonEncodedState = is_object($state) || is_array($state) ? json_encode($state, JSON_INVALID_UTF8_IGNORE|JSON_UNESCAPED_UNICODE) : $state;
1981 1980
 
1982 1981
 			$encodedState = base64_encode($jsonEncodedState);
1983 1982
 			$encodedStateLength = strlen($encodedState);
1984 1983
 			mapi_setprops($stateMessage, [PR_LAST_VERB_EXECUTED => is_int($counter) ? $counter : 0]);
1985
-			$stream = mapi_openproperty($stateMessage, PR_BODY, IID_IStream, STGM_DIRECT, MAPI_CREATE | MAPI_MODIFY);
1984
+			$stream = mapi_openproperty($stateMessage, PR_BODY, IID_IStream, STGM_DIRECT, MAPI_CREATE|MAPI_MODIFY);
1986 1985
 			mapi_stream_setsize($stream, $encodedStateLength);
1987 1986
 			mapi_stream_write($stream, $encodedState);
1988 1987
 			mapi_stream_commit($stream);
@@ -2077,7 +2076,7 @@  discard block
 block discarded – undo
2077 2076
 	private function adviseStoreToSink($store) {
2078 2077
 		// check if we already advised the store
2079 2078
 		if (!in_array($store, $this->changesSinkStores)) {
2080
-			mapi_msgstore_advise($store, null, fnevObjectModified | fnevObjectCreated | fnevObjectMoved | fnevObjectDeleted, $this->changesSink);
2079
+			mapi_msgstore_advise($store, null, fnevObjectModified|fnevObjectCreated|fnevObjectMoved|fnevObjectDeleted, $this->changesSink);
2081 2080
 
2082 2081
 			if (mapi_last_hresult()) {
2083 2082
 				SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->adviseStoreToSink(): failed to advised store '%s' with code 0x%X. Polling will be performed.", $store, mapi_last_hresult()));
@@ -2502,7 +2501,7 @@  discard block
 block discarded – undo
2502 2501
 	 * @return mapiFolderObject
2503 2502
 	 */
2504 2503
 	private function createSearchFolder($searchFolderRoot) {
2505
-		$folderName = "grommunio-sync Search Folder " . @getmypid();
2504
+		$folderName = "grommunio-sync Search Folder ".@getmypid();
2506 2505
 		$searchFolders = mapi_folder_gethierarchytable($searchFolderRoot);
2507 2506
 		$restriction = [
2508 2507
 			RES_CONTENT,
@@ -2551,7 +2550,7 @@  discard block
 block discarded – undo
2551 2550
 					$resOr,
2552 2551
 					[RES_CONTENT,
2553 2552
 						[
2554
-							FUZZYLEVEL => FL_SUBSTRING | FL_IGNORECASE,
2553
+							FUZZYLEVEL => FL_SUBSTRING|FL_IGNORECASE,
2555 2554
 							ULPROPTAG => $property,
2556 2555
 							VALUE => u2w($term),
2557 2556
 						],
Please login to merge, or discard this patch.
Braces   +34 added lines, -68 removed lines patch added patch discarded remove patch
@@ -130,8 +130,7 @@  discard block
 block discarded – undo
130 130
 		if ($this->impersonateUser !== false) {
131 131
 			SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->Logon(): Impersonation active - authenticating: '%s' - impersonating '%s'", $this->mainUser, $this->impersonateUser));
132 132
 			$defaultUser = $this->impersonateUser;
133
-		}
134
-		else {
133
+		} else {
135 134
 			$defaultUser = $this->mainUser;
136 135
 		}
137 136
 
@@ -145,8 +144,7 @@  discard block
 block discarded – undo
145 144
 					$gsync_version = 'Grommunio-Sync_' . @constant('GROMMUNIOSYNC_VERSION');
146 145
 					$user_agent = ($deviceId) ? GSync::GetDeviceManager()->GetUserAgent() : "unknown";
147 146
 					$this->session = @mapi_logon_zarafa($this->mainUser, $pass, MAPI_SERVER, null, null, 0, $gsync_version, $user_agent);
148
-				}
149
-				else {
147
+				} else {
150 148
 					$this->session = @mapi_logon_zarafa($this->mainUser, $pass, MAPI_SERVER, null, null, 0);
151 149
 				}
152 150
 				$this->notifications = true;
@@ -163,8 +161,7 @@  discard block
 block discarded – undo
163 161
 					throw new ServiceUnavailableException("Error connecting to KC (login)");
164 162
 				}
165 163
 			}
166
-		}
167
-		catch (MAPIException $ex) {
164
+		} catch (MAPIException $ex) {
168 165
 			throw new AuthenticationRequiredException($ex->getDisplayMessage());
169 166
 		}
170 167
 
@@ -266,8 +263,7 @@  discard block
 block discarded – undo
266 263
 							$storeProps = mapi_getprops($userstore, [PR_IPM_SUBTREE_ENTRYID]);
267 264
 							$rights = $this->HasSecretaryACLs($userstore, '', $storeProps[PR_IPM_SUBTREE_ENTRYID]);
268 265
 							SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->Setup(): Checking for secretary ACLs on root folder of impersonated store '%s': '%s'", $user, Utils::PrintAsString($rights)));
269
-						}
270
-						else {
266
+						} else {
271 267
 							$zarafauserinfo = @nsp_getuserinfo($this->mainUser);
272 268
 							$rights = (isset($zarafauserinfo['admin']) && $zarafauserinfo['admin']) ? true : false;
273 269
 							SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->Setup(): Checking for admin ACLs on store '%s': '%s'", $user, Utils::PrintAsString($rights)));
@@ -330,8 +326,7 @@  discard block
 block discarded – undo
330 326
 		// for SYSTEM user open the public folders
331 327
 		if (strtoupper($this->storeName) == "SYSTEM") {
332 328
 			$rootfolder = mapi_msgstore_openentry($this->store, $storeProps[PR_IPM_PUBLIC_FOLDERS_ENTRYID]);
333
-		}
334
-		else {
329
+		} else {
335 330
 			$rootfolder = mapi_msgstore_openentry($this->store);
336 331
 		}
337 332
 
@@ -354,8 +349,7 @@  discard block
 block discarded – undo
354 349
 			$folder = $mapiprovider->GetFolder($row);
355 350
 			if ($folder) {
356 351
 				$folders[] = $folder;
357
-			}
358
-			else {
352
+			} else {
359 353
 				SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetHierarchy(): ignoring folder '%s' as MAPIProvider->GetFolder() did not return a SyncFolder object", (isset($row[PR_DISPLAY_NAME]) ? $row[PR_DISPLAY_NAME] : "unknown")));
360 354
 			}
361 355
 		}
@@ -547,8 +541,7 @@  discard block
 block discarded – undo
547 541
 						PR_ICON_INDEX => 262,
548 542
 						PR_LAST_VERB_EXECUTED => 104,
549 543
 					];
550
-				}
551
-				elseif ($sm->replyflag) {
544
+				} elseif ($sm->replyflag) {
552 545
 					$updateProps = [
553 546
 						PR_ICON_INDEX => 261,
554 547
 						PR_LAST_VERB_EXECUTED => 102,
@@ -608,8 +601,7 @@  discard block
 block discarded – undo
608 601
 						$mapiprops[$sendMailProps["html"]] = $bodyHtml . "<br><br>" . $fwbodyHtml;
609 602
 					}
610 603
 				}
611
-			}
612
-			else {
604
+			} else {
613 605
 				// no fwmessage could be opened and we need it because we do not replace mime
614 606
 				if (!isset($sm->replacemime) || $sm->replacemime == false) {
615 607
 					throw new StatusException(sprintf("Grommunio->SendMail(): Could not open message id '%s' in folder id '%s' to be replied/forwarded: 0x%X", $sm->source->itemid, $sm->source->folderid, mapi_last_hresult()), SYNC_COMMONSTATUS_ITEMNOTFOUND);
@@ -657,8 +649,7 @@  discard block
 block discarded – undo
657 649
 		if (!$folderid) {
658 650
 			$entryid = hex2bin($id);
659 651
 			$sk = $id;
660
-		}
661
-		else {
652
+		} else {
662 653
 			// id might be in the new longid format, so we have to split it here
663 654
 			list($fsk, $sk) = Utils::SplitMessageId($id);
664 655
 			// get the entry id of the message
@@ -751,8 +742,7 @@  discard block
 block discarded – undo
751 742
 			$stream = mapi_inetmapi_imtoinet($this->session, $addrbook, $embMessage, ['use_tnef' => -1]);
752 743
 			// set the default contenttype for this kind of messages
753 744
 			$attachment->contenttype = "message/rfc822";
754
-		}
755
-		else {
745
+		} else {
756 746
 			$stream = mapi_openproperty($attach, PR_ATTACH_DATA_BIN, IID_IStream, 0, 0);
757 747
 		}
758 748
 
@@ -764,8 +754,7 @@  discard block
 block discarded – undo
764 754
 		$attachment->data = MAPIStreamWrapper::Open($stream);
765 755
 		if (isset($attprops[PR_ATTACH_MIME_TAG])) {
766 756
 			$attachment->contenttype = $attprops[PR_ATTACH_MIME_TAG];
767
-		}
768
-		elseif (isset($attprops[PR_ATTACH_MIME_TAG_W])) {
757
+		} elseif (isset($attprops[PR_ATTACH_MIME_TAG_W])) {
769 758
 			$attachment->contenttype = $attprops[PR_ATTACH_MIME_TAG_W];
770 759
 		}
771 760
 		// TODO default contenttype
@@ -1164,8 +1153,7 @@  discard block
 block discarded – undo
1164 1153
 							}
1165 1154
 							++$response->recipientcount;
1166 1155
 							$response->recipient[] = $entry;
1167
-						}
1168
-						elseif (is_int($recipient)) {
1156
+						} elseif (is_int($recipient)) {
1169 1157
 							$response->status = $recipient;
1170 1158
 						}
1171 1159
 					}
@@ -1660,8 +1648,7 @@  discard block
 block discarded – undo
1660 1648
 			if (isset($stateMessageProps[PR_LAST_MODIFICATION_TIME])) {
1661 1649
 				return $stateMessageProps[PR_LAST_MODIFICATION_TIME];
1662 1650
 			}
1663
-		}
1664
-		catch (StateNotFoundException $e) {
1651
+		} catch (StateNotFoundException $e) {
1665 1652
 		}
1666 1653
 
1667 1654
 		return "0";
@@ -1859,8 +1846,7 @@  discard block
 block discarded – undo
1859 1846
 				if ($devid) {
1860 1847
 					$this->setDeviceUserData($this->userDeviceData, bin2hex($hierarchyRows[0][PR_ENTRYID]), $devid, $this->mainUser, "statefolder");
1861 1848
 				}
1862
-			}
1863
-			elseif ($rowCnt == 0) {
1849
+			} elseif ($rowCnt == 0) {
1864 1850
 				// legacy code: create the hidden state folder and the device subfolder
1865 1851
 				// this should happen when the user configures the device (autodiscover or first sync if no autodiscover)
1866 1852
 
@@ -1872,8 +1858,7 @@  discard block
 block discarded – undo
1872 1858
 				if ($rowCnt == 1) {
1873 1859
 					$hierarchyRows = mapi_table_queryrows($hierarchy, [PR_ENTRYID], 0, 1);
1874 1860
 					$stateFolder = mapi_msgstore_openentry($this->store, $hierarchyRows[0][PR_ENTRYID]);
1875
-				}
1876
-				elseif ($rowCnt == 0) {
1861
+				} elseif ($rowCnt == 0) {
1877 1862
 					$stateFolder = mapi_folder_createfolder($rootfolder, STORE_STATE_FOLDER, "");
1878 1863
 					mapi_setprops($stateFolder, [PR_ATTR_HIDDEN => true]);
1879 1864
 				}
@@ -1966,8 +1951,7 @@  discard block
 block discarded – undo
1966 1951
 
1967 1952
 		try {
1968 1953
 			$stateMessage = $this->getStateMessage($devid, $type, $key, $counter);
1969
-		}
1970
-		catch (StateNotFoundException $e) {
1954
+		} catch (StateNotFoundException $e) {
1971 1955
 			// if message is not available, try to create a new one
1972 1956
 			$stateMessage = mapi_folder_createmessage($this->stateFolder, MAPI_ASSOCIATED);
1973 1957
 			SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->setStateMessage(): mapi_folder_createmessage 0x%08X", mapi_last_hresult()));
@@ -2136,8 +2120,7 @@  discard block
 block discarded – undo
2136 2120
 					}
2137 2121
 				}
2138 2122
 			}
2139
-		}
2140
-		else {
2123
+		} else {
2141 2124
 			$entryid = @mapi_msgstore_createentryid($this->defaultstore, $user);
2142 2125
 		}
2143 2126
 
@@ -2215,8 +2198,7 @@  discard block
 block discarded – undo
2215 2198
 		// if oof state is set it must be set of oof and get otherwise
2216 2199
 		if (isset($oof->oofstate)) {
2217 2200
 			$this->settingsOofSet($oof);
2218
-		}
2219
-		else {
2201
+		} else {
2220 2202
 			$this->settingsOofGet($oof);
2221 2203
 		}
2222 2204
 	}
@@ -2251,13 +2233,11 @@  discard block
 block discarded – undo
2251 2233
 					@mapi_setprops($this->defaultstore, [PR_EC_OUTOFOFFICE => false]);
2252 2234
 					@mapi_deleteprops($this->defaultstore, [PR_EC_OUTOFOFFICE_FROM, PR_EC_OUTOFOFFICE_UNTIL]);
2253 2235
 					SLog::Write(LOGLEVEL_INFO, "Grommunio->settingsOofGet(): Out of office is set but the from and until are in the past. Disabling out of office.");
2254
-				}
2255
-				elseif ($oofprops[PR_EC_OUTOFOFFICE_FROM] < $oofprops[PR_EC_OUTOFOFFICE_UNTIL]) {
2236
+				} elseif ($oofprops[PR_EC_OUTOFOFFICE_FROM] < $oofprops[PR_EC_OUTOFOFFICE_UNTIL]) {
2256 2237
 					$oof->oofstate = SYNC_SETTINGSOOF_TIMEBASED;
2257 2238
 					$oof->starttime = $oofprops[PR_EC_OUTOFOFFICE_FROM];
2258 2239
 					$oof->endtime = $oofprops[PR_EC_OUTOFOFFICE_UNTIL];
2259
-				}
2260
-				else {
2240
+				} else {
2261 2241
 					SLog::Write(LOGLEVEL_WARN, sprintf(
2262 2242
 						"Grommunio->settingsOofGet(): Time based out of office set but end time ('%s') is before startime ('%s').",
2263 2243
 						date("Y-m-d H:i:s", $oofprops[PR_EC_OUTOFOFFICE_FROM]),
@@ -2265,8 +2245,7 @@  discard block
 block discarded – undo
2265 2245
 					));
2266 2246
 					$oof->Status = SYNC_SETTINGSSTATUS_PROTOCOLLERROR;
2267 2247
 				}
2268
-			}
2269
-			elseif ($oof->oofstate == SYNC_SETTINGSOOF_GLOBAL && (isset($oofprops[PR_EC_OUTOFOFFICE_FROM]) || isset($oofprops[PR_EC_OUTOFOFFICE_UNTIL]))) {
2248
+			} elseif ($oof->oofstate == SYNC_SETTINGSOOF_GLOBAL && (isset($oofprops[PR_EC_OUTOFOFFICE_FROM]) || isset($oofprops[PR_EC_OUTOFOFFICE_UNTIL]))) {
2270 2249
 				SLog::Write(LOGLEVEL_WARN, sprintf(
2271 2250
 					"Grommunio->settingsOofGet(): Time based out of office set but either start time ('%s') or end time ('%s') is missing.",
2272 2251
 					(isset($oofprops[PR_EC_OUTOFOFFICE_FROM]) ? date("Y-m-d H:i:s", $oofprops[PR_EC_OUTOFOFFICE_FROM]) : 'empty'),
@@ -2274,8 +2253,7 @@  discard block
 block discarded – undo
2274 2253
 				));
2275 2254
 				$oof->Status = SYNC_SETTINGSSTATUS_PROTOCOLLERROR;
2276 2255
 			}
2277
-		}
2278
-		else {
2256
+		} else {
2279 2257
 			SLog::Write(LOGLEVEL_WARN, "Grommunio->Unable to get out of office information");
2280 2258
 		}
2281 2259
 
@@ -2303,16 +2281,13 @@  discard block
 block discarded – undo
2303 2281
 				if (isset($oof->starttime, $oof->endtime)) {
2304 2282
 					$props[PR_EC_OUTOFOFFICE_FROM] = $oof->starttime;
2305 2283
 					$props[PR_EC_OUTOFOFFICE_UNTIL] = $oof->endtime;
2306
-				}
2307
-				elseif (isset($oof->starttime) || isset($oof->endtime)) {
2284
+				} elseif (isset($oof->starttime) || isset($oof->endtime)) {
2308 2285
 					$oof->Status = SYNC_SETTINGSSTATUS_PROTOCOLLERROR;
2309 2286
 				}
2310
-			}
2311
-			else {
2287
+			} else {
2312 2288
 				$deleteProps = [PR_EC_OUTOFOFFICE_FROM, PR_EC_OUTOFOFFICE_UNTIL];
2313 2289
 			}
2314
-		}
2315
-		elseif ($oof->oofstate == SYNC_SETTINGSOOF_DISABLED) {
2290
+		} elseif ($oof->oofstate == SYNC_SETTINGSOOF_DISABLED) {
2316 2291
 			$props[PR_EC_OUTOFOFFICE] = false;
2317 2292
 			$deleteProps = [PR_EC_OUTOFOFFICE_FROM, PR_EC_OUTOFOFFICE_UNTIL];
2318 2293
 		}
@@ -2355,8 +2330,7 @@  discard block
 block discarded – undo
2355 2330
 				$emailaddresses->primarysmtpaddress = $user["primary_email"];
2356 2331
 				$account->emailaddresses = $emailaddresses;
2357 2332
 				$userinformation->accounts[] = $account;
2358
-			}
2359
-			else {
2333
+			} else {
2360 2334
 				$userinformation->emailaddresses[] = $user["primary_email"];
2361 2335
 			}
2362 2336
 
@@ -2634,13 +2608,11 @@  discard block
 block discarded – undo
2634 2608
 			// some devices request 0 ambiguous recipients
2635 2609
 			if ($querycnt == 1 && $maxAmbiguousRecipients == 0) {
2636 2610
 				$rowsToQuery = 1;
2637
-			}
2638
-			elseif ($querycnt > 1 && $maxAmbiguousRecipients == 0) {
2611
+			} elseif ($querycnt > 1 && $maxAmbiguousRecipients == 0) {
2639 2612
 				SLog::Write(LOGLEVEL_INFO, sprintf("Grommunio->resolveRecipientGAL(): GAL search found %d recipients but the device hasn't requested ambiguous recipients", $querycnt));
2640 2613
 
2641 2614
 				return $recipientGal;
2642
-			}
2643
-			elseif ($querycnt > 1 && $maxAmbiguousRecipients == 1) {
2615
+			} elseif ($querycnt > 1 && $maxAmbiguousRecipients == 1) {
2644 2616
 				$rowsToQuery = $querycnt;
2645 2617
 			}
2646 2618
 			// get the certificate every time because caching the certificate is less expensive than opening addressbook entry again
@@ -2662,13 +2634,11 @@  discard block
 block discarded – undo
2662 2634
 							SLog::Write(LOGLEVEL_WBXML, sprintf("Grommunio->resolveRecipientGAL(): distlist's '%s' member: '%s'", $to, $distListMembers[$j][PR_DISPLAY_NAME]));
2663 2635
 							$recipientGal[] = $this->createResolveRecipient(SYNC_RESOLVERECIPIENTS_TYPE_GAL, $to, $distListMembers[$j], $nrDistListMembers);
2664 2636
 						}
2665
-					}
2666
-					else {
2637
+					} else {
2667 2638
 						SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->resolveRecipientGAL(): '%s' is a dist list, but return it as is.", $to));
2668 2639
 						$recipientGal[] = $this->createResolveRecipient(SYNC_RESOLVERECIPIENTS_TYPE_GAL, $abentries[$i][PR_SMTP_ADDRESS], $abentries[$i]);
2669 2640
 					}
2670
-				}
2671
-				elseif ($abentries[$i][PR_OBJECT_TYPE] == MAPI_MAILUSER) {
2641
+				} elseif ($abentries[$i][PR_OBJECT_TYPE] == MAPI_MAILUSER) {
2672 2642
 					$recipientGal[] = $this->createResolveRecipient(SYNC_RESOLVERECIPIENTS_TYPE_GAL, $abentries[$i][PR_SMTP_ADDRESS], $abentries[$i]);
2673 2643
 				}
2674 2644
 			}
@@ -2755,8 +2725,7 @@  discard block
 block discarded – undo
2755 2725
 					break;
2756 2726
 				}
2757 2727
 			}
2758
-		}
2759
-		else {
2728
+		} else {
2760 2729
 			SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->resolveRecipientContact(): Unable to open public store: 0x%X", $result));
2761 2730
 		}
2762 2731
 
@@ -2815,18 +2784,15 @@  discard block
 block discarded – undo
2815 2784
 
2816 2785
 		if ($type == SYNC_RESOLVERECIPIENTS_TYPE_GAL) {
2817 2786
 			$certificateProp = PR_EMS_AB_TAGGED_X509_CERT;
2818
-		}
2819
-		elseif ($type == SYNC_RESOLVERECIPIENTS_TYPE_CONTACT) {
2787
+		} elseif ($type == SYNC_RESOLVERECIPIENTS_TYPE_CONTACT) {
2820 2788
 			$certificateProp = PR_USER_X509_CERTIFICATE;
2821
-		}
2822
-		else {
2789
+		} else {
2823 2790
 			$certificateProp = null;
2824 2791
 		}
2825 2792
 
2826 2793
 		if (isset($recipientProperties[$certificateProp]) && is_array($recipientProperties[$certificateProp]) && !empty($recipientProperties[$certificateProp])) {
2827 2794
 			$certificates = $this->getCertificates($recipientProperties[$certificateProp], $recipientCount);
2828
-		}
2829
-		else {
2795
+		} else {
2830 2796
 			$certificates = $this->getCertificates(false);
2831 2797
 			SLog::Write(LOGLEVEL_INFO, sprintf("Grommunio->createResolveRecipient(): No certificate found for '%s' (requested email address: '%s')", $recipientProperties[PR_DISPLAY_NAME], $email));
2832 2798
 		}
Please login to merge, or discard this patch.
lib/grommunio/mapistreamwrapper.php 3 patches
Indentation   +165 added lines, -165 removed lines patch added patch discarded remove patch
@@ -9,171 +9,171 @@
 block discarded – undo
9 9
  */
10 10
 
11 11
 class MAPIStreamWrapper {
12
-	public const PROTOCOL = "mapistream";
13
-
14
-	private $mapistream;
15
-	private $position;
16
-	private $streamlength;
17
-	private $toTruncate;
18
-	private $truncateHtmlSafe;
19
-	private $context;
20
-
21
-	/**
22
-	 * Opens the stream
23
-	 * The mapistream reference is passed over the context.
24
-	 *
25
-	 * @param string $path        Specifies the URL that was passed to the original function
26
-	 * @param string $mode        The mode used to open the file, as detailed for fopen()
27
-	 * @param int    $options     Holds additional flags set by the streams API
28
-	 * @param string $opened_path if the path is opened successfully, and STREAM_USE_PATH is set in options,
29
-	 *                            opened_path should be set to the full path of the file/resource that was actually opened
30
-	 *
31
-	 * @return bool
32
-	 */
33
-	public function stream_open($path, $mode, $options, &$opened_path) {
34
-		$contextOptions = stream_context_get_options($this->context);
35
-		if (!isset($contextOptions[self::PROTOCOL]['stream'])) {
36
-			return false;
37
-		}
38
-
39
-		$this->position = 0;
40
-		$this->toTruncate = false;
41
-		$this->truncateHtmlSafe = (isset($contextOptions[self::PROTOCOL]['truncatehtmlsafe'])) ? $contextOptions[self::PROTOCOL]['truncatehtmlsafe'] : false;
42
-
43
-		// this is our stream!
44
-		$this->mapistream = $contextOptions[self::PROTOCOL]['stream'];
45
-
46
-		// get the data length from mapi
47
-		if ($this->mapistream) {
48
-			$stat = mapi_stream_stat($this->mapistream);
49
-			$this->streamlength = $stat["cb"];
50
-		}
51
-		else {
52
-			$this->streamlength = 0;
53
-		}
54
-
55
-		SLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIStreamWrapper::stream_open(): initialized mapistream: %s - streamlength: %d - HTML-safe-truncate: %s", $this->mapistream, $this->streamlength, Utils::PrintAsString($this->truncateHtmlSafe)));
56
-
57
-		return true;
58
-	}
59
-
60
-	/**
61
-	 * Reads from stream.
62
-	 *
63
-	 * @param int $len amount of bytes to be read
64
-	 *
65
-	 * @return string
66
-	 */
67
-	public function stream_read($len) {
68
-		$len = ($this->position + $len > $this->streamlength) ? ($this->streamlength - $this->position) : $len;
69
-
70
-		// read 4 additional bytes from the stream so we can always truncate correctly
71
-		if ($this->toTruncate && $this->position + $len >= $this->streamlength) {
72
-			$len += 4;
73
-		}
74
-		if ($this->mapistream) {
75
-			$data = mapi_stream_read($this->mapistream, $len);
76
-		}
77
-		else {
78
-			$data = "";
79
-		}
80
-		$this->position += strlen($data);
81
-
82
-		// we need to truncate UTF8 compatible if ftruncate() was called
83
-		if ($this->toTruncate && $this->position >= $this->streamlength) {
84
-			$data = Utils::Utf8_truncate($data, $this->streamlength, $this->truncateHtmlSafe);
85
-		}
86
-
87
-		return $data;
88
-	}
89
-
90
-	/**
91
-	 * Stream "seek" functionality.
92
-	 *
93
-	 * @param int $offset
94
-	 * @param int $whence
95
-	 *
96
-	 * @return bool
97
-	 */
98
-	public function stream_seek($offset, $whence = SEEK_SET) {
99
-		switch ($whence) {
100
-			case SEEK_SET:
101
-				$mapiWhence = STREAM_SEEK_SET;
102
-				break;
103
-
104
-			case SEEK_END:
105
-				$mapiWhence = STREAM_SEEK_END;
106
-				break;
107
-
108
-			default:
109
-				$mapiWhence = STREAM_SEEK_CUR;
110
-		}
111
-
112
-		return mapi_stream_seek($this->mapistream, $offset, $mapiWhence);
113
-	}
114
-
115
-	/**
116
-	 * Returns the current position on stream.
117
-	 *
118
-	 * @return int
119
-	 */
120
-	public function stream_tell() {
121
-		return $this->position;
122
-	}
123
-
124
-	/**
125
-	 * Indicates if 'end of file' is reached.
126
-	 *
127
-	 * @return bool
128
-	 */
129
-	public function stream_eof() {
130
-		return $this->position >= $this->streamlength;
131
-	}
132
-
133
-	/**
134
-	 * Truncates the stream to the new size.
135
-	 *
136
-	 * @param int $new_size
137
-	 *
138
-	 * @return bool
139
-	 */
140
-	public function stream_truncate($new_size) {
141
-		$this->streamlength = $new_size;
142
-		$this->toTruncate = true;
143
-
144
-		if ($this->position > $this->streamlength) {
145
-			SLog::Write(LOGLEVEL_WARN, sprintf("MAPIStreamWrapper->stream_truncate(): stream position (%d) ahead of new size of %d. Repositioning pointer to end of stream.", $this->position, $this->streamlength));
146
-			$this->position = $this->streamlength;
147
-		}
148
-
149
-		return true;
150
-	}
151
-
152
-	/**
153
-	 * Retrieves information about a stream.
154
-	 *
155
-	 * @return array
156
-	 */
157
-	public function stream_stat() {
158
-		return [
159
-			7 => $this->streamlength,
160
-			'size' => $this->streamlength,
161
-		];
162
-	}
163
-
164
-	/**
165
-	 * Instantiates a MAPIStreamWrapper.
166
-	 *
167
-	 * @param mapistream $mapistream       The stream to be wrapped
168
-	 * @param bool       $truncatehtmlsafe Indicates if a truncation should be done html-safe - default: false
169
-	 *
170
-	 * @return MAPIStreamWrapper
171
-	 */
172
-	public static function Open($mapistream, $truncatehtmlsafe = false) {
173
-		$context = stream_context_create([self::PROTOCOL => ['stream' => &$mapistream, 'truncatehtmlsafe' => $truncatehtmlsafe]]);
174
-
175
-		return fopen(self::PROTOCOL . "://", 'r', false, $context);
176
-	}
12
+    public const PROTOCOL = "mapistream";
13
+
14
+    private $mapistream;
15
+    private $position;
16
+    private $streamlength;
17
+    private $toTruncate;
18
+    private $truncateHtmlSafe;
19
+    private $context;
20
+
21
+    /**
22
+     * Opens the stream
23
+     * The mapistream reference is passed over the context.
24
+     *
25
+     * @param string $path        Specifies the URL that was passed to the original function
26
+     * @param string $mode        The mode used to open the file, as detailed for fopen()
27
+     * @param int    $options     Holds additional flags set by the streams API
28
+     * @param string $opened_path if the path is opened successfully, and STREAM_USE_PATH is set in options,
29
+     *                            opened_path should be set to the full path of the file/resource that was actually opened
30
+     *
31
+     * @return bool
32
+     */
33
+    public function stream_open($path, $mode, $options, &$opened_path) {
34
+        $contextOptions = stream_context_get_options($this->context);
35
+        if (!isset($contextOptions[self::PROTOCOL]['stream'])) {
36
+            return false;
37
+        }
38
+
39
+        $this->position = 0;
40
+        $this->toTruncate = false;
41
+        $this->truncateHtmlSafe = (isset($contextOptions[self::PROTOCOL]['truncatehtmlsafe'])) ? $contextOptions[self::PROTOCOL]['truncatehtmlsafe'] : false;
42
+
43
+        // this is our stream!
44
+        $this->mapistream = $contextOptions[self::PROTOCOL]['stream'];
45
+
46
+        // get the data length from mapi
47
+        if ($this->mapistream) {
48
+            $stat = mapi_stream_stat($this->mapistream);
49
+            $this->streamlength = $stat["cb"];
50
+        }
51
+        else {
52
+            $this->streamlength = 0;
53
+        }
54
+
55
+        SLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIStreamWrapper::stream_open(): initialized mapistream: %s - streamlength: %d - HTML-safe-truncate: %s", $this->mapistream, $this->streamlength, Utils::PrintAsString($this->truncateHtmlSafe)));
56
+
57
+        return true;
58
+    }
59
+
60
+    /**
61
+     * Reads from stream.
62
+     *
63
+     * @param int $len amount of bytes to be read
64
+     *
65
+     * @return string
66
+     */
67
+    public function stream_read($len) {
68
+        $len = ($this->position + $len > $this->streamlength) ? ($this->streamlength - $this->position) : $len;
69
+
70
+        // read 4 additional bytes from the stream so we can always truncate correctly
71
+        if ($this->toTruncate && $this->position + $len >= $this->streamlength) {
72
+            $len += 4;
73
+        }
74
+        if ($this->mapistream) {
75
+            $data = mapi_stream_read($this->mapistream, $len);
76
+        }
77
+        else {
78
+            $data = "";
79
+        }
80
+        $this->position += strlen($data);
81
+
82
+        // we need to truncate UTF8 compatible if ftruncate() was called
83
+        if ($this->toTruncate && $this->position >= $this->streamlength) {
84
+            $data = Utils::Utf8_truncate($data, $this->streamlength, $this->truncateHtmlSafe);
85
+        }
86
+
87
+        return $data;
88
+    }
89
+
90
+    /**
91
+     * Stream "seek" functionality.
92
+     *
93
+     * @param int $offset
94
+     * @param int $whence
95
+     *
96
+     * @return bool
97
+     */
98
+    public function stream_seek($offset, $whence = SEEK_SET) {
99
+        switch ($whence) {
100
+            case SEEK_SET:
101
+                $mapiWhence = STREAM_SEEK_SET;
102
+                break;
103
+
104
+            case SEEK_END:
105
+                $mapiWhence = STREAM_SEEK_END;
106
+                break;
107
+
108
+            default:
109
+                $mapiWhence = STREAM_SEEK_CUR;
110
+        }
111
+
112
+        return mapi_stream_seek($this->mapistream, $offset, $mapiWhence);
113
+    }
114
+
115
+    /**
116
+     * Returns the current position on stream.
117
+     *
118
+     * @return int
119
+     */
120
+    public function stream_tell() {
121
+        return $this->position;
122
+    }
123
+
124
+    /**
125
+     * Indicates if 'end of file' is reached.
126
+     *
127
+     * @return bool
128
+     */
129
+    public function stream_eof() {
130
+        return $this->position >= $this->streamlength;
131
+    }
132
+
133
+    /**
134
+     * Truncates the stream to the new size.
135
+     *
136
+     * @param int $new_size
137
+     *
138
+     * @return bool
139
+     */
140
+    public function stream_truncate($new_size) {
141
+        $this->streamlength = $new_size;
142
+        $this->toTruncate = true;
143
+
144
+        if ($this->position > $this->streamlength) {
145
+            SLog::Write(LOGLEVEL_WARN, sprintf("MAPIStreamWrapper->stream_truncate(): stream position (%d) ahead of new size of %d. Repositioning pointer to end of stream.", $this->position, $this->streamlength));
146
+            $this->position = $this->streamlength;
147
+        }
148
+
149
+        return true;
150
+    }
151
+
152
+    /**
153
+     * Retrieves information about a stream.
154
+     *
155
+     * @return array
156
+     */
157
+    public function stream_stat() {
158
+        return [
159
+            7 => $this->streamlength,
160
+            'size' => $this->streamlength,
161
+        ];
162
+    }
163
+
164
+    /**
165
+     * Instantiates a MAPIStreamWrapper.
166
+     *
167
+     * @param mapistream $mapistream       The stream to be wrapped
168
+     * @param bool       $truncatehtmlsafe Indicates if a truncation should be done html-safe - default: false
169
+     *
170
+     * @return MAPIStreamWrapper
171
+     */
172
+    public static function Open($mapistream, $truncatehtmlsafe = false) {
173
+        $context = stream_context_create([self::PROTOCOL => ['stream' => &$mapistream, 'truncatehtmlsafe' => $truncatehtmlsafe]]);
174
+
175
+        return fopen(self::PROTOCOL . "://", 'r', false, $context);
176
+    }
177 177
 }
178 178
 
179 179
 stream_wrapper_register(MAPIStreamWrapper::PROTOCOL, "MAPIStreamWrapper");
Please login to merge, or discard this patch.
Spacing   +1 added lines, -1 removed lines patch added patch discarded remove patch
@@ -172,7 +172,7 @@
 block discarded – undo
172 172
 	public static function Open($mapistream, $truncatehtmlsafe = false) {
173 173
 		$context = stream_context_create([self::PROTOCOL => ['stream' => &$mapistream, 'truncatehtmlsafe' => $truncatehtmlsafe]]);
174 174
 
175
-		return fopen(self::PROTOCOL . "://", 'r', false, $context);
175
+		return fopen(self::PROTOCOL."://", 'r', false, $context);
176 176
 	}
177 177
 }
178 178
 
Please login to merge, or discard this patch.
Braces   +2 added lines, -4 removed lines patch added patch discarded remove patch
@@ -47,8 +47,7 @@  discard block
 block discarded – undo
47 47
 		if ($this->mapistream) {
48 48
 			$stat = mapi_stream_stat($this->mapistream);
49 49
 			$this->streamlength = $stat["cb"];
50
-		}
51
-		else {
50
+		} else {
52 51
 			$this->streamlength = 0;
53 52
 		}
54 53
 
@@ -73,8 +72,7 @@  discard block
 block discarded – undo
73 72
 		}
74 73
 		if ($this->mapistream) {
75 74
 			$data = mapi_stream_read($this->mapistream, $len);
76
-		}
77
-		else {
75
+		} else {
78 76
 			$data = "";
79 77
 		}
80 78
 		$this->position += strlen($data);
Please login to merge, or discard this patch.
lib/grommunio/exporter.php 3 patches
Indentation   +286 added lines, -286 removed lines patch added patch discarded remove patch
@@ -15,290 +15,290 @@
 block discarded – undo
15 15
  * that the ImportProxies are used.
16 16
  */
17 17
 class ExportChangesICS implements IExportChanges {
18
-	private $folderid;
19
-	private $store;
20
-	private $session;
21
-	private $restriction;
22
-	private $contentparameters;
23
-	private $flags;
24
-	private $exporterflags;
25
-	private $exporter;
26
-	private $moveSrcState;
27
-	private $moveDstState;
28
-
29
-	/**
30
-	 * Constructor.
31
-	 *
32
-	 * @param mapisession $session
33
-	 * @param mapistore   $store
34
-	 * @param mixed       $folderid
35
-	 *
36
-	 * @throws StatusException
37
-	 */
38
-	public function __construct($session, $store, $folderid = false) {
39
-		// Open a hierarchy or a contents exporter depending on whether a folderid was specified
40
-		$this->session = $session;
41
-		$this->folderid = $folderid;
42
-		$this->store = $store;
43
-		$this->restriction = false;
44
-
45
-		try {
46
-			if ($folderid) {
47
-				$entryid = mapi_msgstore_entryidfromsourcekey($store, $folderid);
48
-			}
49
-			else {
50
-				$storeprops = mapi_getprops($this->store, [PR_IPM_SUBTREE_ENTRYID, PR_IPM_PUBLIC_FOLDERS_ENTRYID]);
51
-				if (GSync::GetBackend()->GetImpersonatedUser() == 'system') {
52
-					$entryid = $storeprops[PR_IPM_PUBLIC_FOLDERS_ENTRYID];
53
-				}
54
-				else {
55
-					$entryid = $storeprops[PR_IPM_SUBTREE_ENTRYID];
56
-				}
57
-			}
58
-
59
-			$folder = false;
60
-			if ($entryid) {
61
-				$folder = mapi_msgstore_openentry($this->store, $entryid);
62
-				if (!$folder) {
63
-					SLog::Write(LOGLEVEL_WARN, sprintf("ExportChangesICS(): Error, mapi_msgstore_openentry() failed: 0x%08X", mapi_last_hresult()));
64
-				}
65
-			}
66
-
67
-			// Get the actual ICS exporter
68
-			if ($folder) {
69
-				if ($folderid) {
70
-					$this->exporter = mapi_openproperty($folder, PR_CONTENTS_SYNCHRONIZER, IID_IExchangeExportChanges, 0, 0);
71
-				}
72
-				else {
73
-					$this->exporter = mapi_openproperty($folder, PR_HIERARCHY_SYNCHRONIZER, IID_IExchangeExportChanges, 0, 0);
74
-				}
75
-			}
76
-			else {
77
-				$this->exporter = false;
78
-			}
79
-		}
80
-		catch (MAPIException $me) {
81
-			$this->exporter = false;
82
-			// We return the general error SYNC_FSSTATUS_CODEUNKNOWN (12) which is also SYNC_STATUS_FOLDERHIERARCHYCHANGED (12)
83
-			// if this happened while doing content sync, the mobile will try to resync the folderhierarchy
84
-			throw new StatusException(sprintf("ExportChangesICS('%s','%s','%s'): Error, unable to open folder: 0x%X", $session, $store, Utils::PrintAsString($folderid), mapi_last_hresult()), SYNC_FSSTATUS_CODEUNKNOWN);
85
-		}
86
-	}
87
-
88
-	/**
89
-	 * Configures the exporter.
90
-	 *
91
-	 * @param string $state
92
-	 * @param int    $flags
93
-	 *
94
-	 * @throws StatusException
95
-	 *
96
-	 * @return bool
97
-	 */
98
-	public function Config($state, $flags = 0) {
99
-		$this->exporterflags = 0;
100
-		$this->flags = $flags;
101
-
102
-		// this should never happen
103
-		if ($this->exporter === false || is_array($state)) {
104
-			throw new StatusException("ExportChangesICS->Config(): Error, exporter not available", SYNC_FSSTATUS_CODEUNKNOWN, null, LOGLEVEL_ERROR);
105
-		}
106
-
107
-		// change exporterflags if we are doing a ContentExport
108
-		if ($this->folderid) {
109
-			$this->exporterflags |= SYNC_NORMAL | SYNC_READ_STATE;
110
-
111
-			// Initial sync, we don't want deleted items. If the initial sync is chunked
112
-			// we check the change ID of the syncstate (0 at initial sync)
113
-			// On subsequent syncs, we do want to receive delete events.
114
-			if (strlen($state) == 0 || bin2hex(substr($state, 4, 4)) == "00000000") {
115
-				if (!($this->flags & BACKEND_DISCARD_DATA)) {
116
-					SLog::Write(LOGLEVEL_DEBUG, "ExportChangesICS->Config(): syncing initial data");
117
-				}
118
-				$this->exporterflags |= SYNC_NO_SOFT_DELETIONS | SYNC_NO_DELETIONS;
119
-			}
120
-		}
121
-
122
-		if ($this->flags & BACKEND_DISCARD_DATA) {
123
-			$this->exporterflags |= SYNC_CATCHUP;
124
-			$this->exporterflags |= SYNC_STATE_READONLY;
125
-		}
126
-
127
-		// Put the state information in a stream that can be used by ICS
128
-		$stream = mapi_stream_create();
129
-		if (strlen($state) == 0) {
130
-			$state = hex2bin("0000000000000000");
131
-		}
132
-
133
-		if (!($this->flags & BACKEND_DISCARD_DATA)) {
134
-			SLog::Write(LOGLEVEL_DEBUG, sprintf("ExportChangesICS->Config() initialized with state: 0x%s", bin2hex($state)));
135
-		}
136
-
137
-		mapi_stream_write($stream, $state);
138
-		$this->statestream = $stream;
139
-	}
140
-
141
-	/**
142
-	 * Configures additional parameters used for content synchronization.
143
-	 *
144
-	 * @param ContentParameters $contentparameters
145
-	 *
146
-	 * @throws StatusException
147
-	 *
148
-	 * @return bool
149
-	 */
150
-	public function ConfigContentParameters($contentparameters) {
151
-		$filtertype = $contentparameters->GetFilterType();
152
-
153
-		switch ($contentparameters->GetContentClass()) {
154
-			case "Email":
155
-				$this->restriction = ($filtertype || !Utils::CheckMapiExtVersion('7')) ? MAPIUtils::GetEmailRestriction(Utils::GetCutOffDate($filtertype)) : false;
156
-				break;
157
-
158
-			case "Calendar":
159
-				$this->restriction = ($filtertype || !Utils::CheckMapiExtVersion('7')) ? MAPIUtils::GetCalendarRestriction($this->store, Utils::GetCutOffDate($filtertype)) : false;
160
-				break;
161
-
162
-			default:
163
-			case "Contacts":
164
-			case "Tasks":
165
-				$this->restriction = false;
166
-				break;
167
-		}
168
-
169
-		$this->contentParameters = $contentparameters;
170
-	}
171
-
172
-	/**
173
-	 * Sets the importer the exporter will sent it's changes to
174
-	 * and initializes the Exporter.
175
-	 *
176
-	 * @param object &$importer Implementation of IImportChanges
177
-	 *
178
-	 * @throws StatusException
179
-	 *
180
-	 * @return bool
181
-	 */
182
-	public function InitializeExporter(&$importer) {
183
-		// Because we're using ICS, we need to wrap the given importer to make it suitable to pass
184
-		// to ICS. We do this in two steps: first, wrap the importer with our own PHP importer class
185
-		// which removes all MAPI dependency, and then wrap that class with a C++ wrapper so we can
186
-		// pass it to ICS
187
-
188
-		// this should never happen!
189
-		if ($this->exporter === false || !isset($this->statestream) || !isset($this->flags) || !isset($this->exporterflags) ||
190
-			($this->folderid && !isset($this->contentParameters))) {
191
-			throw new StatusException("ExportChangesICS->InitializeExporter(): Error, exporter or essential data not available", SYNC_FSSTATUS_CODEUNKNOWN, null, LOGLEVEL_ERROR);
192
-		}
193
-
194
-		// PHP wrapper
195
-		$phpwrapper = new PHPWrapper($this->session, $this->store, $importer, $this->folderid);
196
-
197
-		// with a folderid we are going to get content
198
-		if ($this->folderid) {
199
-			$phpwrapper->ConfigContentParameters($this->contentParameters);
200
-
201
-			// ICS c++ wrapper
202
-			$mapiimporter = mapi_wrap_importcontentschanges($phpwrapper);
203
-			$includeprops = false;
204
-		}
205
-		else {
206
-			$mapiimporter = mapi_wrap_importhierarchychanges($phpwrapper);
207
-			$includeprops = [PR_SOURCE_KEY, PR_DISPLAY_NAME];
208
-		}
209
-
210
-		if (!$mapiimporter) {
211
-			throw new StatusException(sprintf("ExportChangesICS->InitializeExporter(): Error, mapi_wrap_import_*_changes() failed: 0x%X", mapi_last_hresult()), SYNC_FSSTATUS_CODEUNKNOWN, null, LOGLEVEL_WARN);
212
-		}
213
-
214
-		$ret = mapi_exportchanges_config($this->exporter, $this->statestream, $this->exporterflags, $mapiimporter, $this->restriction, $includeprops, false, 1);
215
-		if (!$ret) {
216
-			throw new StatusException(sprintf("ExportChangesICS->InitializeExporter(): Error, mapi_exportchanges_config() failed: 0x%X", mapi_last_hresult()), SYNC_FSSTATUS_CODEUNKNOWN, null, LOGLEVEL_WARN);
217
-		}
218
-
219
-		$changes = mapi_exportchanges_getchangecount($this->exporter);
220
-		if ($changes || !($this->flags & BACKEND_DISCARD_DATA)) {
221
-			SLog::Write(LOGLEVEL_DEBUG, sprintf("ExportChangesICS->InitializeExporter() successfully. %d changes ready to sync for '%s'.", $changes, ($this->folderid) ? bin2hex($this->folderid) : 'hierarchy'));
222
-		}
223
-
224
-		return $ret;
225
-	}
226
-
227
-	/**
228
-	 * Indicates if the exporter was configured with the BACKEND_DISCARD_DATA flag.
229
-	 *
230
-	 * @return bool
231
-	 */
232
-	public function HasDiscardDataFlag() {
233
-		if (isset($this->flags) && $this->flags & BACKEND_DISCARD_DATA) {
234
-			return true;
235
-		}
236
-
237
-		return false;
238
-	}
239
-
240
-	/**
241
-	 * Reads the current state from the Exporter.
242
-	 *
243
-	 * @throws StatusException
244
-	 *
245
-	 * @return string
246
-	 */
247
-	public function GetState() {
248
-		$error = false;
249
-		if (!isset($this->statestream) || $this->exporter === false) {
250
-			$error = true;
251
-		}
252
-
253
-		if ($error === true || mapi_exportchanges_updatestate($this->exporter, $this->statestream) != true) {
254
-			throw new StatusException(sprintf("ExportChangesICS->GetState(): Error, state not available or unable to update: 0x%X", mapi_last_hresult()), (($this->folderid) ? SYNC_STATUS_FOLDERHIERARCHYCHANGED : SYNC_FSSTATUS_CODEUNKNOWN), null, LOGLEVEL_WARN);
255
-		}
256
-
257
-		mapi_stream_seek($this->statestream, 0, STREAM_SEEK_SET);
258
-
259
-		$state = "";
260
-		while (true) {
261
-			$data = mapi_stream_read($this->statestream, 4096);
262
-			if (strlen($data)) {
263
-				$state .= $data;
264
-			}
265
-			else {
266
-				break;
267
-			}
268
-		}
269
-
270
-		return $state;
271
-	}
272
-
273
-	/**
274
-	 * Returns the amount of changes to be exported.
275
-	 *
276
-	 * @return int
277
-	 */
278
-	public function GetChangeCount() {
279
-		if ($this->exporter) {
280
-			return mapi_exportchanges_getchangecount($this->exporter);
281
-		}
282
-
283
-		return 0;
284
-	}
285
-
286
-	/**
287
-	 * Synchronizes a change.
288
-	 *
289
-	 * @return array
290
-	 */
291
-	public function Synchronize() {
292
-		if ($this->flags & BACKEND_DISCARD_DATA) {
293
-			SLog::Write(LOGLEVEL_WARN, 'ExportChangesICS->Synchronize(): not supported in combination with the BACKEND_DISCARD_DATA flag.');
294
-
295
-			return false;
296
-		}
297
-
298
-		if ($this->exporter) {
299
-			return mapi_exportchanges_synchronize($this->exporter);
300
-		}
301
-
302
-		return false;
303
-	}
18
+    private $folderid;
19
+    private $store;
20
+    private $session;
21
+    private $restriction;
22
+    private $contentparameters;
23
+    private $flags;
24
+    private $exporterflags;
25
+    private $exporter;
26
+    private $moveSrcState;
27
+    private $moveDstState;
28
+
29
+    /**
30
+     * Constructor.
31
+     *
32
+     * @param mapisession $session
33
+     * @param mapistore   $store
34
+     * @param mixed       $folderid
35
+     *
36
+     * @throws StatusException
37
+     */
38
+    public function __construct($session, $store, $folderid = false) {
39
+        // Open a hierarchy or a contents exporter depending on whether a folderid was specified
40
+        $this->session = $session;
41
+        $this->folderid = $folderid;
42
+        $this->store = $store;
43
+        $this->restriction = false;
44
+
45
+        try {
46
+            if ($folderid) {
47
+                $entryid = mapi_msgstore_entryidfromsourcekey($store, $folderid);
48
+            }
49
+            else {
50
+                $storeprops = mapi_getprops($this->store, [PR_IPM_SUBTREE_ENTRYID, PR_IPM_PUBLIC_FOLDERS_ENTRYID]);
51
+                if (GSync::GetBackend()->GetImpersonatedUser() == 'system') {
52
+                    $entryid = $storeprops[PR_IPM_PUBLIC_FOLDERS_ENTRYID];
53
+                }
54
+                else {
55
+                    $entryid = $storeprops[PR_IPM_SUBTREE_ENTRYID];
56
+                }
57
+            }
58
+
59
+            $folder = false;
60
+            if ($entryid) {
61
+                $folder = mapi_msgstore_openentry($this->store, $entryid);
62
+                if (!$folder) {
63
+                    SLog::Write(LOGLEVEL_WARN, sprintf("ExportChangesICS(): Error, mapi_msgstore_openentry() failed: 0x%08X", mapi_last_hresult()));
64
+                }
65
+            }
66
+
67
+            // Get the actual ICS exporter
68
+            if ($folder) {
69
+                if ($folderid) {
70
+                    $this->exporter = mapi_openproperty($folder, PR_CONTENTS_SYNCHRONIZER, IID_IExchangeExportChanges, 0, 0);
71
+                }
72
+                else {
73
+                    $this->exporter = mapi_openproperty($folder, PR_HIERARCHY_SYNCHRONIZER, IID_IExchangeExportChanges, 0, 0);
74
+                }
75
+            }
76
+            else {
77
+                $this->exporter = false;
78
+            }
79
+        }
80
+        catch (MAPIException $me) {
81
+            $this->exporter = false;
82
+            // We return the general error SYNC_FSSTATUS_CODEUNKNOWN (12) which is also SYNC_STATUS_FOLDERHIERARCHYCHANGED (12)
83
+            // if this happened while doing content sync, the mobile will try to resync the folderhierarchy
84
+            throw new StatusException(sprintf("ExportChangesICS('%s','%s','%s'): Error, unable to open folder: 0x%X", $session, $store, Utils::PrintAsString($folderid), mapi_last_hresult()), SYNC_FSSTATUS_CODEUNKNOWN);
85
+        }
86
+    }
87
+
88
+    /**
89
+     * Configures the exporter.
90
+     *
91
+     * @param string $state
92
+     * @param int    $flags
93
+     *
94
+     * @throws StatusException
95
+     *
96
+     * @return bool
97
+     */
98
+    public function Config($state, $flags = 0) {
99
+        $this->exporterflags = 0;
100
+        $this->flags = $flags;
101
+
102
+        // this should never happen
103
+        if ($this->exporter === false || is_array($state)) {
104
+            throw new StatusException("ExportChangesICS->Config(): Error, exporter not available", SYNC_FSSTATUS_CODEUNKNOWN, null, LOGLEVEL_ERROR);
105
+        }
106
+
107
+        // change exporterflags if we are doing a ContentExport
108
+        if ($this->folderid) {
109
+            $this->exporterflags |= SYNC_NORMAL | SYNC_READ_STATE;
110
+
111
+            // Initial sync, we don't want deleted items. If the initial sync is chunked
112
+            // we check the change ID of the syncstate (0 at initial sync)
113
+            // On subsequent syncs, we do want to receive delete events.
114
+            if (strlen($state) == 0 || bin2hex(substr($state, 4, 4)) == "00000000") {
115
+                if (!($this->flags & BACKEND_DISCARD_DATA)) {
116
+                    SLog::Write(LOGLEVEL_DEBUG, "ExportChangesICS->Config(): syncing initial data");
117
+                }
118
+                $this->exporterflags |= SYNC_NO_SOFT_DELETIONS | SYNC_NO_DELETIONS;
119
+            }
120
+        }
121
+
122
+        if ($this->flags & BACKEND_DISCARD_DATA) {
123
+            $this->exporterflags |= SYNC_CATCHUP;
124
+            $this->exporterflags |= SYNC_STATE_READONLY;
125
+        }
126
+
127
+        // Put the state information in a stream that can be used by ICS
128
+        $stream = mapi_stream_create();
129
+        if (strlen($state) == 0) {
130
+            $state = hex2bin("0000000000000000");
131
+        }
132
+
133
+        if (!($this->flags & BACKEND_DISCARD_DATA)) {
134
+            SLog::Write(LOGLEVEL_DEBUG, sprintf("ExportChangesICS->Config() initialized with state: 0x%s", bin2hex($state)));
135
+        }
136
+
137
+        mapi_stream_write($stream, $state);
138
+        $this->statestream = $stream;
139
+    }
140
+
141
+    /**
142
+     * Configures additional parameters used for content synchronization.
143
+     *
144
+     * @param ContentParameters $contentparameters
145
+     *
146
+     * @throws StatusException
147
+     *
148
+     * @return bool
149
+     */
150
+    public function ConfigContentParameters($contentparameters) {
151
+        $filtertype = $contentparameters->GetFilterType();
152
+
153
+        switch ($contentparameters->GetContentClass()) {
154
+            case "Email":
155
+                $this->restriction = ($filtertype || !Utils::CheckMapiExtVersion('7')) ? MAPIUtils::GetEmailRestriction(Utils::GetCutOffDate($filtertype)) : false;
156
+                break;
157
+
158
+            case "Calendar":
159
+                $this->restriction = ($filtertype || !Utils::CheckMapiExtVersion('7')) ? MAPIUtils::GetCalendarRestriction($this->store, Utils::GetCutOffDate($filtertype)) : false;
160
+                break;
161
+
162
+            default:
163
+            case "Contacts":
164
+            case "Tasks":
165
+                $this->restriction = false;
166
+                break;
167
+        }
168
+
169
+        $this->contentParameters = $contentparameters;
170
+    }
171
+
172
+    /**
173
+     * Sets the importer the exporter will sent it's changes to
174
+     * and initializes the Exporter.
175
+     *
176
+     * @param object &$importer Implementation of IImportChanges
177
+     *
178
+     * @throws StatusException
179
+     *
180
+     * @return bool
181
+     */
182
+    public function InitializeExporter(&$importer) {
183
+        // Because we're using ICS, we need to wrap the given importer to make it suitable to pass
184
+        // to ICS. We do this in two steps: first, wrap the importer with our own PHP importer class
185
+        // which removes all MAPI dependency, and then wrap that class with a C++ wrapper so we can
186
+        // pass it to ICS
187
+
188
+        // this should never happen!
189
+        if ($this->exporter === false || !isset($this->statestream) || !isset($this->flags) || !isset($this->exporterflags) ||
190
+            ($this->folderid && !isset($this->contentParameters))) {
191
+            throw new StatusException("ExportChangesICS->InitializeExporter(): Error, exporter or essential data not available", SYNC_FSSTATUS_CODEUNKNOWN, null, LOGLEVEL_ERROR);
192
+        }
193
+
194
+        // PHP wrapper
195
+        $phpwrapper = new PHPWrapper($this->session, $this->store, $importer, $this->folderid);
196
+
197
+        // with a folderid we are going to get content
198
+        if ($this->folderid) {
199
+            $phpwrapper->ConfigContentParameters($this->contentParameters);
200
+
201
+            // ICS c++ wrapper
202
+            $mapiimporter = mapi_wrap_importcontentschanges($phpwrapper);
203
+            $includeprops = false;
204
+        }
205
+        else {
206
+            $mapiimporter = mapi_wrap_importhierarchychanges($phpwrapper);
207
+            $includeprops = [PR_SOURCE_KEY, PR_DISPLAY_NAME];
208
+        }
209
+
210
+        if (!$mapiimporter) {
211
+            throw new StatusException(sprintf("ExportChangesICS->InitializeExporter(): Error, mapi_wrap_import_*_changes() failed: 0x%X", mapi_last_hresult()), SYNC_FSSTATUS_CODEUNKNOWN, null, LOGLEVEL_WARN);
212
+        }
213
+
214
+        $ret = mapi_exportchanges_config($this->exporter, $this->statestream, $this->exporterflags, $mapiimporter, $this->restriction, $includeprops, false, 1);
215
+        if (!$ret) {
216
+            throw new StatusException(sprintf("ExportChangesICS->InitializeExporter(): Error, mapi_exportchanges_config() failed: 0x%X", mapi_last_hresult()), SYNC_FSSTATUS_CODEUNKNOWN, null, LOGLEVEL_WARN);
217
+        }
218
+
219
+        $changes = mapi_exportchanges_getchangecount($this->exporter);
220
+        if ($changes || !($this->flags & BACKEND_DISCARD_DATA)) {
221
+            SLog::Write(LOGLEVEL_DEBUG, sprintf("ExportChangesICS->InitializeExporter() successfully. %d changes ready to sync for '%s'.", $changes, ($this->folderid) ? bin2hex($this->folderid) : 'hierarchy'));
222
+        }
223
+
224
+        return $ret;
225
+    }
226
+
227
+    /**
228
+     * Indicates if the exporter was configured with the BACKEND_DISCARD_DATA flag.
229
+     *
230
+     * @return bool
231
+     */
232
+    public function HasDiscardDataFlag() {
233
+        if (isset($this->flags) && $this->flags & BACKEND_DISCARD_DATA) {
234
+            return true;
235
+        }
236
+
237
+        return false;
238
+    }
239
+
240
+    /**
241
+     * Reads the current state from the Exporter.
242
+     *
243
+     * @throws StatusException
244
+     *
245
+     * @return string
246
+     */
247
+    public function GetState() {
248
+        $error = false;
249
+        if (!isset($this->statestream) || $this->exporter === false) {
250
+            $error = true;
251
+        }
252
+
253
+        if ($error === true || mapi_exportchanges_updatestate($this->exporter, $this->statestream) != true) {
254
+            throw new StatusException(sprintf("ExportChangesICS->GetState(): Error, state not available or unable to update: 0x%X", mapi_last_hresult()), (($this->folderid) ? SYNC_STATUS_FOLDERHIERARCHYCHANGED : SYNC_FSSTATUS_CODEUNKNOWN), null, LOGLEVEL_WARN);
255
+        }
256
+
257
+        mapi_stream_seek($this->statestream, 0, STREAM_SEEK_SET);
258
+
259
+        $state = "";
260
+        while (true) {
261
+            $data = mapi_stream_read($this->statestream, 4096);
262
+            if (strlen($data)) {
263
+                $state .= $data;
264
+            }
265
+            else {
266
+                break;
267
+            }
268
+        }
269
+
270
+        return $state;
271
+    }
272
+
273
+    /**
274
+     * Returns the amount of changes to be exported.
275
+     *
276
+     * @return int
277
+     */
278
+    public function GetChangeCount() {
279
+        if ($this->exporter) {
280
+            return mapi_exportchanges_getchangecount($this->exporter);
281
+        }
282
+
283
+        return 0;
284
+    }
285
+
286
+    /**
287
+     * Synchronizes a change.
288
+     *
289
+     * @return array
290
+     */
291
+    public function Synchronize() {
292
+        if ($this->flags & BACKEND_DISCARD_DATA) {
293
+            SLog::Write(LOGLEVEL_WARN, 'ExportChangesICS->Synchronize(): not supported in combination with the BACKEND_DISCARD_DATA flag.');
294
+
295
+            return false;
296
+        }
297
+
298
+        if ($this->exporter) {
299
+            return mapi_exportchanges_synchronize($this->exporter);
300
+        }
301
+
302
+        return false;
303
+    }
304 304
 }
Please login to merge, or discard this patch.
Spacing   +2 added lines, -2 removed lines patch added patch discarded remove patch
@@ -106,7 +106,7 @@  discard block
 block discarded – undo
106 106
 
107 107
 		// change exporterflags if we are doing a ContentExport
108 108
 		if ($this->folderid) {
109
-			$this->exporterflags |= SYNC_NORMAL | SYNC_READ_STATE;
109
+			$this->exporterflags |= SYNC_NORMAL|SYNC_READ_STATE;
110 110
 
111 111
 			// Initial sync, we don't want deleted items. If the initial sync is chunked
112 112
 			// we check the change ID of the syncstate (0 at initial sync)
@@ -115,7 +115,7 @@  discard block
 block discarded – undo
115 115
 				if (!($this->flags & BACKEND_DISCARD_DATA)) {
116 116
 					SLog::Write(LOGLEVEL_DEBUG, "ExportChangesICS->Config(): syncing initial data");
117 117
 				}
118
-				$this->exporterflags |= SYNC_NO_SOFT_DELETIONS | SYNC_NO_DELETIONS;
118
+				$this->exporterflags |= SYNC_NO_SOFT_DELETIONS|SYNC_NO_DELETIONS;
119 119
 			}
120 120
 		}
121 121
 
Please login to merge, or discard this patch.
Braces   +7 added lines, -14 removed lines patch added patch discarded remove patch
@@ -45,13 +45,11 @@  discard block
 block discarded – undo
45 45
 		try {
46 46
 			if ($folderid) {
47 47
 				$entryid = mapi_msgstore_entryidfromsourcekey($store, $folderid);
48
-			}
49
-			else {
48
+			} else {
50 49
 				$storeprops = mapi_getprops($this->store, [PR_IPM_SUBTREE_ENTRYID, PR_IPM_PUBLIC_FOLDERS_ENTRYID]);
51 50
 				if (GSync::GetBackend()->GetImpersonatedUser() == 'system') {
52 51
 					$entryid = $storeprops[PR_IPM_PUBLIC_FOLDERS_ENTRYID];
53
-				}
54
-				else {
52
+				} else {
55 53
 					$entryid = $storeprops[PR_IPM_SUBTREE_ENTRYID];
56 54
 				}
57 55
 			}
@@ -68,16 +66,13 @@  discard block
 block discarded – undo
68 66
 			if ($folder) {
69 67
 				if ($folderid) {
70 68
 					$this->exporter = mapi_openproperty($folder, PR_CONTENTS_SYNCHRONIZER, IID_IExchangeExportChanges, 0, 0);
71
-				}
72
-				else {
69
+				} else {
73 70
 					$this->exporter = mapi_openproperty($folder, PR_HIERARCHY_SYNCHRONIZER, IID_IExchangeExportChanges, 0, 0);
74 71
 				}
75
-			}
76
-			else {
72
+			} else {
77 73
 				$this->exporter = false;
78 74
 			}
79
-		}
80
-		catch (MAPIException $me) {
75
+		} catch (MAPIException $me) {
81 76
 			$this->exporter = false;
82 77
 			// We return the general error SYNC_FSSTATUS_CODEUNKNOWN (12) which is also SYNC_STATUS_FOLDERHIERARCHYCHANGED (12)
83 78
 			// if this happened while doing content sync, the mobile will try to resync the folderhierarchy
@@ -201,8 +196,7 @@  discard block
 block discarded – undo
201 196
 			// ICS c++ wrapper
202 197
 			$mapiimporter = mapi_wrap_importcontentschanges($phpwrapper);
203 198
 			$includeprops = false;
204
-		}
205
-		else {
199
+		} else {
206 200
 			$mapiimporter = mapi_wrap_importhierarchychanges($phpwrapper);
207 201
 			$includeprops = [PR_SOURCE_KEY, PR_DISPLAY_NAME];
208 202
 		}
@@ -261,8 +255,7 @@  discard block
 block discarded – undo
261 255
 			$data = mapi_stream_read($this->statestream, 4096);
262 256
 			if (strlen($data)) {
263 257
 				$state .= $data;
264
-			}
265
-			else {
258
+			} else {
266 259
 				break;
267 260
 			}
268 261
 		}
Please login to merge, or discard this patch.
lib/grommunio/mapiprovider.php 3 patches
Indentation   +3146 added lines, -3146 removed lines patch added patch discarded remove patch
@@ -6,3159 +6,3159 @@
 block discarded – undo
6 6
  */
7 7
 
8 8
 class MAPIProvider {
9
-	private $session;
10
-	private $store;
11
-	private $zRFC822;
12
-	private $addressbook;
13
-	private $storeProps;
14
-	private $inboxProps;
15
-	private $rootProps;
16
-	private $specialFoldersData;
17
-
18
-	/**
19
-	 * Constructor of the MAPI Provider
20
-	 * Almost all methods of this class require a MAPI session and/or store.
21
-	 *
22
-	 * @param resource $session
23
-	 * @param resource $store
24
-	 */
25
-	public function __construct($session, $store) {
26
-		$this->session = $session;
27
-		$this->store = $store;
28
-	}
29
-
30
-	/*----------------------------------------------------------------------------------------------------------
9
+    private $session;
10
+    private $store;
11
+    private $zRFC822;
12
+    private $addressbook;
13
+    private $storeProps;
14
+    private $inboxProps;
15
+    private $rootProps;
16
+    private $specialFoldersData;
17
+
18
+    /**
19
+     * Constructor of the MAPI Provider
20
+     * Almost all methods of this class require a MAPI session and/or store.
21
+     *
22
+     * @param resource $session
23
+     * @param resource $store
24
+     */
25
+    public function __construct($session, $store) {
26
+        $this->session = $session;
27
+        $this->store = $store;
28
+    }
29
+
30
+    /*----------------------------------------------------------------------------------------------------------
31 31
 	 * GETTER
32 32
 	 */
33 33
 
34
-	/**
35
-	 * Reads a message from MAPI
36
-	 * Depending on the message class, a contact, appointment, task or email is read.
37
-	 *
38
-	 * @param mixed             $mapimessage
39
-	 * @param ContentParameters $contentparameters
40
-	 *
41
-	 * @return SyncObject
42
-	 */
43
-	public function GetMessage($mapimessage, $contentparameters) {
44
-		// Gets the Sync object from a MAPI object according to its message class
45
-
46
-		$props = mapi_getprops($mapimessage, [PR_MESSAGE_CLASS]);
47
-		if (isset($props[PR_MESSAGE_CLASS])) {
48
-			$messageclass = $props[PR_MESSAGE_CLASS];
49
-		}
50
-		else {
51
-			$messageclass = "IPM";
52
-		}
53
-
54
-		if (strpos($messageclass, "IPM.Contact") === 0) {
55
-			return $this->getContact($mapimessage, $contentparameters);
56
-		}
57
-		if (strpos($messageclass, "IPM.Appointment") === 0) {
58
-			return $this->getAppointment($mapimessage, $contentparameters);
59
-		}
60
-		if (strpos($messageclass, "IPM.Task") === 0 && strpos($messageclass, "IPM.TaskRequest") === false) {
61
-			return $this->getTask($mapimessage, $contentparameters);
62
-		}
63
-		if (strpos($messageclass, "IPM.StickyNote") === 0) {
64
-			return $this->getNote($mapimessage, $contentparameters);
65
-		}
66
-
67
-		return $this->getEmail($mapimessage, $contentparameters);
68
-	}
69
-
70
-	/**
71
-	 * Reads a contact object from MAPI.
72
-	 *
73
-	 * @param mixed             $mapimessage
74
-	 * @param ContentParameters $contentparameters
75
-	 *
76
-	 * @return SyncContact
77
-	 */
78
-	private function getContact($mapimessage, $contentparameters) {
79
-		$message = new SyncContact();
80
-
81
-		// Standard one-to-one mappings first
82
-		$this->getPropsFromMAPI($message, $mapimessage, MAPIMapping::GetContactMapping());
83
-
84
-		// Contact specific props
85
-		$contactproperties = MAPIMapping::GetContactProperties();
86
-		$messageprops = $this->getProps($mapimessage, $contactproperties);
87
-
88
-		// set the body according to contentparameters and supported AS version
89
-		$this->setMessageBody($mapimessage, $contentparameters, $message);
90
-
91
-		// check the picture
92
-		if (isset($messageprops[$contactproperties["haspic"]]) && $messageprops[$contactproperties["haspic"]]) {
93
-			// Add attachments
94
-			$attachtable = mapi_message_getattachmenttable($mapimessage);
95
-			mapi_table_restrict($attachtable, MAPIUtils::GetContactPicRestriction());
96
-			$rows = mapi_table_queryallrows($attachtable, [PR_ATTACH_NUM, PR_ATTACH_SIZE]);
97
-
98
-			foreach ($rows as $row) {
99
-				if (isset($row[PR_ATTACH_NUM])) {
100
-					$mapiattach = mapi_message_openattach($mapimessage, $row[PR_ATTACH_NUM]);
101
-					$message->picture = base64_encode(mapi_attach_openbin($mapiattach, PR_ATTACH_DATA_BIN));
102
-				}
103
-			}
104
-		}
105
-
106
-		return $message;
107
-	}
108
-
109
-	/**
110
-	 * Reads a task object from MAPI.
111
-	 *
112
-	 * @param mixed             $mapimessage
113
-	 * @param ContentParameters $contentparameters
114
-	 *
115
-	 * @return SyncTask
116
-	 */
117
-	private function getTask($mapimessage, $contentparameters) {
118
-		$message = new SyncTask();
119
-
120
-		// Standard one-to-one mappings first
121
-		$this->getPropsFromMAPI($message, $mapimessage, MAPIMapping::GetTaskMapping());
122
-
123
-		// Task specific props
124
-		$taskproperties = MAPIMapping::GetTaskProperties();
125
-		$messageprops = $this->getProps($mapimessage, $taskproperties);
126
-
127
-		// set the body according to contentparameters and supported AS version
128
-		$this->setMessageBody($mapimessage, $contentparameters, $message);
129
-
130
-		// task with deadoccur is an occurrence of a recurring task and does not need to be handled as recurring
131
-		// webaccess does not set deadoccur for the initial recurring task
132
-		if (isset($messageprops[$taskproperties["isrecurringtag"]]) &&
133
-			$messageprops[$taskproperties["isrecurringtag"]] &&
134
-			(!isset($messageprops[$taskproperties["deadoccur"]]) ||
135
-			(isset($messageprops[$taskproperties["deadoccur"]]) &&
136
-			!$messageprops[$taskproperties["deadoccur"]]))) {
137
-			// Process recurrence
138
-			$message->recurrence = new SyncTaskRecurrence();
139
-			$this->getRecurrence($mapimessage, $messageprops, $message, $message->recurrence, false);
140
-		}
141
-
142
-		// when set the task to complete using the WebAccess, the dateComplete property is not set correctly
143
-		if ($message->complete == 1 && !isset($message->datecompleted)) {
144
-			$message->datecompleted = time();
145
-		}
146
-
147
-		// if no reminder is set, announce that to the mobile
148
-		if (!isset($message->reminderset)) {
149
-			$message->reminderset = 0;
150
-		}
151
-
152
-		return $message;
153
-	}
154
-
155
-	/**
156
-	 * Reads an appointment object from MAPI.
157
-	 *
158
-	 * @param mixed             $mapimessage
159
-	 * @param ContentParameters $contentparameters
160
-	 *
161
-	 * @return SyncAppointment
162
-	 */
163
-	private function getAppointment($mapimessage, $contentparameters) {
164
-		$message = new SyncAppointment();
165
-
166
-		// Standard one-to-one mappings first
167
-		$this->getPropsFromMAPI($message, $mapimessage, MAPIMapping::GetAppointmentMapping());
168
-
169
-		// Appointment specific props
170
-		$appointmentprops = MAPIMapping::GetAppointmentProperties();
171
-		$messageprops = $this->getProps($mapimessage, $appointmentprops);
172
-
173
-		// set the body according to contentparameters and supported AS version
174
-		$this->setMessageBody($mapimessage, $contentparameters, $message);
175
-
176
-		// Set reminder time if reminderset is true
177
-		if (isset($messageprops[$appointmentprops["reminderset"]]) && $messageprops[$appointmentprops["reminderset"]] == true) {
178
-			if ($messageprops[$appointmentprops["remindertime"]] == 0x5AE980E1) {
179
-				$message->reminder = 15;
180
-			}
181
-			else {
182
-				$message->reminder = $messageprops[$appointmentprops["remindertime"]];
183
-			}
184
-		}
185
-
186
-		if (!isset($message->uid)) {
187
-			$message->uid = bin2hex($messageprops[$appointmentprops["sourcekey"]]);
188
-		}
189
-		else {
190
-			$message->uid = Utils::GetICalUidFromOLUid($message->uid);
191
-		}
192
-
193
-		// Always set organizer information because some devices do not work properly without it
194
-		if (isset($messageprops[$appointmentprops["representingentryid"]], $messageprops[$appointmentprops["representingname"]])
195
-			) {
196
-			$message->organizeremail = w2u($this->getSMTPAddressFromEntryID($messageprops[$appointmentprops["representingentryid"]]));
197
-			// if the email address can't be resolved, fall back to PR_SENT_REPRESENTING_SEARCH_KEY
198
-			if ($message->organizeremail == "" && isset($messageprops[$appointmentprops["sentrepresentinsrchk"]])) {
199
-				$message->organizeremail = $this->getEmailAddressFromSearchKey($messageprops[$appointmentprops["sentrepresentinsrchk"]]);
200
-			}
201
-			$message->organizername = w2u($messageprops[$appointmentprops["representingname"]]);
202
-		}
203
-
204
-		$appTz = false; // if the appointment has some timezone information saved on the server
205
-		if (!empty($messageprops[$appointmentprops["timezonetag"]])) {
206
-			$tz = $this->getTZFromMAPIBlob($messageprops[$appointmentprops["timezonetag"]]);
207
-			$appTz = true;
208
-		}
209
-		elseif (!empty($messageprops[$appointmentprops["timezonedesc"]])) {
210
-			// Windows uses UTC in timezone description in opposite to mstzones in TimezoneUtil which uses GMT
211
-			$wintz = str_replace("UTC", "GMT", $messageprops[$appointmentprops["timezonedesc"]]);
212
-			$tz = TimezoneUtil::GetFullTZFromTZName(TimezoneUtil::GetTZNameFromWinTZ($wintz));
213
-			$appTz = true;
214
-		}
215
-		else {
216
-			// set server default timezone (correct timezone should be configured!)
217
-			$tz = TimezoneUtil::GetFullTZ();
218
-		}
219
-
220
-		if (isset($messageprops[$appointmentprops["isrecurring"]]) && $messageprops[$appointmentprops["isrecurring"]]) {
221
-			// Process recurrence
222
-			$message->recurrence = new SyncRecurrence();
223
-			$this->getRecurrence($mapimessage, $messageprops, $message, $message->recurrence, $tz);
224
-
225
-			if (empty($message->alldayevent)) {
226
-				$message->timezone = base64_encode(TimezoneUtil::GetSyncBlobFromTZ($tz));
227
-			}
228
-		}
229
-
230
-		// Do attendees
231
-		$reciptable = mapi_message_getrecipienttable($mapimessage);
232
-		// Only get first 256 recipients, to prevent possible load issues.
233
-		$rows = mapi_table_queryrows($reciptable, [PR_DISPLAY_NAME, PR_EMAIL_ADDRESS, PR_SMTP_ADDRESS, PR_ADDRTYPE, PR_RECIPIENT_TRACKSTATUS, PR_RECIPIENT_TYPE, PR_SEARCH_KEY], 0, 256);
234
-
235
-		// Exception: we do not synchronize appointments with more than 250 attendees
236
-		if (count($rows) > 250) {
237
-			$message->id = bin2hex($messageprops[$appointmentprops["sourcekey"]]);
238
-			$mbe = new SyncObjectBrokenException("Appointment has too many attendees");
239
-			$mbe->SetSyncObject($message);
240
-
241
-			throw $mbe;
242
-		}
243
-
244
-		if (count($rows) > 0) {
245
-			$message->attendees = [];
246
-		}
247
-
248
-		foreach ($rows as $row) {
249
-			$attendee = new SyncAttendee();
250
-
251
-			$attendee->name = w2u($row[PR_DISPLAY_NAME]);
252
-			// smtp address is always a proper email address
253
-			if (isset($row[PR_SMTP_ADDRESS])) {
254
-				$attendee->email = w2u($row[PR_SMTP_ADDRESS]);
255
-			}
256
-			elseif (isset($row[PR_ADDRTYPE], $row[PR_EMAIL_ADDRESS])) {
257
-				// if address type is SMTP, it's also a proper email address
258
-				if ($row[PR_ADDRTYPE] == "SMTP") {
259
-					$attendee->email = w2u($row[PR_EMAIL_ADDRESS]);
260
-				}
261
-				// if address type is ZARAFA, the PR_EMAIL_ADDRESS contains username
262
-				elseif ($row[PR_ADDRTYPE] == "ZARAFA") {
263
-					$userinfo = @nsp_getuserinfo($row[PR_EMAIL_ADDRESS]);
264
-					if (is_array($userinfo) && isset($userinfo["primary_email"])) {
265
-						$attendee->email = w2u($userinfo["primary_email"]);
266
-					}
267
-					// if the user was not found, do a fallback to PR_SEARCH_KEY
268
-					// @see https://jira.z-hub.io/browse/ZP-1178
269
-					elseif (isset($row[PR_SEARCH_KEY])) {
270
-						$attendee->email = w2u($this->getEmailAddressFromSearchKey($row[PR_SEARCH_KEY]));
271
-					}
272
-					else {
273
-						SLog::Write(LOGLEVEL_WARN, sprintf("MAPIProvider->getAppointment: The attendee '%s' of type ZARAFA can not be resolved. Code: 0x%X", $row[PR_EMAIL_ADDRESS], mapi_last_hresult()));
274
-					}
275
-				}
276
-			}
277
-
278
-			// set attendee's status and type if they're available and if we are the organizer
279
-			$storeprops = $this->GetStoreProps();
280
-			if (isset($row[PR_RECIPIENT_TRACKSTATUS], $messageprops[$appointmentprops["representingentryid"]], $storeprops[PR_MAILBOX_OWNER_ENTRYID]) &&
281
-					$messageprops[$appointmentprops["representingentryid"]] == $storeprops[PR_MAILBOX_OWNER_ENTRYID]) {
282
-				$attendee->attendeestatus = $row[PR_RECIPIENT_TRACKSTATUS];
283
-			}
284
-			if (isset($row[PR_RECIPIENT_TYPE])) {
285
-				$attendee->attendeetype = $row[PR_RECIPIENT_TYPE];
286
-			}
287
-			// Some attendees have no email or name (eg resources), and if you
288
-			// don't send one of those fields, the phone will give an error ... so
289
-			// we don't send it in that case.
290
-			// also ignore the "attendee" if the email is equal to the organizers' email
291
-			if (isset($attendee->name, $attendee->email) && $attendee->email != "" && (!isset($message->organizeremail) || (isset($message->organizeremail) && $attendee->email != $message->organizeremail))) {
292
-				array_push($message->attendees, $attendee);
293
-			}
294
-		}
295
-
296
-		// Status 0 = no meeting, status 1 = organizer, status 2/3/4/5 = tentative/accepted/declined/notresponded
297
-		if (isset($messageprops[$appointmentprops["meetingstatus"]]) && $messageprops[$appointmentprops["meetingstatus"]] > 1) {
298
-			if (!isset($message->attendees) || !is_array($message->attendees)) {
299
-				$message->attendees = [];
300
-			}
301
-			// Work around iOS6 cancellation issue when there are no attendees for this meeting. Just add ourselves as the sole attendee.
302
-			if (count($message->attendees) == 0) {
303
-				SLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->getAppointment: adding ourself as an attendee for iOS6 workaround"));
304
-				$attendee = new SyncAttendee();
305
-
306
-				$meinfo = nsp_getuserinfo(Request::GetUser());
307
-
308
-				if (is_array($meinfo)) {
309
-					$attendee->email = w2u($meinfo["primary_email"]);
310
-					$attendee->name = w2u($meinfo["fullname"]);
311
-					$attendee->attendeetype = MAPI_TO;
312
-
313
-					array_push($message->attendees, $attendee);
314
-				}
315
-			}
316
-			$message->responsetype = $messageprops[$appointmentprops["responsestatus"]];
317
-		}
318
-
319
-		// If it's an appointment which doesn't have any attendees, we have to make sure that
320
-		// the user is the owner or it will not work properly with android devices
321
-		// @see https://jira.z-hub.io/browse/ZP-1020
322
-		if (isset($messageprops[$appointmentprops["meetingstatus"]]) && $messageprops[$appointmentprops["meetingstatus"]] == olNonMeeting && empty($message->attendees)) {
323
-			$meinfo = nsp_getuserinfo(Request::GetUser());
324
-
325
-			if (is_array($meinfo)) {
326
-				$message->organizeremail = w2u($meinfo["primary_email"]);
327
-				$message->organizername = w2u($meinfo["fullname"]);
328
-				SLog::Write(LOGLEVEL_DEBUG, "MAPIProvider->getAppointment(): setting ourself as the organizer for an appointment without attendees.");
329
-			}
330
-		}
331
-
332
-		if (!isset($message->nativebodytype)) {
333
-			$message->nativebodytype = MAPIUtils::GetNativeBodyType($messageprops);
334
-		}
335
-		elseif ($message->nativebodytype == SYNC_BODYPREFERENCE_UNDEFINED) {
336
-			$nbt = MAPIUtils::GetNativeBodyType($messageprops);
337
-			SLog::Write(LOGLEVEL_INFO, sprintf("MAPIProvider->getAppointment(): native body type is undefined. Set it to %d.", $nbt));
338
-			$message->nativebodytype = $nbt;
339
-		}
340
-
341
-		// If the user is working from a location other than the office the busystatus should be interpreted as free.
342
-		if (isset($message->busystatus) && $message->busystatus == fbWorkingElsewhere) {
343
-			$message->busystatus = fbFree;
344
-		}
345
-
346
-		// If the busystatus has the value of -1, we should be interpreted as tentative (1) / ZP-581
347
-		if (isset($message->busystatus) && $message->busystatus == -1) {
348
-			$message->busystatus = fbTentative;
349
-		}
350
-
351
-		// All-day events might appear as 24h (or multiple of it) long when they start not exactly at midnight (+/- bias of the timezone)
352
-		if (isset($message->alldayevent) && $message->alldayevent) {
353
-			$localStartTime = localtime($message->starttime, 1);
354
-
355
-			// The appointment is all-day but doesn't start at midnight.
356
-			// If it was created in another timezone and we have that information,
357
-			// set the startime to the midnight of the current timezone.
358
-			if ($appTz && ($localStartTime['tm_hour'] || $localStartTime['tm_min'])) {
359
-				SLog::Write(LOGLEVEL_DEBUG, "MAPIProvider->getAppointment(): all-day event starting not midnight.");
360
-				$duration = $message->endtime - $message->starttime;
361
-				$serverTz = TimezoneUtil::GetFullTZ();
362
-				$message->starttime = $this->getGMTTimeByTZ($this->getLocaltimeByTZ($message->starttime, $tz), $serverTz);
363
-				$message->endtime = $message->starttime + $duration;
364
-			}
365
-		}
366
-
367
-		return $message;
368
-	}
369
-
370
-	/**
371
-	 * Reads recurrence information from MAPI.
372
-	 *
373
-	 * @param mixed      $mapimessage
374
-	 * @param array      $recurprops
375
-	 * @param SyncObject &$syncMessage    the message
376
-	 * @param SyncObject &$syncRecurrence the  recurrence message
377
-	 * @param array      $tz              timezone information
378
-	 *
379
-	 * @return
380
-	 */
381
-	private function getRecurrence($mapimessage, $recurprops, &$syncMessage, &$syncRecurrence, $tz) {
382
-		if ($syncRecurrence instanceof SyncTaskRecurrence) {
383
-			$recurrence = new TaskRecurrence($this->store, $mapimessage);
384
-		}
385
-		else {
386
-			$recurrence = new Recurrence($this->store, $mapimessage);
387
-		}
388
-
389
-		switch ($recurrence->recur["type"]) {
390
-			case 10: // daily
391
-				switch ($recurrence->recur["subtype"]) {
392
-					default:
393
-						$syncRecurrence->type = 0;
394
-						break;
395
-
396
-					case 1:
397
-						$syncRecurrence->type = 0;
398
-						$syncRecurrence->dayofweek = 62; // mon-fri
399
-						$syncRecurrence->interval = 1;
400
-						break;
401
-				}
402
-				break;
403
-
404
-			case 11: // weekly
405
-				$syncRecurrence->type = 1;
406
-				break;
407
-
408
-			case 12: // monthly
409
-				switch ($recurrence->recur["subtype"]) {
410
-					default:
411
-						$syncRecurrence->type = 2;
412
-						break;
413
-
414
-					case 3:
415
-						$syncRecurrence->type = 3;
416
-						break;
417
-				}
418
-				break;
419
-
420
-			case 13: // yearly
421
-				switch ($recurrence->recur["subtype"]) {
422
-					default:
423
-						$syncRecurrence->type = 4;
424
-						break;
425
-
426
-					case 2:
427
-						$syncRecurrence->type = 5;
428
-						break;
429
-
430
-					case 3:
431
-						$syncRecurrence->type = 6;
432
-						break;
433
-				}
434
-		}
435
-		// Termination
436
-		switch ($recurrence->recur["term"]) {
437
-			case 0x21:
438
-				$syncRecurrence->until = $recurrence->recur["end"];
439
-				// fixes Mantis #350 : recur-end does not consider timezones - use ClipEnd if available
440
-				if (isset($recurprops[$recurrence->proptags["enddate_recurring"]])) {
441
-					$syncRecurrence->until = $recurprops[$recurrence->proptags["enddate_recurring"]];
442
-				}
443
-				// add one day (minus 1 sec) to the end time to make sure the last occurrence is covered
444
-				$syncRecurrence->until += 86399;
445
-				break;
446
-
447
-			case 0x22:
448
-				$syncRecurrence->occurrences = $recurrence->recur["numoccur"];
449
-				break;
450
-
451
-			case 0x23:
452
-				// never ends
453
-				break;
454
-		}
455
-
456
-		// Correct 'alldayevent' because outlook fails to set it on recurring items of 24 hours or longer
457
-		if (isset($recurrence->recur["endocc"], $recurrence->recur["startocc"]) && ($recurrence->recur["endocc"] - $recurrence->recur["startocc"] >= 1440)) {
458
-			$syncMessage->alldayevent = true;
459
-		}
460
-
461
-		// Interval is different according to the type/subtype
462
-		switch ($recurrence->recur["type"]) {
463
-			case 10:
464
-				if ($recurrence->recur["subtype"] == 0) {
465
-					$syncRecurrence->interval = (int) ($recurrence->recur["everyn"] / 1440);
466
-				}  // minutes
467
-				break;
468
-
469
-			case 11:
470
-			case 12:
471
-				$syncRecurrence->interval = $recurrence->recur["everyn"];
472
-				break; // months / weeks
473
-
474
-			case 13:
475
-				$syncRecurrence->interval = (int) ($recurrence->recur["everyn"] / 12);
476
-				break; // months
477
-		}
478
-
479
-		if (isset($recurrence->recur["weekdays"])) {
480
-			$syncRecurrence->dayofweek = $recurrence->recur["weekdays"];
481
-		} // bitmask of days (1 == sunday, 128 == saturday
482
-		if (isset($recurrence->recur["nday"])) {
483
-			$syncRecurrence->weekofmonth = $recurrence->recur["nday"];
484
-		} // N'th {DAY} of {X} (0-5)
485
-		if (isset($recurrence->recur["month"])) {
486
-			$syncRecurrence->monthofyear = (int) ($recurrence->recur["month"] / (60 * 24 * 29)) + 1;
487
-		} // works ok due to rounding. see also $monthminutes below (1-12)
488
-		if (isset($recurrence->recur["monthday"])) {
489
-			$syncRecurrence->dayofmonth = $recurrence->recur["monthday"];
490
-		} // day of month (1-31)
491
-
492
-		// All changed exceptions are appointments within the 'exceptions' array. They contain the same items as a normal appointment
493
-		foreach ($recurrence->recur["changed_occurrences"] as $change) {
494
-			$exception = new SyncAppointmentException();
495
-
496
-			// start, end, basedate, subject, remind_before, reminderset, location, busystatus, alldayevent, label
497
-			if (isset($change["start"])) {
498
-				$exception->starttime = $this->getGMTTimeByTZ($change["start"], $tz);
499
-			}
500
-			if (isset($change["end"])) {
501
-				$exception->endtime = $this->getGMTTimeByTZ($change["end"], $tz);
502
-			}
503
-			if (isset($change["basedate"])) {
504
-				$exception->exceptionstarttime = $this->getGMTTimeByTZ($this->getDayStartOfTimestamp($change["basedate"]) + $recurrence->recur["startocc"] * 60, $tz);
505
-
506
-				// open body because getting only property might not work because of memory limit
507
-				$exceptionatt = $recurrence->getExceptionAttachment($change["basedate"]);
508
-				if ($exceptionatt) {
509
-					$exceptionobj = mapi_attach_openobj($exceptionatt, 0);
510
-					$this->setMessageBodyForType($exceptionobj, SYNC_BODYPREFERENCE_PLAIN, $exception);
511
-				}
512
-			}
513
-			if (isset($change["subject"])) {
514
-				$exception->subject = w2u($change["subject"]);
515
-			}
516
-			if (isset($change["reminder_before"]) && $change["reminder_before"]) {
517
-				$exception->reminder = $change["remind_before"];
518
-			}
519
-			if (isset($change["location"])) {
520
-				$exception->location = w2u($change["location"]);
521
-			}
522
-			if (isset($change["busystatus"])) {
523
-				$exception->busystatus = $change["busystatus"];
524
-			}
525
-			if (isset($change["alldayevent"])) {
526
-				$exception->alldayevent = $change["alldayevent"];
527
-			}
528
-
529
-			// set some data from the original appointment
530
-			if (isset($syncMessage->uid)) {
531
-				$exception->uid = $syncMessage->uid;
532
-			}
533
-			if (isset($syncMessage->organizername)) {
534
-				$exception->organizername = $syncMessage->organizername;
535
-			}
536
-			if (isset($syncMessage->organizeremail)) {
537
-				$exception->organizeremail = $syncMessage->organizeremail;
538
-			}
539
-
540
-			if (!isset($syncMessage->exceptions)) {
541
-				$syncMessage->exceptions = [];
542
-			}
543
-
544
-			// If the user is working from a location other than the office the busystatus should be interpreted as free.
545
-			if (isset($exception->busystatus) && $exception->busystatus == fbWorkingElsewhere) {
546
-				$exception->busystatus = fbFree;
547
-			}
548
-
549
-			// If the busystatus has the value of -1, we should be interpreted as tentative (1) / ZP-581
550
-			if (isset($exception->busystatus) && $exception->busystatus == -1) {
551
-				$exception->busystatus = fbTentative;
552
-			}
553
-
554
-			// if an exception lasts 24 hours and the series are an allday events, set also the exception to allday event,
555
-			// otherwise it will be a 24 hour long event on some mobiles.
556
-			// @see https://jira.z-hub.io/browse/ZP-980
557
-			if (isset($exception->starttime, $exception->endtime) && ($exception->endtime - $exception->starttime == 86400) && $syncMessage->alldayevent) {
558
-				$exception->alldayevent = 1;
559
-			}
560
-			array_push($syncMessage->exceptions, $exception);
561
-		}
562
-
563
-		// Deleted appointments contain only the original date (basedate) and a 'deleted' tag
564
-		foreach ($recurrence->recur["deleted_occurrences"] as $deleted) {
565
-			$exception = new SyncAppointmentException();
566
-
567
-			$exception->exceptionstarttime = $this->getGMTTimeByTZ($this->getDayStartOfTimestamp($deleted) + $recurrence->recur["startocc"] * 60, $tz);
568
-			$exception->deleted = "1";
569
-
570
-			if (!isset($syncMessage->exceptions)) {
571
-				$syncMessage->exceptions = [];
572
-			}
573
-
574
-			array_push($syncMessage->exceptions, $exception);
575
-		}
576
-
577
-		if (isset($syncMessage->complete) && $syncMessage->complete) {
578
-			$syncRecurrence->complete = $syncMessage->complete;
579
-		}
580
-	}
581
-
582
-	/**
583
-	 * Reads an email object from MAPI.
584
-	 *
585
-	 * @param mixed             $mapimessage
586
-	 * @param ContentParameters $contentparameters
587
-	 *
588
-	 * @return SyncEmail
589
-	 */
590
-	private function getEmail($mapimessage, $contentparameters) {
591
-		// This workaround fixes ZP-729 and still works with Outlook.
592
-		// FIXME: It should be properly fixed when refactoring.
593
-		$bpReturnType = Utils::GetBodyPreferenceBestMatch($contentparameters->GetBodyPreference());
594
-		if (($contentparameters->GetMimeSupport() == SYNC_MIMESUPPORT_NEVER) ||
595
-				($key = array_search(SYNC_BODYPREFERENCE_MIME, $contentparameters->GetBodyPreference()) === false) ||
596
-				$bpReturnType != SYNC_BODYPREFERENCE_MIME) {
597
-			MAPIUtils::ParseSmime($this->session, $this->store, $this->getAddressbook(), $mapimessage);
598
-		}
599
-
600
-		$message = new SyncMail();
601
-
602
-		$this->getPropsFromMAPI($message, $mapimessage, MAPIMapping::GetEmailMapping());
603
-
604
-		$emailproperties = MAPIMapping::GetEmailProperties();
605
-		$messageprops = $this->getProps($mapimessage, $emailproperties);
606
-
607
-		if (isset($messageprops[PR_SOURCE_KEY])) {
608
-			$sourcekey = $messageprops[PR_SOURCE_KEY];
609
-		}
610
-		else {
611
-			$mbe = new SyncObjectBrokenException("The message doesn't have a sourcekey");
612
-			$mbe->SetSyncObject($message);
613
-
614
-			throw $mbe;
615
-		}
616
-
617
-		// set the body according to contentparameters and supported AS version
618
-		$this->setMessageBody($mapimessage, $contentparameters, $message);
619
-
620
-		$fromname = $fromaddr = "";
621
-
622
-		if (isset($messageprops[$emailproperties["representingname"]])) {
623
-			// remove encapsulating double quotes from the representingname
624
-			$fromname = preg_replace('/^\"(.*)\"$/', "\${1}", $messageprops[$emailproperties["representingname"]]);
625
-		}
626
-		if (isset($messageprops[$emailproperties["representingentryid"]])) {
627
-			$fromaddr = $this->getSMTPAddressFromEntryID($messageprops[$emailproperties["representingentryid"]]);
628
-		}
629
-
630
-		// if the email address can't be resolved, fall back to PR_SENT_REPRESENTING_SEARCH_KEY
631
-		if ($fromaddr == "" && isset($messageprops[$emailproperties["representingsearchkey"]])) {
632
-			$fromaddr = $this->getEmailAddressFromSearchKey($messageprops[$emailproperties["representingsearchkey"]]);
633
-		}
634
-
635
-		if ($fromname == $fromaddr) {
636
-			$fromname = "";
637
-		}
638
-
639
-		if ($fromname) {
640
-			$from = "\"" . w2u($fromname) . "\" <" . w2u($fromaddr) . ">";
641
-		}
642
-		else { // START CHANGED dw2412 HTC shows "error" if sender name is unknown
643
-			$from = "\"" . w2u($fromaddr) . "\" <" . w2u($fromaddr) . ">";
644
-		}
645
-		// END CHANGED dw2412 HTC shows "error" if sender name is unknown
646
-
647
-		$message->from = $from;
648
-
649
-		// process Meeting Requests
650
-		if (isset($message->messageclass) && strpos($message->messageclass, "IPM.Schedule.Meeting") === 0) {
651
-			$message->meetingrequest = new SyncMeetingRequest();
652
-			$this->getPropsFromMAPI($message->meetingrequest, $mapimessage, MAPIMapping::GetMeetingRequestMapping());
653
-
654
-			$meetingrequestproperties = MAPIMapping::GetMeetingRequestProperties();
655
-			$props = $this->getProps($mapimessage, $meetingrequestproperties);
656
-
657
-			// Get the GOID
658
-			if (isset($props[$meetingrequestproperties["goidtag"]])) {
659
-				$message->meetingrequest->globalobjid = base64_encode($props[$meetingrequestproperties["goidtag"]]);
660
-			}
661
-
662
-			// Set Timezone
663
-			if (isset($props[$meetingrequestproperties["timezonetag"]])) {
664
-				$tz = $this->getTZFromMAPIBlob($props[$meetingrequestproperties["timezonetag"]]);
665
-			}
666
-			else {
667
-				$tz = TimezoneUtil::GetFullTZ();
668
-			}
669
-
670
-			$message->meetingrequest->timezone = base64_encode(TimezoneUtil::GetSyncBlobFromTZ($tz));
671
-
672
-			// send basedate if exception
673
-			if (isset($props[$meetingrequestproperties["recReplTime"]]) ||
674
-				(isset($props[$meetingrequestproperties["lidIsException"]]) && $props[$meetingrequestproperties["lidIsException"]] == true)) {
675
-				if (isset($props[$meetingrequestproperties["recReplTime"]])) {
676
-					$basedate = $props[$meetingrequestproperties["recReplTime"]];
677
-					$message->meetingrequest->recurrenceid = $this->getGMTTimeByTZ($basedate, $this->getGMTTZ());
678
-				}
679
-				else {
680
-					if (!isset($props[$meetingrequestproperties["goidtag"]]) || !isset($props[$meetingrequestproperties["recurStartTime"]]) || !isset($props[$meetingrequestproperties["timezonetag"]])) {
681
-						SLog::Write(LOGLEVEL_WARN, "Missing property to set correct basedate for exception");
682
-					}
683
-					else {
684
-						$basedate = Utils::ExtractBaseDate($props[$meetingrequestproperties["goidtag"]], $props[$meetingrequestproperties["recurStartTime"]]);
685
-						$message->meetingrequest->recurrenceid = $this->getGMTTimeByTZ($basedate, $tz);
686
-					}
687
-				}
688
-			}
689
-
690
-			// Organizer is the sender
691
-			if (strpos($message->messageclass, "IPM.Schedule.Meeting.Resp") === 0) {
692
-				$message->meetingrequest->organizer = $message->to;
693
-			}
694
-			else {
695
-				$message->meetingrequest->organizer = $message->from;
696
-			}
697
-
698
-			// Process recurrence
699
-			if (isset($props[$meetingrequestproperties["isrecurringtag"]]) && $props[$meetingrequestproperties["isrecurringtag"]]) {
700
-				$myrec = new SyncMeetingRequestRecurrence();
701
-				// get recurrence -> put $message->meetingrequest as message so the 'alldayevent' is set correctly
702
-				$this->getRecurrence($mapimessage, $props, $message->meetingrequest, $myrec, $tz);
703
-				$message->meetingrequest->recurrences = [$myrec];
704
-			}
705
-
706
-			// Force the 'alldayevent' in the object at all times. (non-existent == 0)
707
-			if (!isset($message->meetingrequest->alldayevent) || $message->meetingrequest->alldayevent == "") {
708
-				$message->meetingrequest->alldayevent = 0;
709
-			}
710
-
711
-			// Instancetype
712
-			// 0 = single appointment
713
-			// 1 = master recurring appointment
714
-			// 2 = single instance of recurring appointment
715
-			// 3 = exception of recurring appointment
716
-			$message->meetingrequest->instancetype = 0;
717
-			if (isset($props[$meetingrequestproperties["isrecurringtag"]]) && $props[$meetingrequestproperties["isrecurringtag"]] == 1) {
718
-				$message->meetingrequest->instancetype = 1;
719
-			}
720
-			elseif ((!isset($props[$meetingrequestproperties["isrecurringtag"]]) || $props[$meetingrequestproperties["isrecurringtag"]] == 0) && isset($message->meetingrequest->recurrenceid)) {
721
-				if (isset($props[$meetingrequestproperties["appSeqNr"]]) && $props[$meetingrequestproperties["appSeqNr"]] == 0) {
722
-					$message->meetingrequest->instancetype = 2;
723
-				}
724
-				else {
725
-					$message->meetingrequest->instancetype = 3;
726
-				}
727
-			}
728
-
729
-			// Disable reminder if it is off
730
-			if (!isset($props[$meetingrequestproperties["reminderset"]]) || $props[$meetingrequestproperties["reminderset"]] == false) {
731
-				$message->meetingrequest->reminder = "";
732
-			}
733
-			// the property saves reminder in minutes, but we need it in secs
734
-			else {
735
-				// /set the default reminder time to seconds
736
-				if ($props[$meetingrequestproperties["remindertime"]] == 0x5AE980E1) {
737
-					$message->meetingrequest->reminder = 900;
738
-				}
739
-				else {
740
-					$message->meetingrequest->reminder = $props[$meetingrequestproperties["remindertime"]] * 60;
741
-				}
742
-			}
743
-
744
-			// Set sensitivity to 0 if missing
745
-			if (!isset($message->meetingrequest->sensitivity)) {
746
-				$message->meetingrequest->sensitivity = 0;
747
-			}
748
-
749
-			// If the user is working from a location other than the office the busystatus should be interpreted as free.
750
-			if (isset($message->meetingrequest->busystatus) && $message->meetingrequest->busystatus == fbWorkingElsewhere) {
751
-				$message->meetingrequest->busystatus = fbFree;
752
-			}
753
-
754
-			// If the busystatus has the value of -1, we should be interpreted as tentative (1) / ZP-581
755
-			if (isset($message->meetingrequest->busystatus) && $message->meetingrequest->busystatus == -1) {
756
-				$message->meetingrequest->busystatus = fbTentative;
757
-			}
758
-
759
-			// if a meeting request response hasn't been processed yet,
760
-			// do it so that the attendee status is updated on the mobile
761
-			if (!isset($messageprops[$emailproperties["processed"]])) {
762
-				// check if we are not sending the MR so we can process it - ZP-581
763
-				$cuser = GSync::GetBackend()->GetUserDetails(GSync::GetBackend()->GetCurrentUsername());
764
-				if (isset($cuser["emailaddress"]) && $cuser["emailaddress"] != $fromaddr) {
765
-					if (!isset($req)) {
766
-						$req = new Meetingrequest($this->store, $mapimessage, $this->session);
767
-					}
768
-					if ($req->isMeetingRequestResponse()) {
769
-						$req->processMeetingRequestResponse();
770
-					}
771
-					if ($req->isMeetingCancellation()) {
772
-						$req->processMeetingCancellation();
773
-					}
774
-				}
775
-			}
776
-			$message->contentclass = DEFAULT_CALENDAR_CONTENTCLASS;
777
-
778
-			// MeetingMessageType values
779
-			// 0 = A silent update was performed, or the message type is unspecified.
780
-			// 1 = Initial meeting request.
781
-			// 2 = Full update.
782
-			// 3 = Informational update.
783
-			// 4 = Outdated. A newer meeting request or meeting update was received after this message.
784
-			// 5 = Identifies the delegator's copy of the meeting request.
785
-			// 6 = Identifies that the meeting request has been delegated and the meeting request cannot be responded to.
786
-			$message->meetingrequest->meetingmessagetype = mtgEmpty;
787
-
788
-			if (isset($props[$meetingrequestproperties["meetingType"]])) {
789
-				switch ($props[$meetingrequestproperties["meetingType"]]) {
790
-					case mtgRequest:
791
-						$message->meetingrequest->meetingmessagetype = 1;
792
-						break;
793
-
794
-					case mtgFull:
795
-						$message->meetingrequest->meetingmessagetype = 2;
796
-						break;
797
-
798
-					case mtgInfo:
799
-						$message->meetingrequest->meetingmessagetype = 3;
800
-						break;
801
-
802
-					case mtgOutOfDate:
803
-						$message->meetingrequest->meetingmessagetype = 4;
804
-						break;
805
-
806
-					case mtgDelegatorCopy:
807
-						$message->meetingrequest->meetingmessagetype = 5;
808
-						break;
809
-				}
810
-			}
811
-		}
812
-
813
-		// Add attachments
814
-		$attachtable = mapi_message_getattachmenttable($mapimessage);
815
-		$rows = mapi_table_queryallrows($attachtable, [PR_ATTACH_NUM]);
816
-		$entryid = bin2hex($messageprops[$emailproperties["entryid"]]);
817
-		$parentSourcekey = bin2hex($messageprops[$emailproperties["parentsourcekey"]]);
818
-
819
-		foreach ($rows as $row) {
820
-			if (isset($row[PR_ATTACH_NUM])) {
821
-				if (Request::GetProtocolVersion() >= 12.0) {
822
-					$attach = new SyncBaseAttachment();
823
-				}
824
-				else {
825
-					$attach = new SyncAttachment();
826
-				}
827
-
828
-				$mapiattach = mapi_message_openattach($mapimessage, $row[PR_ATTACH_NUM]);
829
-				$attachprops = mapi_getprops($mapiattach, [PR_ATTACH_LONG_FILENAME, PR_ATTACH_FILENAME, PR_ATTACHMENT_HIDDEN, PR_ATTACH_CONTENT_ID, PR_ATTACH_CONTENT_ID_W, PR_ATTACH_MIME_TAG, PR_ATTACH_MIME_TAG_W, PR_ATTACH_METHOD, PR_DISPLAY_NAME, PR_DISPLAY_NAME_W, PR_ATTACH_SIZE, PR_ATTACH_FLAGS]);
830
-				if ((isset($attachprops[PR_ATTACH_MIME_TAG]) && strpos(strtolower($attachprops[PR_ATTACH_MIME_TAG]), 'signed') !== false) ||
831
-					(isset($attachprops[PR_ATTACH_MIME_TAG_W]) && strpos(strtolower($attachprops[PR_ATTACH_MIME_TAG_W]), 'signed') !== false)) {
832
-					continue;
833
-				}
834
-
835
-				// the displayname is handled equally for all AS versions
836
-				$attach->displayname = w2u((isset($attachprops[PR_ATTACH_LONG_FILENAME])) ? $attachprops[PR_ATTACH_LONG_FILENAME] : ((isset($attachprops[PR_ATTACH_FILENAME])) ? $attachprops[PR_ATTACH_FILENAME] : ((isset($attachprops[PR_DISPLAY_NAME])) ? $attachprops[PR_DISPLAY_NAME] : "attachment.bin")));
837
-				// fix attachment name in case of inline images
838
-				if (($attach->displayname == "inline.txt" && (isset($attachprops[PR_ATTACH_MIME_TAG]) || $attachprops[PR_ATTACH_MIME_TAG_W])) ||
839
-				(substr_compare($attach->displayname, "attachment", 0, 10, true) === 0 && substr_compare($attach->displayname, ".dat", -4, 4, true) === 0)) {
840
-					$mimetype = (isset($attachprops[PR_ATTACH_MIME_TAG])) ? $attachprops[PR_ATTACH_MIME_TAG] : $attachprops[PR_ATTACH_MIME_TAG_W];
841
-					$mime = explode("/", $mimetype);
842
-
843
-					if (count($mime) == 2 && $mime[0] == "image") {
844
-						$attach->displayname = "inline." . $mime[1];
845
-					}
846
-				}
847
-
848
-				// set AS version specific parameters
849
-				if (Request::GetProtocolVersion() >= 12.0) {
850
-					$attach->filereference = sprintf("%s:%s:%s", $entryid, $row[PR_ATTACH_NUM], $parentSourcekey);
851
-					$attach->method = (isset($attachprops[PR_ATTACH_METHOD])) ? $attachprops[PR_ATTACH_METHOD] : ATTACH_BY_VALUE;
852
-
853
-					// if displayname does not have the eml extension for embedde messages, android and WP devices won't open it
854
-					if ($attach->method == ATTACH_EMBEDDED_MSG) {
855
-						if (strtolower(substr($attach->displayname, -4)) != '.eml') {
856
-							$attach->displayname .= '.eml';
857
-						}
858
-					}
859
-					// android devices require attachment size in order to display an attachment properly
860
-					if (!isset($attachprops[PR_ATTACH_SIZE])) {
861
-						$stream = mapi_openproperty($mapiattach, PR_ATTACH_DATA_BIN, IID_IStream, 0, 0);
862
-						// It's not possible to open some (embedded only?) messages, so we need to open the attachment object itself to get the data
863
-						if (mapi_last_hresult()) {
864
-							$embMessage = mapi_attach_openobj($mapiattach);
865
-							$addrbook = $this->getAddressbook();
866
-							$stream = mapi_inetmapi_imtoinet($this->session, $addrbook, $embMessage, ['use_tnef' => -1]);
867
-						}
868
-						$stat = mapi_stream_stat($stream);
869
-						$attach->estimatedDataSize = $stat['cb'];
870
-					}
871
-					else {
872
-						$attach->estimatedDataSize = $attachprops[PR_ATTACH_SIZE];
873
-					}
874
-
875
-					if (isset($attachprops[PR_ATTACH_CONTENT_ID]) && $attachprops[PR_ATTACH_CONTENT_ID]) {
876
-						$attach->contentid = $attachprops[PR_ATTACH_CONTENT_ID];
877
-					}
878
-
879
-					if (!isset($attach->contentid) && isset($attachprops[PR_ATTACH_CONTENT_ID_W]) && $attachprops[PR_ATTACH_CONTENT_ID_W]) {
880
-						$attach->contentid = $attachprops[PR_ATTACH_CONTENT_ID_W];
881
-					}
882
-
883
-					if (isset($attachprops[PR_ATTACHMENT_HIDDEN]) && $attachprops[PR_ATTACHMENT_HIDDEN]) {
884
-						$attach->isinline = 1;
885
-					}
886
-
887
-					if (isset($attach->contentid, $attachprops[PR_ATTACH_FLAGS]) && $attachprops[PR_ATTACH_FLAGS] & 4) {
888
-						$attach->isinline = 1;
889
-					}
890
-
891
-					if (!isset($message->asattachments)) {
892
-						$message->asattachments = [];
893
-					}
894
-
895
-					array_push($message->asattachments, $attach);
896
-				}
897
-				else {
898
-					$attach->attsize = $attachprops[PR_ATTACH_SIZE];
899
-					$attach->attname = sprintf("%s:%s:%s", $entryid, $row[PR_ATTACH_NUM], $parentSourcekey);
900
-					if (!isset($message->attachments)) {
901
-						$message->attachments = [];
902
-					}
903
-
904
-					array_push($message->attachments, $attach);
905
-				}
906
-			}
907
-		}
908
-
909
-		// Get To/Cc as SMTP addresses (this is different from displayto and displaycc because we are putting
910
-		// in the SMTP addresses as well, while displayto and displaycc could just contain the display names
911
-		$message->to = [];
912
-		$message->cc = [];
913
-
914
-		$reciptable = mapi_message_getrecipienttable($mapimessage);
915
-		$rows = mapi_table_queryallrows($reciptable, [PR_RECIPIENT_TYPE, PR_DISPLAY_NAME, PR_ADDRTYPE, PR_EMAIL_ADDRESS, PR_SMTP_ADDRESS, PR_ENTRYID, PR_SEARCH_KEY]);
916
-
917
-		foreach ($rows as $row) {
918
-			$address = "";
919
-			$fulladdr = "";
920
-
921
-			$addrtype = isset($row[PR_ADDRTYPE]) ? $row[PR_ADDRTYPE] : "";
922
-
923
-			if (isset($row[PR_SMTP_ADDRESS])) {
924
-				$address = $row[PR_SMTP_ADDRESS];
925
-			}
926
-			elseif ($addrtype == "SMTP" && isset($row[PR_EMAIL_ADDRESS])) {
927
-				$address = $row[PR_EMAIL_ADDRESS];
928
-			}
929
-			elseif ($addrtype == "ZARAFA" && isset($row[PR_ENTRYID])) {
930
-				$address = $this->getSMTPAddressFromEntryID($row[PR_ENTRYID]);
931
-			}
932
-
933
-			// if the user was not found, do a fallback to PR_SEARCH_KEY
934
-			// @see https://jira.z-hub.io/browse/ZP-1178
935
-			if (empty($address) && isset($row[PR_SEARCH_KEY])) {
936
-				$address = $this->getEmailAddressFromSearchKey($row[PR_SEARCH_KEY]);
937
-			}
938
-
939
-			$name = isset($row[PR_DISPLAY_NAME]) ? $row[PR_DISPLAY_NAME] : "";
940
-
941
-			if ($name == "" || $name == $address) {
942
-				$fulladdr = w2u($address);
943
-			}
944
-			else {
945
-				if (substr($name, 0, 1) != '"' && substr($name, -1) != '"') {
946
-					$fulladdr = "\"" . w2u($name) . "\" <" . w2u($address) . ">";
947
-				}
948
-				else {
949
-					$fulladdr = w2u($name) . "<" . w2u($address) . ">";
950
-				}
951
-			}
952
-
953
-			if ($row[PR_RECIPIENT_TYPE] == MAPI_TO) {
954
-				array_push($message->to, $fulladdr);
955
-			}
956
-			elseif ($row[PR_RECIPIENT_TYPE] == MAPI_CC) {
957
-				array_push($message->cc, $fulladdr);
958
-			}
959
-		}
960
-
961
-		if (is_array($message->to) && !empty($message->to)) {
962
-			$message->to = implode(", ", $message->to);
963
-		}
964
-		if (is_array($message->cc) && !empty($message->cc)) {
965
-			$message->cc = implode(", ", $message->cc);
966
-		}
967
-
968
-		// without importance some mobiles assume "0" (low) - Mantis #439
969
-		if (!isset($message->importance)) {
970
-			$message->importance = IMPORTANCE_NORMAL;
971
-		}
972
-
973
-		if (!isset($message->internetcpid)) {
974
-			$message->internetcpid = (defined('STORE_INTERNET_CPID')) ? constant('STORE_INTERNET_CPID') : INTERNET_CPID_WINDOWS1252;
975
-		}
976
-		$this->setFlag($mapimessage, $message);
977
-		// TODO checkcontentclass
978
-		if (!isset($message->contentclass)) {
979
-			$message->contentclass = DEFAULT_EMAIL_CONTENTCLASS;
980
-		}
981
-
982
-		if (!isset($message->nativebodytype)) {
983
-			$message->nativebodytype = MAPIUtils::GetNativeBodyType($messageprops);
984
-		}
985
-		elseif ($message->nativebodytype == SYNC_BODYPREFERENCE_UNDEFINED) {
986
-			$nbt = MAPIUtils::GetNativeBodyType($messageprops);
987
-			SLog::Write(LOGLEVEL_INFO, sprintf("MAPIProvider->getEmail(): native body type is undefined. Set it to %d.", $nbt));
988
-			$message->nativebodytype = $nbt;
989
-		}
990
-
991
-		// reply, reply to all, forward flags
992
-		if (isset($message->lastverbexecuted) && $message->lastverbexecuted) {
993
-			$message->lastverbexecuted = Utils::GetLastVerbExecuted($message->lastverbexecuted);
994
-		}
995
-
996
-		return $message;
997
-	}
998
-
999
-	/**
1000
-	 * Reads a note object from MAPI.
1001
-	 *
1002
-	 * @param mixed             $mapimessage
1003
-	 * @param ContentParameters $contentparameters
1004
-	 *
1005
-	 * @return SyncNote
1006
-	 */
1007
-	private function getNote($mapimessage, $contentparameters) {
1008
-		$message = new SyncNote();
1009
-
1010
-		// Standard one-to-one mappings first
1011
-		$this->getPropsFromMAPI($message, $mapimessage, MAPIMapping::GetNoteMapping());
1012
-
1013
-		// set the body according to contentparameters and supported AS version
1014
-		$this->setMessageBody($mapimessage, $contentparameters, $message);
1015
-
1016
-		return $message;
1017
-	}
1018
-
1019
-	/**
1020
-	 * Creates a SyncFolder from MAPI properties.
1021
-	 *
1022
-	 * @param mixed $folderprops
1023
-	 *
1024
-	 * @return SyncFolder
1025
-	 */
1026
-	public function GetFolder($folderprops) {
1027
-		$folder = new SyncFolder();
1028
-
1029
-		$storeprops = $this->GetStoreProps();
1030
-
1031
-		// For ZCP 7.0.x we need to retrieve more properties explicitly, see ZP-780
1032
-		if (isset($folderprops[PR_SOURCE_KEY]) && !isset($folderprops[PR_ENTRYID]) && !isset($folderprops[PR_CONTAINER_CLASS])) {
1033
-			$entryid = mapi_msgstore_entryidfromsourcekey($this->store, $folderprops[PR_SOURCE_KEY]);
1034
-			$mapifolder = mapi_msgstore_openentry($this->store, $entryid);
1035
-			$folderprops = mapi_getprops($mapifolder, [PR_DISPLAY_NAME, PR_PARENT_ENTRYID, PR_ENTRYID, PR_SOURCE_KEY, PR_PARENT_SOURCE_KEY, PR_CONTAINER_CLASS, PR_ATTR_HIDDEN, PR_EXTENDED_FOLDER_FLAGS]);
1036
-			SLog::Write(LOGLEVEL_DEBUG, "MAPIProvider->GetFolder(): received insufficient of data from ICS. Fetching required data.");
1037
-		}
1038
-
1039
-		if (!isset(
1040
-				$folderprops[PR_DISPLAY_NAME],
1041
-				$folderprops[PR_PARENT_ENTRYID],
1042
-				$folderprops[PR_SOURCE_KEY],
1043
-				$folderprops[PR_ENTRYID],
1044
-				$folderprops[PR_PARENT_SOURCE_KEY],
1045
-				$storeprops[PR_IPM_SUBTREE_ENTRYID])) {
1046
-			SLog::Write(LOGLEVEL_ERROR, "MAPIProvider->GetFolder(): invalid folder. Missing properties");
1047
-
1048
-			return false;
1049
-		}
1050
-
1051
-		// ignore hidden folders
1052
-		if (isset($folderprops[PR_ATTR_HIDDEN]) && $folderprops[PR_ATTR_HIDDEN] != false) {
1053
-			SLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->GetFolder(): invalid folder '%s' as it is a hidden folder (PR_ATTR_HIDDEN)", $folderprops[PR_DISPLAY_NAME]));
1054
-
1055
-			return false;
1056
-		}
1057
-
1058
-		// ignore certain undesired folders, like "RSS Feeds" and "Suggested contacts"
1059
-		if ((isset($folderprops[PR_CONTAINER_CLASS]) && $folderprops[PR_CONTAINER_CLASS] == "IPF.Note.OutlookHomepage") ||
1060
-				in_array($folderprops[PR_ENTRYID], $this->getSpecialFoldersData())) {
1061
-			SLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->GetFolder(): folder '%s' should not be synchronized", $folderprops[PR_DISPLAY_NAME]));
1062
-
1063
-			return false;
1064
-		}
1065
-
1066
-		$folder->BackendId = bin2hex($folderprops[PR_SOURCE_KEY]);
1067
-		$folderOrigin = DeviceManager::FLD_ORIGIN_USER;
1068
-		if (GSync::GetBackend()->GetImpersonatedUser()) {
1069
-			$folderOrigin = DeviceManager::FLD_ORIGIN_IMPERSONATED;
1070
-		}
1071
-		$folder->serverid = GSync::GetDeviceManager()->GetFolderIdForBackendId($folder->BackendId, true, $folderOrigin, $folderprops[PR_DISPLAY_NAME]);
1072
-		if ($folderprops[PR_PARENT_ENTRYID] == $storeprops[PR_IPM_SUBTREE_ENTRYID] || $folderprops[PR_PARENT_ENTRYID] == $storeprops[PR_IPM_PUBLIC_FOLDERS_ENTRYID]) {
1073
-			$folder->parentid = "0";
1074
-		}
1075
-		else {
1076
-			$folder->parentid = GSync::GetDeviceManager()->GetFolderIdForBackendId(bin2hex($folderprops[PR_PARENT_SOURCE_KEY]));
1077
-		}
1078
-		$folder->displayname = w2u($folderprops[PR_DISPLAY_NAME]);
1079
-		$folder->type = $this->GetFolderType($folderprops[PR_ENTRYID], isset($folderprops[PR_CONTAINER_CLASS]) ? $folderprops[PR_CONTAINER_CLASS] : false);
1080
-
1081
-		return $folder;
1082
-	}
1083
-
1084
-	/**
1085
-	 * Returns the foldertype for an entryid
1086
-	 * Gets the folder type by checking the default folders in MAPI.
1087
-	 *
1088
-	 * @param string $entryid
1089
-	 * @param string $class   (opt)
1090
-	 *
1091
-	 * @return long
1092
-	 */
1093
-	public function GetFolderType($entryid, $class = false) {
1094
-		$storeprops = $this->GetStoreProps();
1095
-		$inboxprops = $this->GetInboxProps();
1096
-
1097
-		if ($entryid == $storeprops[PR_IPM_WASTEBASKET_ENTRYID]) {
1098
-			return SYNC_FOLDER_TYPE_WASTEBASKET;
1099
-		}
1100
-		if ($entryid == $storeprops[PR_IPM_SENTMAIL_ENTRYID]) {
1101
-			return SYNC_FOLDER_TYPE_SENTMAIL;
1102
-		}
1103
-		if ($entryid == $storeprops[PR_IPM_OUTBOX_ENTRYID]) {
1104
-			return SYNC_FOLDER_TYPE_OUTBOX;
1105
-		}
1106
-
1107
-		// Public folders do not have inboxprops
1108
-		// @see https://jira.z-hub.io/browse/ZP-995
1109
-		if (!empty($inboxprops)) {
1110
-			if ($entryid == $inboxprops[PR_ENTRYID]) {
1111
-				return SYNC_FOLDER_TYPE_INBOX;
1112
-			}
1113
-			if ($entryid == $inboxprops[PR_IPM_DRAFTS_ENTRYID]) {
1114
-				return SYNC_FOLDER_TYPE_DRAFTS;
1115
-			}
1116
-			if ($entryid == $inboxprops[PR_IPM_TASK_ENTRYID]) {
1117
-				return SYNC_FOLDER_TYPE_TASK;
1118
-			}
1119
-			if ($entryid == $inboxprops[PR_IPM_APPOINTMENT_ENTRYID]) {
1120
-				return SYNC_FOLDER_TYPE_APPOINTMENT;
1121
-			}
1122
-			if ($entryid == $inboxprops[PR_IPM_CONTACT_ENTRYID]) {
1123
-				return SYNC_FOLDER_TYPE_CONTACT;
1124
-			}
1125
-			if ($entryid == $inboxprops[PR_IPM_NOTE_ENTRYID]) {
1126
-				return SYNC_FOLDER_TYPE_NOTE;
1127
-			}
1128
-			if ($entryid == $inboxprops[PR_IPM_JOURNAL_ENTRYID]) {
1129
-				return SYNC_FOLDER_TYPE_JOURNAL;
1130
-			}
1131
-		}
1132
-
1133
-		// user created folders
1134
-		if ($class == "IPF.Note") {
1135
-			return SYNC_FOLDER_TYPE_USER_MAIL;
1136
-		}
1137
-		if ($class == "IPF.Task") {
1138
-			return SYNC_FOLDER_TYPE_USER_TASK;
1139
-		}
1140
-		if ($class == "IPF.Appointment") {
1141
-			return SYNC_FOLDER_TYPE_USER_APPOINTMENT;
1142
-		}
1143
-		if ($class == "IPF.Contact") {
1144
-			return SYNC_FOLDER_TYPE_USER_CONTACT;
1145
-		}
1146
-		if ($class == "IPF.StickyNote") {
1147
-			return SYNC_FOLDER_TYPE_USER_NOTE;
1148
-		}
1149
-		if ($class == "IPF.Journal") {
1150
-			return SYNC_FOLDER_TYPE_USER_JOURNAL;
1151
-		}
1152
-
1153
-		return SYNC_FOLDER_TYPE_OTHER;
1154
-	}
1155
-
1156
-	/**
1157
-	 * Indicates if the entry id is a default MAPI folder.
1158
-	 *
1159
-	 * @param string $entryid
1160
-	 *
1161
-	 * @return bool
1162
-	 */
1163
-	public function IsMAPIDefaultFolder($entryid) {
1164
-		$msgstore_props = mapi_getprops($this->store, [PR_ENTRYID, PR_DISPLAY_NAME, PR_IPM_SUBTREE_ENTRYID, PR_IPM_OUTBOX_ENTRYID, PR_IPM_SENTMAIL_ENTRYID, PR_IPM_WASTEBASKET_ENTRYID, PR_MDB_PROVIDER, PR_IPM_PUBLIC_FOLDERS_ENTRYID, PR_IPM_FAVORITES_ENTRYID, PR_MAILBOX_OWNER_ENTRYID]);
1165
-
1166
-		$inboxProps = [];
1167
-		$inbox = mapi_msgstore_getreceivefolder($this->store);
1168
-		if (!mapi_last_hresult()) {
1169
-			$inboxProps = mapi_getprops($inbox, [PR_ENTRYID]);
1170
-		}
1171
-
1172
-		$root = mapi_msgstore_openentry($this->store, null); // TODO use getRootProps()
1173
-		$rootProps = mapi_getprops($root, [PR_IPM_APPOINTMENT_ENTRYID, PR_IPM_CONTACT_ENTRYID, PR_IPM_DRAFTS_ENTRYID, PR_IPM_JOURNAL_ENTRYID, PR_IPM_NOTE_ENTRYID, PR_IPM_TASK_ENTRYID, PR_ADDITIONAL_REN_ENTRYIDS]);
1174
-
1175
-		$additional_ren_entryids = [];
1176
-		if (isset($rootProps[PR_ADDITIONAL_REN_ENTRYIDS])) {
1177
-			$additional_ren_entryids = $rootProps[PR_ADDITIONAL_REN_ENTRYIDS];
1178
-		}
1179
-
1180
-		$defaultfolders = [
1181
-			"inbox" => ["inbox" => PR_ENTRYID],
1182
-			"outbox" => ["store" => PR_IPM_OUTBOX_ENTRYID],
1183
-			"sent" => ["store" => PR_IPM_SENTMAIL_ENTRYID],
1184
-			"wastebasket" => ["store" => PR_IPM_WASTEBASKET_ENTRYID],
1185
-			"favorites" => ["store" => PR_IPM_FAVORITES_ENTRYID],
1186
-			"publicfolders" => ["store" => PR_IPM_PUBLIC_FOLDERS_ENTRYID],
1187
-			"calendar" => ["root" => PR_IPM_APPOINTMENT_ENTRYID],
1188
-			"contact" => ["root" => PR_IPM_CONTACT_ENTRYID],
1189
-			"drafts" => ["root" => PR_IPM_DRAFTS_ENTRYID],
1190
-			"journal" => ["root" => PR_IPM_JOURNAL_ENTRYID],
1191
-			"note" => ["root" => PR_IPM_NOTE_ENTRYID],
1192
-			"task" => ["root" => PR_IPM_TASK_ENTRYID],
1193
-			"junk" => ["additional" => 4],
1194
-			"syncissues" => ["additional" => 1],
1195
-			"conflicts" => ["additional" => 0],
1196
-			"localfailures" => ["additional" => 2],
1197
-			"serverfailures" => ["additional" => 3],
1198
-		];
1199
-
1200
-		foreach ($defaultfolders as $key => $prop) {
1201
-			$tag = reset($prop);
1202
-			$from = key($prop);
1203
-
1204
-			switch ($from) {
1205
-				case "inbox":
1206
-					if (isset($inboxProps[$tag]) && $entryid == $inboxProps[$tag]) {
1207
-						SLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->IsMAPIFolder(): Inbox found, key '%s'", $key));
1208
-
1209
-						return true;
1210
-					}
1211
-					break;
1212
-
1213
-				case "store":
1214
-					if (isset($msgstore_props[$tag]) && $entryid == $msgstore_props[$tag]) {
1215
-						SLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->IsMAPIFolder(): Store folder found, key '%s'", $key));
1216
-
1217
-						return true;
1218
-					}
1219
-					break;
1220
-
1221
-				case "root":
1222
-					if (isset($rootProps[$tag]) && $entryid == $rootProps[$tag]) {
1223
-						SLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->IsMAPIFolder(): Root folder found, key '%s'", $key));
1224
-
1225
-						return true;
1226
-					}
1227
-					break;
1228
-
1229
-				case "additional":
1230
-					if (isset($additional_ren_entryids[$tag]) && $entryid == $additional_ren_entryids[$tag]) {
1231
-						SLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->IsMAPIFolder(): Additional folder found, key '%s'", $key));
1232
-
1233
-						return true;
1234
-					}
1235
-					break;
1236
-			}
1237
-		}
1238
-
1239
-		return false;
1240
-	}
1241
-
1242
-	/*----------------------------------------------------------------------------------------------------------
34
+    /**
35
+     * Reads a message from MAPI
36
+     * Depending on the message class, a contact, appointment, task or email is read.
37
+     *
38
+     * @param mixed             $mapimessage
39
+     * @param ContentParameters $contentparameters
40
+     *
41
+     * @return SyncObject
42
+     */
43
+    public function GetMessage($mapimessage, $contentparameters) {
44
+        // Gets the Sync object from a MAPI object according to its message class
45
+
46
+        $props = mapi_getprops($mapimessage, [PR_MESSAGE_CLASS]);
47
+        if (isset($props[PR_MESSAGE_CLASS])) {
48
+            $messageclass = $props[PR_MESSAGE_CLASS];
49
+        }
50
+        else {
51
+            $messageclass = "IPM";
52
+        }
53
+
54
+        if (strpos($messageclass, "IPM.Contact") === 0) {
55
+            return $this->getContact($mapimessage, $contentparameters);
56
+        }
57
+        if (strpos($messageclass, "IPM.Appointment") === 0) {
58
+            return $this->getAppointment($mapimessage, $contentparameters);
59
+        }
60
+        if (strpos($messageclass, "IPM.Task") === 0 && strpos($messageclass, "IPM.TaskRequest") === false) {
61
+            return $this->getTask($mapimessage, $contentparameters);
62
+        }
63
+        if (strpos($messageclass, "IPM.StickyNote") === 0) {
64
+            return $this->getNote($mapimessage, $contentparameters);
65
+        }
66
+
67
+        return $this->getEmail($mapimessage, $contentparameters);
68
+    }
69
+
70
+    /**
71
+     * Reads a contact object from MAPI.
72
+     *
73
+     * @param mixed             $mapimessage
74
+     * @param ContentParameters $contentparameters
75
+     *
76
+     * @return SyncContact
77
+     */
78
+    private function getContact($mapimessage, $contentparameters) {
79
+        $message = new SyncContact();
80
+
81
+        // Standard one-to-one mappings first
82
+        $this->getPropsFromMAPI($message, $mapimessage, MAPIMapping::GetContactMapping());
83
+
84
+        // Contact specific props
85
+        $contactproperties = MAPIMapping::GetContactProperties();
86
+        $messageprops = $this->getProps($mapimessage, $contactproperties);
87
+
88
+        // set the body according to contentparameters and supported AS version
89
+        $this->setMessageBody($mapimessage, $contentparameters, $message);
90
+
91
+        // check the picture
92
+        if (isset($messageprops[$contactproperties["haspic"]]) && $messageprops[$contactproperties["haspic"]]) {
93
+            // Add attachments
94
+            $attachtable = mapi_message_getattachmenttable($mapimessage);
95
+            mapi_table_restrict($attachtable, MAPIUtils::GetContactPicRestriction());
96
+            $rows = mapi_table_queryallrows($attachtable, [PR_ATTACH_NUM, PR_ATTACH_SIZE]);
97
+
98
+            foreach ($rows as $row) {
99
+                if (isset($row[PR_ATTACH_NUM])) {
100
+                    $mapiattach = mapi_message_openattach($mapimessage, $row[PR_ATTACH_NUM]);
101
+                    $message->picture = base64_encode(mapi_attach_openbin($mapiattach, PR_ATTACH_DATA_BIN));
102
+                }
103
+            }
104
+        }
105
+
106
+        return $message;
107
+    }
108
+
109
+    /**
110
+     * Reads a task object from MAPI.
111
+     *
112
+     * @param mixed             $mapimessage
113
+     * @param ContentParameters $contentparameters
114
+     *
115
+     * @return SyncTask
116
+     */
117
+    private function getTask($mapimessage, $contentparameters) {
118
+        $message = new SyncTask();
119
+
120
+        // Standard one-to-one mappings first
121
+        $this->getPropsFromMAPI($message, $mapimessage, MAPIMapping::GetTaskMapping());
122
+
123
+        // Task specific props
124
+        $taskproperties = MAPIMapping::GetTaskProperties();
125
+        $messageprops = $this->getProps($mapimessage, $taskproperties);
126
+
127
+        // set the body according to contentparameters and supported AS version
128
+        $this->setMessageBody($mapimessage, $contentparameters, $message);
129
+
130
+        // task with deadoccur is an occurrence of a recurring task and does not need to be handled as recurring
131
+        // webaccess does not set deadoccur for the initial recurring task
132
+        if (isset($messageprops[$taskproperties["isrecurringtag"]]) &&
133
+            $messageprops[$taskproperties["isrecurringtag"]] &&
134
+            (!isset($messageprops[$taskproperties["deadoccur"]]) ||
135
+            (isset($messageprops[$taskproperties["deadoccur"]]) &&
136
+            !$messageprops[$taskproperties["deadoccur"]]))) {
137
+            // Process recurrence
138
+            $message->recurrence = new SyncTaskRecurrence();
139
+            $this->getRecurrence($mapimessage, $messageprops, $message, $message->recurrence, false);
140
+        }
141
+
142
+        // when set the task to complete using the WebAccess, the dateComplete property is not set correctly
143
+        if ($message->complete == 1 && !isset($message->datecompleted)) {
144
+            $message->datecompleted = time();
145
+        }
146
+
147
+        // if no reminder is set, announce that to the mobile
148
+        if (!isset($message->reminderset)) {
149
+            $message->reminderset = 0;
150
+        }
151
+
152
+        return $message;
153
+    }
154
+
155
+    /**
156
+     * Reads an appointment object from MAPI.
157
+     *
158
+     * @param mixed             $mapimessage
159
+     * @param ContentParameters $contentparameters
160
+     *
161
+     * @return SyncAppointment
162
+     */
163
+    private function getAppointment($mapimessage, $contentparameters) {
164
+        $message = new SyncAppointment();
165
+
166
+        // Standard one-to-one mappings first
167
+        $this->getPropsFromMAPI($message, $mapimessage, MAPIMapping::GetAppointmentMapping());
168
+
169
+        // Appointment specific props
170
+        $appointmentprops = MAPIMapping::GetAppointmentProperties();
171
+        $messageprops = $this->getProps($mapimessage, $appointmentprops);
172
+
173
+        // set the body according to contentparameters and supported AS version
174
+        $this->setMessageBody($mapimessage, $contentparameters, $message);
175
+
176
+        // Set reminder time if reminderset is true
177
+        if (isset($messageprops[$appointmentprops["reminderset"]]) && $messageprops[$appointmentprops["reminderset"]] == true) {
178
+            if ($messageprops[$appointmentprops["remindertime"]] == 0x5AE980E1) {
179
+                $message->reminder = 15;
180
+            }
181
+            else {
182
+                $message->reminder = $messageprops[$appointmentprops["remindertime"]];
183
+            }
184
+        }
185
+
186
+        if (!isset($message->uid)) {
187
+            $message->uid = bin2hex($messageprops[$appointmentprops["sourcekey"]]);
188
+        }
189
+        else {
190
+            $message->uid = Utils::GetICalUidFromOLUid($message->uid);
191
+        }
192
+
193
+        // Always set organizer information because some devices do not work properly without it
194
+        if (isset($messageprops[$appointmentprops["representingentryid"]], $messageprops[$appointmentprops["representingname"]])
195
+            ) {
196
+            $message->organizeremail = w2u($this->getSMTPAddressFromEntryID($messageprops[$appointmentprops["representingentryid"]]));
197
+            // if the email address can't be resolved, fall back to PR_SENT_REPRESENTING_SEARCH_KEY
198
+            if ($message->organizeremail == "" && isset($messageprops[$appointmentprops["sentrepresentinsrchk"]])) {
199
+                $message->organizeremail = $this->getEmailAddressFromSearchKey($messageprops[$appointmentprops["sentrepresentinsrchk"]]);
200
+            }
201
+            $message->organizername = w2u($messageprops[$appointmentprops["representingname"]]);
202
+        }
203
+
204
+        $appTz = false; // if the appointment has some timezone information saved on the server
205
+        if (!empty($messageprops[$appointmentprops["timezonetag"]])) {
206
+            $tz = $this->getTZFromMAPIBlob($messageprops[$appointmentprops["timezonetag"]]);
207
+            $appTz = true;
208
+        }
209
+        elseif (!empty($messageprops[$appointmentprops["timezonedesc"]])) {
210
+            // Windows uses UTC in timezone description in opposite to mstzones in TimezoneUtil which uses GMT
211
+            $wintz = str_replace("UTC", "GMT", $messageprops[$appointmentprops["timezonedesc"]]);
212
+            $tz = TimezoneUtil::GetFullTZFromTZName(TimezoneUtil::GetTZNameFromWinTZ($wintz));
213
+            $appTz = true;
214
+        }
215
+        else {
216
+            // set server default timezone (correct timezone should be configured!)
217
+            $tz = TimezoneUtil::GetFullTZ();
218
+        }
219
+
220
+        if (isset($messageprops[$appointmentprops["isrecurring"]]) && $messageprops[$appointmentprops["isrecurring"]]) {
221
+            // Process recurrence
222
+            $message->recurrence = new SyncRecurrence();
223
+            $this->getRecurrence($mapimessage, $messageprops, $message, $message->recurrence, $tz);
224
+
225
+            if (empty($message->alldayevent)) {
226
+                $message->timezone = base64_encode(TimezoneUtil::GetSyncBlobFromTZ($tz));
227
+            }
228
+        }
229
+
230
+        // Do attendees
231
+        $reciptable = mapi_message_getrecipienttable($mapimessage);
232
+        // Only get first 256 recipients, to prevent possible load issues.
233
+        $rows = mapi_table_queryrows($reciptable, [PR_DISPLAY_NAME, PR_EMAIL_ADDRESS, PR_SMTP_ADDRESS, PR_ADDRTYPE, PR_RECIPIENT_TRACKSTATUS, PR_RECIPIENT_TYPE, PR_SEARCH_KEY], 0, 256);
234
+
235
+        // Exception: we do not synchronize appointments with more than 250 attendees
236
+        if (count($rows) > 250) {
237
+            $message->id = bin2hex($messageprops[$appointmentprops["sourcekey"]]);
238
+            $mbe = new SyncObjectBrokenException("Appointment has too many attendees");
239
+            $mbe->SetSyncObject($message);
240
+
241
+            throw $mbe;
242
+        }
243
+
244
+        if (count($rows) > 0) {
245
+            $message->attendees = [];
246
+        }
247
+
248
+        foreach ($rows as $row) {
249
+            $attendee = new SyncAttendee();
250
+
251
+            $attendee->name = w2u($row[PR_DISPLAY_NAME]);
252
+            // smtp address is always a proper email address
253
+            if (isset($row[PR_SMTP_ADDRESS])) {
254
+                $attendee->email = w2u($row[PR_SMTP_ADDRESS]);
255
+            }
256
+            elseif (isset($row[PR_ADDRTYPE], $row[PR_EMAIL_ADDRESS])) {
257
+                // if address type is SMTP, it's also a proper email address
258
+                if ($row[PR_ADDRTYPE] == "SMTP") {
259
+                    $attendee->email = w2u($row[PR_EMAIL_ADDRESS]);
260
+                }
261
+                // if address type is ZARAFA, the PR_EMAIL_ADDRESS contains username
262
+                elseif ($row[PR_ADDRTYPE] == "ZARAFA") {
263
+                    $userinfo = @nsp_getuserinfo($row[PR_EMAIL_ADDRESS]);
264
+                    if (is_array($userinfo) && isset($userinfo["primary_email"])) {
265
+                        $attendee->email = w2u($userinfo["primary_email"]);
266
+                    }
267
+                    // if the user was not found, do a fallback to PR_SEARCH_KEY
268
+                    // @see https://jira.z-hub.io/browse/ZP-1178
269
+                    elseif (isset($row[PR_SEARCH_KEY])) {
270
+                        $attendee->email = w2u($this->getEmailAddressFromSearchKey($row[PR_SEARCH_KEY]));
271
+                    }
272
+                    else {
273
+                        SLog::Write(LOGLEVEL_WARN, sprintf("MAPIProvider->getAppointment: The attendee '%s' of type ZARAFA can not be resolved. Code: 0x%X", $row[PR_EMAIL_ADDRESS], mapi_last_hresult()));
274
+                    }
275
+                }
276
+            }
277
+
278
+            // set attendee's status and type if they're available and if we are the organizer
279
+            $storeprops = $this->GetStoreProps();
280
+            if (isset($row[PR_RECIPIENT_TRACKSTATUS], $messageprops[$appointmentprops["representingentryid"]], $storeprops[PR_MAILBOX_OWNER_ENTRYID]) &&
281
+                    $messageprops[$appointmentprops["representingentryid"]] == $storeprops[PR_MAILBOX_OWNER_ENTRYID]) {
282
+                $attendee->attendeestatus = $row[PR_RECIPIENT_TRACKSTATUS];
283
+            }
284
+            if (isset($row[PR_RECIPIENT_TYPE])) {
285
+                $attendee->attendeetype = $row[PR_RECIPIENT_TYPE];
286
+            }
287
+            // Some attendees have no email or name (eg resources), and if you
288
+            // don't send one of those fields, the phone will give an error ... so
289
+            // we don't send it in that case.
290
+            // also ignore the "attendee" if the email is equal to the organizers' email
291
+            if (isset($attendee->name, $attendee->email) && $attendee->email != "" && (!isset($message->organizeremail) || (isset($message->organizeremail) && $attendee->email != $message->organizeremail))) {
292
+                array_push($message->attendees, $attendee);
293
+            }
294
+        }
295
+
296
+        // Status 0 = no meeting, status 1 = organizer, status 2/3/4/5 = tentative/accepted/declined/notresponded
297
+        if (isset($messageprops[$appointmentprops["meetingstatus"]]) && $messageprops[$appointmentprops["meetingstatus"]] > 1) {
298
+            if (!isset($message->attendees) || !is_array($message->attendees)) {
299
+                $message->attendees = [];
300
+            }
301
+            // Work around iOS6 cancellation issue when there are no attendees for this meeting. Just add ourselves as the sole attendee.
302
+            if (count($message->attendees) == 0) {
303
+                SLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->getAppointment: adding ourself as an attendee for iOS6 workaround"));
304
+                $attendee = new SyncAttendee();
305
+
306
+                $meinfo = nsp_getuserinfo(Request::GetUser());
307
+
308
+                if (is_array($meinfo)) {
309
+                    $attendee->email = w2u($meinfo["primary_email"]);
310
+                    $attendee->name = w2u($meinfo["fullname"]);
311
+                    $attendee->attendeetype = MAPI_TO;
312
+
313
+                    array_push($message->attendees, $attendee);
314
+                }
315
+            }
316
+            $message->responsetype = $messageprops[$appointmentprops["responsestatus"]];
317
+        }
318
+
319
+        // If it's an appointment which doesn't have any attendees, we have to make sure that
320
+        // the user is the owner or it will not work properly with android devices
321
+        // @see https://jira.z-hub.io/browse/ZP-1020
322
+        if (isset($messageprops[$appointmentprops["meetingstatus"]]) && $messageprops[$appointmentprops["meetingstatus"]] == olNonMeeting && empty($message->attendees)) {
323
+            $meinfo = nsp_getuserinfo(Request::GetUser());
324
+
325
+            if (is_array($meinfo)) {
326
+                $message->organizeremail = w2u($meinfo["primary_email"]);
327
+                $message->organizername = w2u($meinfo["fullname"]);
328
+                SLog::Write(LOGLEVEL_DEBUG, "MAPIProvider->getAppointment(): setting ourself as the organizer for an appointment without attendees.");
329
+            }
330
+        }
331
+
332
+        if (!isset($message->nativebodytype)) {
333
+            $message->nativebodytype = MAPIUtils::GetNativeBodyType($messageprops);
334
+        }
335
+        elseif ($message->nativebodytype == SYNC_BODYPREFERENCE_UNDEFINED) {
336
+            $nbt = MAPIUtils::GetNativeBodyType($messageprops);
337
+            SLog::Write(LOGLEVEL_INFO, sprintf("MAPIProvider->getAppointment(): native body type is undefined. Set it to %d.", $nbt));
338
+            $message->nativebodytype = $nbt;
339
+        }
340
+
341
+        // If the user is working from a location other than the office the busystatus should be interpreted as free.
342
+        if (isset($message->busystatus) && $message->busystatus == fbWorkingElsewhere) {
343
+            $message->busystatus = fbFree;
344
+        }
345
+
346
+        // If the busystatus has the value of -1, we should be interpreted as tentative (1) / ZP-581
347
+        if (isset($message->busystatus) && $message->busystatus == -1) {
348
+            $message->busystatus = fbTentative;
349
+        }
350
+
351
+        // All-day events might appear as 24h (or multiple of it) long when they start not exactly at midnight (+/- bias of the timezone)
352
+        if (isset($message->alldayevent) && $message->alldayevent) {
353
+            $localStartTime = localtime($message->starttime, 1);
354
+
355
+            // The appointment is all-day but doesn't start at midnight.
356
+            // If it was created in another timezone and we have that information,
357
+            // set the startime to the midnight of the current timezone.
358
+            if ($appTz && ($localStartTime['tm_hour'] || $localStartTime['tm_min'])) {
359
+                SLog::Write(LOGLEVEL_DEBUG, "MAPIProvider->getAppointment(): all-day event starting not midnight.");
360
+                $duration = $message->endtime - $message->starttime;
361
+                $serverTz = TimezoneUtil::GetFullTZ();
362
+                $message->starttime = $this->getGMTTimeByTZ($this->getLocaltimeByTZ($message->starttime, $tz), $serverTz);
363
+                $message->endtime = $message->starttime + $duration;
364
+            }
365
+        }
366
+
367
+        return $message;
368
+    }
369
+
370
+    /**
371
+     * Reads recurrence information from MAPI.
372
+     *
373
+     * @param mixed      $mapimessage
374
+     * @param array      $recurprops
375
+     * @param SyncObject &$syncMessage    the message
376
+     * @param SyncObject &$syncRecurrence the  recurrence message
377
+     * @param array      $tz              timezone information
378
+     *
379
+     * @return
380
+     */
381
+    private function getRecurrence($mapimessage, $recurprops, &$syncMessage, &$syncRecurrence, $tz) {
382
+        if ($syncRecurrence instanceof SyncTaskRecurrence) {
383
+            $recurrence = new TaskRecurrence($this->store, $mapimessage);
384
+        }
385
+        else {
386
+            $recurrence = new Recurrence($this->store, $mapimessage);
387
+        }
388
+
389
+        switch ($recurrence->recur["type"]) {
390
+            case 10: // daily
391
+                switch ($recurrence->recur["subtype"]) {
392
+                    default:
393
+                        $syncRecurrence->type = 0;
394
+                        break;
395
+
396
+                    case 1:
397
+                        $syncRecurrence->type = 0;
398
+                        $syncRecurrence->dayofweek = 62; // mon-fri
399
+                        $syncRecurrence->interval = 1;
400
+                        break;
401
+                }
402
+                break;
403
+
404
+            case 11: // weekly
405
+                $syncRecurrence->type = 1;
406
+                break;
407
+
408
+            case 12: // monthly
409
+                switch ($recurrence->recur["subtype"]) {
410
+                    default:
411
+                        $syncRecurrence->type = 2;
412
+                        break;
413
+
414
+                    case 3:
415
+                        $syncRecurrence->type = 3;
416
+                        break;
417
+                }
418
+                break;
419
+
420
+            case 13: // yearly
421
+                switch ($recurrence->recur["subtype"]) {
422
+                    default:
423
+                        $syncRecurrence->type = 4;
424
+                        break;
425
+
426
+                    case 2:
427
+                        $syncRecurrence->type = 5;
428
+                        break;
429
+
430
+                    case 3:
431
+                        $syncRecurrence->type = 6;
432
+                        break;
433
+                }
434
+        }
435
+        // Termination
436
+        switch ($recurrence->recur["term"]) {
437
+            case 0x21:
438
+                $syncRecurrence->until = $recurrence->recur["end"];
439
+                // fixes Mantis #350 : recur-end does not consider timezones - use ClipEnd if available
440
+                if (isset($recurprops[$recurrence->proptags["enddate_recurring"]])) {
441
+                    $syncRecurrence->until = $recurprops[$recurrence->proptags["enddate_recurring"]];
442
+                }
443
+                // add one day (minus 1 sec) to the end time to make sure the last occurrence is covered
444
+                $syncRecurrence->until += 86399;
445
+                break;
446
+
447
+            case 0x22:
448
+                $syncRecurrence->occurrences = $recurrence->recur["numoccur"];
449
+                break;
450
+
451
+            case 0x23:
452
+                // never ends
453
+                break;
454
+        }
455
+
456
+        // Correct 'alldayevent' because outlook fails to set it on recurring items of 24 hours or longer
457
+        if (isset($recurrence->recur["endocc"], $recurrence->recur["startocc"]) && ($recurrence->recur["endocc"] - $recurrence->recur["startocc"] >= 1440)) {
458
+            $syncMessage->alldayevent = true;
459
+        }
460
+
461
+        // Interval is different according to the type/subtype
462
+        switch ($recurrence->recur["type"]) {
463
+            case 10:
464
+                if ($recurrence->recur["subtype"] == 0) {
465
+                    $syncRecurrence->interval = (int) ($recurrence->recur["everyn"] / 1440);
466
+                }  // minutes
467
+                break;
468
+
469
+            case 11:
470
+            case 12:
471
+                $syncRecurrence->interval = $recurrence->recur["everyn"];
472
+                break; // months / weeks
473
+
474
+            case 13:
475
+                $syncRecurrence->interval = (int) ($recurrence->recur["everyn"] / 12);
476
+                break; // months
477
+        }
478
+
479
+        if (isset($recurrence->recur["weekdays"])) {
480
+            $syncRecurrence->dayofweek = $recurrence->recur["weekdays"];
481
+        } // bitmask of days (1 == sunday, 128 == saturday
482
+        if (isset($recurrence->recur["nday"])) {
483
+            $syncRecurrence->weekofmonth = $recurrence->recur["nday"];
484
+        } // N'th {DAY} of {X} (0-5)
485
+        if (isset($recurrence->recur["month"])) {
486
+            $syncRecurrence->monthofyear = (int) ($recurrence->recur["month"] / (60 * 24 * 29)) + 1;
487
+        } // works ok due to rounding. see also $monthminutes below (1-12)
488
+        if (isset($recurrence->recur["monthday"])) {
489
+            $syncRecurrence->dayofmonth = $recurrence->recur["monthday"];
490
+        } // day of month (1-31)
491
+
492
+        // All changed exceptions are appointments within the 'exceptions' array. They contain the same items as a normal appointment
493
+        foreach ($recurrence->recur["changed_occurrences"] as $change) {
494
+            $exception = new SyncAppointmentException();
495
+
496
+            // start, end, basedate, subject, remind_before, reminderset, location, busystatus, alldayevent, label
497
+            if (isset($change["start"])) {
498
+                $exception->starttime = $this->getGMTTimeByTZ($change["start"], $tz);
499
+            }
500
+            if (isset($change["end"])) {
501
+                $exception->endtime = $this->getGMTTimeByTZ($change["end"], $tz);
502
+            }
503
+            if (isset($change["basedate"])) {
504
+                $exception->exceptionstarttime = $this->getGMTTimeByTZ($this->getDayStartOfTimestamp($change["basedate"]) + $recurrence->recur["startocc"] * 60, $tz);
505
+
506
+                // open body because getting only property might not work because of memory limit
507
+                $exceptionatt = $recurrence->getExceptionAttachment($change["basedate"]);
508
+                if ($exceptionatt) {
509
+                    $exceptionobj = mapi_attach_openobj($exceptionatt, 0);
510
+                    $this->setMessageBodyForType($exceptionobj, SYNC_BODYPREFERENCE_PLAIN, $exception);
511
+                }
512
+            }
513
+            if (isset($change["subject"])) {
514
+                $exception->subject = w2u($change["subject"]);
515
+            }
516
+            if (isset($change["reminder_before"]) && $change["reminder_before"]) {
517
+                $exception->reminder = $change["remind_before"];
518
+            }
519
+            if (isset($change["location"])) {
520
+                $exception->location = w2u($change["location"]);
521
+            }
522
+            if (isset($change["busystatus"])) {
523
+                $exception->busystatus = $change["busystatus"];
524
+            }
525
+            if (isset($change["alldayevent"])) {
526
+                $exception->alldayevent = $change["alldayevent"];
527
+            }
528
+
529
+            // set some data from the original appointment
530
+            if (isset($syncMessage->uid)) {
531
+                $exception->uid = $syncMessage->uid;
532
+            }
533
+            if (isset($syncMessage->organizername)) {
534
+                $exception->organizername = $syncMessage->organizername;
535
+            }
536
+            if (isset($syncMessage->organizeremail)) {
537
+                $exception->organizeremail = $syncMessage->organizeremail;
538
+            }
539
+
540
+            if (!isset($syncMessage->exceptions)) {
541
+                $syncMessage->exceptions = [];
542
+            }
543
+
544
+            // If the user is working from a location other than the office the busystatus should be interpreted as free.
545
+            if (isset($exception->busystatus) && $exception->busystatus == fbWorkingElsewhere) {
546
+                $exception->busystatus = fbFree;
547
+            }
548
+
549
+            // If the busystatus has the value of -1, we should be interpreted as tentative (1) / ZP-581
550
+            if (isset($exception->busystatus) && $exception->busystatus == -1) {
551
+                $exception->busystatus = fbTentative;
552
+            }
553
+
554
+            // if an exception lasts 24 hours and the series are an allday events, set also the exception to allday event,
555
+            // otherwise it will be a 24 hour long event on some mobiles.
556
+            // @see https://jira.z-hub.io/browse/ZP-980
557
+            if (isset($exception->starttime, $exception->endtime) && ($exception->endtime - $exception->starttime == 86400) && $syncMessage->alldayevent) {
558
+                $exception->alldayevent = 1;
559
+            }
560
+            array_push($syncMessage->exceptions, $exception);
561
+        }
562
+
563
+        // Deleted appointments contain only the original date (basedate) and a 'deleted' tag
564
+        foreach ($recurrence->recur["deleted_occurrences"] as $deleted) {
565
+            $exception = new SyncAppointmentException();
566
+
567
+            $exception->exceptionstarttime = $this->getGMTTimeByTZ($this->getDayStartOfTimestamp($deleted) + $recurrence->recur["startocc"] * 60, $tz);
568
+            $exception->deleted = "1";
569
+
570
+            if (!isset($syncMessage->exceptions)) {
571
+                $syncMessage->exceptions = [];
572
+            }
573
+
574
+            array_push($syncMessage->exceptions, $exception);
575
+        }
576
+
577
+        if (isset($syncMessage->complete) && $syncMessage->complete) {
578
+            $syncRecurrence->complete = $syncMessage->complete;
579
+        }
580
+    }
581
+
582
+    /**
583
+     * Reads an email object from MAPI.
584
+     *
585
+     * @param mixed             $mapimessage
586
+     * @param ContentParameters $contentparameters
587
+     *
588
+     * @return SyncEmail
589
+     */
590
+    private function getEmail($mapimessage, $contentparameters) {
591
+        // This workaround fixes ZP-729 and still works with Outlook.
592
+        // FIXME: It should be properly fixed when refactoring.
593
+        $bpReturnType = Utils::GetBodyPreferenceBestMatch($contentparameters->GetBodyPreference());
594
+        if (($contentparameters->GetMimeSupport() == SYNC_MIMESUPPORT_NEVER) ||
595
+                ($key = array_search(SYNC_BODYPREFERENCE_MIME, $contentparameters->GetBodyPreference()) === false) ||
596
+                $bpReturnType != SYNC_BODYPREFERENCE_MIME) {
597
+            MAPIUtils::ParseSmime($this->session, $this->store, $this->getAddressbook(), $mapimessage);
598
+        }
599
+
600
+        $message = new SyncMail();
601
+
602
+        $this->getPropsFromMAPI($message, $mapimessage, MAPIMapping::GetEmailMapping());
603
+
604
+        $emailproperties = MAPIMapping::GetEmailProperties();
605
+        $messageprops = $this->getProps($mapimessage, $emailproperties);
606
+
607
+        if (isset($messageprops[PR_SOURCE_KEY])) {
608
+            $sourcekey = $messageprops[PR_SOURCE_KEY];
609
+        }
610
+        else {
611
+            $mbe = new SyncObjectBrokenException("The message doesn't have a sourcekey");
612
+            $mbe->SetSyncObject($message);
613
+
614
+            throw $mbe;
615
+        }
616
+
617
+        // set the body according to contentparameters and supported AS version
618
+        $this->setMessageBody($mapimessage, $contentparameters, $message);
619
+
620
+        $fromname = $fromaddr = "";
621
+
622
+        if (isset($messageprops[$emailproperties["representingname"]])) {
623
+            // remove encapsulating double quotes from the representingname
624
+            $fromname = preg_replace('/^\"(.*)\"$/', "\${1}", $messageprops[$emailproperties["representingname"]]);
625
+        }
626
+        if (isset($messageprops[$emailproperties["representingentryid"]])) {
627
+            $fromaddr = $this->getSMTPAddressFromEntryID($messageprops[$emailproperties["representingentryid"]]);
628
+        }
629
+
630
+        // if the email address can't be resolved, fall back to PR_SENT_REPRESENTING_SEARCH_KEY
631
+        if ($fromaddr == "" && isset($messageprops[$emailproperties["representingsearchkey"]])) {
632
+            $fromaddr = $this->getEmailAddressFromSearchKey($messageprops[$emailproperties["representingsearchkey"]]);
633
+        }
634
+
635
+        if ($fromname == $fromaddr) {
636
+            $fromname = "";
637
+        }
638
+
639
+        if ($fromname) {
640
+            $from = "\"" . w2u($fromname) . "\" <" . w2u($fromaddr) . ">";
641
+        }
642
+        else { // START CHANGED dw2412 HTC shows "error" if sender name is unknown
643
+            $from = "\"" . w2u($fromaddr) . "\" <" . w2u($fromaddr) . ">";
644
+        }
645
+        // END CHANGED dw2412 HTC shows "error" if sender name is unknown
646
+
647
+        $message->from = $from;
648
+
649
+        // process Meeting Requests
650
+        if (isset($message->messageclass) && strpos($message->messageclass, "IPM.Schedule.Meeting") === 0) {
651
+            $message->meetingrequest = new SyncMeetingRequest();
652
+            $this->getPropsFromMAPI($message->meetingrequest, $mapimessage, MAPIMapping::GetMeetingRequestMapping());
653
+
654
+            $meetingrequestproperties = MAPIMapping::GetMeetingRequestProperties();
655
+            $props = $this->getProps($mapimessage, $meetingrequestproperties);
656
+
657
+            // Get the GOID
658
+            if (isset($props[$meetingrequestproperties["goidtag"]])) {
659
+                $message->meetingrequest->globalobjid = base64_encode($props[$meetingrequestproperties["goidtag"]]);
660
+            }
661
+
662
+            // Set Timezone
663
+            if (isset($props[$meetingrequestproperties["timezonetag"]])) {
664
+                $tz = $this->getTZFromMAPIBlob($props[$meetingrequestproperties["timezonetag"]]);
665
+            }
666
+            else {
667
+                $tz = TimezoneUtil::GetFullTZ();
668
+            }
669
+
670
+            $message->meetingrequest->timezone = base64_encode(TimezoneUtil::GetSyncBlobFromTZ($tz));
671
+
672
+            // send basedate if exception
673
+            if (isset($props[$meetingrequestproperties["recReplTime"]]) ||
674
+                (isset($props[$meetingrequestproperties["lidIsException"]]) && $props[$meetingrequestproperties["lidIsException"]] == true)) {
675
+                if (isset($props[$meetingrequestproperties["recReplTime"]])) {
676
+                    $basedate = $props[$meetingrequestproperties["recReplTime"]];
677
+                    $message->meetingrequest->recurrenceid = $this->getGMTTimeByTZ($basedate, $this->getGMTTZ());
678
+                }
679
+                else {
680
+                    if (!isset($props[$meetingrequestproperties["goidtag"]]) || !isset($props[$meetingrequestproperties["recurStartTime"]]) || !isset($props[$meetingrequestproperties["timezonetag"]])) {
681
+                        SLog::Write(LOGLEVEL_WARN, "Missing property to set correct basedate for exception");
682
+                    }
683
+                    else {
684
+                        $basedate = Utils::ExtractBaseDate($props[$meetingrequestproperties["goidtag"]], $props[$meetingrequestproperties["recurStartTime"]]);
685
+                        $message->meetingrequest->recurrenceid = $this->getGMTTimeByTZ($basedate, $tz);
686
+                    }
687
+                }
688
+            }
689
+
690
+            // Organizer is the sender
691
+            if (strpos($message->messageclass, "IPM.Schedule.Meeting.Resp") === 0) {
692
+                $message->meetingrequest->organizer = $message->to;
693
+            }
694
+            else {
695
+                $message->meetingrequest->organizer = $message->from;
696
+            }
697
+
698
+            // Process recurrence
699
+            if (isset($props[$meetingrequestproperties["isrecurringtag"]]) && $props[$meetingrequestproperties["isrecurringtag"]]) {
700
+                $myrec = new SyncMeetingRequestRecurrence();
701
+                // get recurrence -> put $message->meetingrequest as message so the 'alldayevent' is set correctly
702
+                $this->getRecurrence($mapimessage, $props, $message->meetingrequest, $myrec, $tz);
703
+                $message->meetingrequest->recurrences = [$myrec];
704
+            }
705
+
706
+            // Force the 'alldayevent' in the object at all times. (non-existent == 0)
707
+            if (!isset($message->meetingrequest->alldayevent) || $message->meetingrequest->alldayevent == "") {
708
+                $message->meetingrequest->alldayevent = 0;
709
+            }
710
+
711
+            // Instancetype
712
+            // 0 = single appointment
713
+            // 1 = master recurring appointment
714
+            // 2 = single instance of recurring appointment
715
+            // 3 = exception of recurring appointment
716
+            $message->meetingrequest->instancetype = 0;
717
+            if (isset($props[$meetingrequestproperties["isrecurringtag"]]) && $props[$meetingrequestproperties["isrecurringtag"]] == 1) {
718
+                $message->meetingrequest->instancetype = 1;
719
+            }
720
+            elseif ((!isset($props[$meetingrequestproperties["isrecurringtag"]]) || $props[$meetingrequestproperties["isrecurringtag"]] == 0) && isset($message->meetingrequest->recurrenceid)) {
721
+                if (isset($props[$meetingrequestproperties["appSeqNr"]]) && $props[$meetingrequestproperties["appSeqNr"]] == 0) {
722
+                    $message->meetingrequest->instancetype = 2;
723
+                }
724
+                else {
725
+                    $message->meetingrequest->instancetype = 3;
726
+                }
727
+            }
728
+
729
+            // Disable reminder if it is off
730
+            if (!isset($props[$meetingrequestproperties["reminderset"]]) || $props[$meetingrequestproperties["reminderset"]] == false) {
731
+                $message->meetingrequest->reminder = "";
732
+            }
733
+            // the property saves reminder in minutes, but we need it in secs
734
+            else {
735
+                // /set the default reminder time to seconds
736
+                if ($props[$meetingrequestproperties["remindertime"]] == 0x5AE980E1) {
737
+                    $message->meetingrequest->reminder = 900;
738
+                }
739
+                else {
740
+                    $message->meetingrequest->reminder = $props[$meetingrequestproperties["remindertime"]] * 60;
741
+                }
742
+            }
743
+
744
+            // Set sensitivity to 0 if missing
745
+            if (!isset($message->meetingrequest->sensitivity)) {
746
+                $message->meetingrequest->sensitivity = 0;
747
+            }
748
+
749
+            // If the user is working from a location other than the office the busystatus should be interpreted as free.
750
+            if (isset($message->meetingrequest->busystatus) && $message->meetingrequest->busystatus == fbWorkingElsewhere) {
751
+                $message->meetingrequest->busystatus = fbFree;
752
+            }
753
+
754
+            // If the busystatus has the value of -1, we should be interpreted as tentative (1) / ZP-581
755
+            if (isset($message->meetingrequest->busystatus) && $message->meetingrequest->busystatus == -1) {
756
+                $message->meetingrequest->busystatus = fbTentative;
757
+            }
758
+
759
+            // if a meeting request response hasn't been processed yet,
760
+            // do it so that the attendee status is updated on the mobile
761
+            if (!isset($messageprops[$emailproperties["processed"]])) {
762
+                // check if we are not sending the MR so we can process it - ZP-581
763
+                $cuser = GSync::GetBackend()->GetUserDetails(GSync::GetBackend()->GetCurrentUsername());
764
+                if (isset($cuser["emailaddress"]) && $cuser["emailaddress"] != $fromaddr) {
765
+                    if (!isset($req)) {
766
+                        $req = new Meetingrequest($this->store, $mapimessage, $this->session);
767
+                    }
768
+                    if ($req->isMeetingRequestResponse()) {
769
+                        $req->processMeetingRequestResponse();
770
+                    }
771
+                    if ($req->isMeetingCancellation()) {
772
+                        $req->processMeetingCancellation();
773
+                    }
774
+                }
775
+            }
776
+            $message->contentclass = DEFAULT_CALENDAR_CONTENTCLASS;
777
+
778
+            // MeetingMessageType values
779
+            // 0 = A silent update was performed, or the message type is unspecified.
780
+            // 1 = Initial meeting request.
781
+            // 2 = Full update.
782
+            // 3 = Informational update.
783
+            // 4 = Outdated. A newer meeting request or meeting update was received after this message.
784
+            // 5 = Identifies the delegator's copy of the meeting request.
785
+            // 6 = Identifies that the meeting request has been delegated and the meeting request cannot be responded to.
786
+            $message->meetingrequest->meetingmessagetype = mtgEmpty;
787
+
788
+            if (isset($props[$meetingrequestproperties["meetingType"]])) {
789
+                switch ($props[$meetingrequestproperties["meetingType"]]) {
790
+                    case mtgRequest:
791
+                        $message->meetingrequest->meetingmessagetype = 1;
792
+                        break;
793
+
794
+                    case mtgFull:
795
+                        $message->meetingrequest->meetingmessagetype = 2;
796
+                        break;
797
+
798
+                    case mtgInfo:
799
+                        $message->meetingrequest->meetingmessagetype = 3;
800
+                        break;
801
+
802
+                    case mtgOutOfDate:
803
+                        $message->meetingrequest->meetingmessagetype = 4;
804
+                        break;
805
+
806
+                    case mtgDelegatorCopy:
807
+                        $message->meetingrequest->meetingmessagetype = 5;
808
+                        break;
809
+                }
810
+            }
811
+        }
812
+
813
+        // Add attachments
814
+        $attachtable = mapi_message_getattachmenttable($mapimessage);
815
+        $rows = mapi_table_queryallrows($attachtable, [PR_ATTACH_NUM]);
816
+        $entryid = bin2hex($messageprops[$emailproperties["entryid"]]);
817
+        $parentSourcekey = bin2hex($messageprops[$emailproperties["parentsourcekey"]]);
818
+
819
+        foreach ($rows as $row) {
820
+            if (isset($row[PR_ATTACH_NUM])) {
821
+                if (Request::GetProtocolVersion() >= 12.0) {
822
+                    $attach = new SyncBaseAttachment();
823
+                }
824
+                else {
825
+                    $attach = new SyncAttachment();
826
+                }
827
+
828
+                $mapiattach = mapi_message_openattach($mapimessage, $row[PR_ATTACH_NUM]);
829
+                $attachprops = mapi_getprops($mapiattach, [PR_ATTACH_LONG_FILENAME, PR_ATTACH_FILENAME, PR_ATTACHMENT_HIDDEN, PR_ATTACH_CONTENT_ID, PR_ATTACH_CONTENT_ID_W, PR_ATTACH_MIME_TAG, PR_ATTACH_MIME_TAG_W, PR_ATTACH_METHOD, PR_DISPLAY_NAME, PR_DISPLAY_NAME_W, PR_ATTACH_SIZE, PR_ATTACH_FLAGS]);
830
+                if ((isset($attachprops[PR_ATTACH_MIME_TAG]) && strpos(strtolower($attachprops[PR_ATTACH_MIME_TAG]), 'signed') !== false) ||
831
+                    (isset($attachprops[PR_ATTACH_MIME_TAG_W]) && strpos(strtolower($attachprops[PR_ATTACH_MIME_TAG_W]), 'signed') !== false)) {
832
+                    continue;
833
+                }
834
+
835
+                // the displayname is handled equally for all AS versions
836
+                $attach->displayname = w2u((isset($attachprops[PR_ATTACH_LONG_FILENAME])) ? $attachprops[PR_ATTACH_LONG_FILENAME] : ((isset($attachprops[PR_ATTACH_FILENAME])) ? $attachprops[PR_ATTACH_FILENAME] : ((isset($attachprops[PR_DISPLAY_NAME])) ? $attachprops[PR_DISPLAY_NAME] : "attachment.bin")));
837
+                // fix attachment name in case of inline images
838
+                if (($attach->displayname == "inline.txt" && (isset($attachprops[PR_ATTACH_MIME_TAG]) || $attachprops[PR_ATTACH_MIME_TAG_W])) ||
839
+                (substr_compare($attach->displayname, "attachment", 0, 10, true) === 0 && substr_compare($attach->displayname, ".dat", -4, 4, true) === 0)) {
840
+                    $mimetype = (isset($attachprops[PR_ATTACH_MIME_TAG])) ? $attachprops[PR_ATTACH_MIME_TAG] : $attachprops[PR_ATTACH_MIME_TAG_W];
841
+                    $mime = explode("/", $mimetype);
842
+
843
+                    if (count($mime) == 2 && $mime[0] == "image") {
844
+                        $attach->displayname = "inline." . $mime[1];
845
+                    }
846
+                }
847
+
848
+                // set AS version specific parameters
849
+                if (Request::GetProtocolVersion() >= 12.0) {
850
+                    $attach->filereference = sprintf("%s:%s:%s", $entryid, $row[PR_ATTACH_NUM], $parentSourcekey);
851
+                    $attach->method = (isset($attachprops[PR_ATTACH_METHOD])) ? $attachprops[PR_ATTACH_METHOD] : ATTACH_BY_VALUE;
852
+
853
+                    // if displayname does not have the eml extension for embedde messages, android and WP devices won't open it
854
+                    if ($attach->method == ATTACH_EMBEDDED_MSG) {
855
+                        if (strtolower(substr($attach->displayname, -4)) != '.eml') {
856
+                            $attach->displayname .= '.eml';
857
+                        }
858
+                    }
859
+                    // android devices require attachment size in order to display an attachment properly
860
+                    if (!isset($attachprops[PR_ATTACH_SIZE])) {
861
+                        $stream = mapi_openproperty($mapiattach, PR_ATTACH_DATA_BIN, IID_IStream, 0, 0);
862
+                        // It's not possible to open some (embedded only?) messages, so we need to open the attachment object itself to get the data
863
+                        if (mapi_last_hresult()) {
864
+                            $embMessage = mapi_attach_openobj($mapiattach);
865
+                            $addrbook = $this->getAddressbook();
866
+                            $stream = mapi_inetmapi_imtoinet($this->session, $addrbook, $embMessage, ['use_tnef' => -1]);
867
+                        }
868
+                        $stat = mapi_stream_stat($stream);
869
+                        $attach->estimatedDataSize = $stat['cb'];
870
+                    }
871
+                    else {
872
+                        $attach->estimatedDataSize = $attachprops[PR_ATTACH_SIZE];
873
+                    }
874
+
875
+                    if (isset($attachprops[PR_ATTACH_CONTENT_ID]) && $attachprops[PR_ATTACH_CONTENT_ID]) {
876
+                        $attach->contentid = $attachprops[PR_ATTACH_CONTENT_ID];
877
+                    }
878
+
879
+                    if (!isset($attach->contentid) && isset($attachprops[PR_ATTACH_CONTENT_ID_W]) && $attachprops[PR_ATTACH_CONTENT_ID_W]) {
880
+                        $attach->contentid = $attachprops[PR_ATTACH_CONTENT_ID_W];
881
+                    }
882
+
883
+                    if (isset($attachprops[PR_ATTACHMENT_HIDDEN]) && $attachprops[PR_ATTACHMENT_HIDDEN]) {
884
+                        $attach->isinline = 1;
885
+                    }
886
+
887
+                    if (isset($attach->contentid, $attachprops[PR_ATTACH_FLAGS]) && $attachprops[PR_ATTACH_FLAGS] & 4) {
888
+                        $attach->isinline = 1;
889
+                    }
890
+
891
+                    if (!isset($message->asattachments)) {
892
+                        $message->asattachments = [];
893
+                    }
894
+
895
+                    array_push($message->asattachments, $attach);
896
+                }
897
+                else {
898
+                    $attach->attsize = $attachprops[PR_ATTACH_SIZE];
899
+                    $attach->attname = sprintf("%s:%s:%s", $entryid, $row[PR_ATTACH_NUM], $parentSourcekey);
900
+                    if (!isset($message->attachments)) {
901
+                        $message->attachments = [];
902
+                    }
903
+
904
+                    array_push($message->attachments, $attach);
905
+                }
906
+            }
907
+        }
908
+
909
+        // Get To/Cc as SMTP addresses (this is different from displayto and displaycc because we are putting
910
+        // in the SMTP addresses as well, while displayto and displaycc could just contain the display names
911
+        $message->to = [];
912
+        $message->cc = [];
913
+
914
+        $reciptable = mapi_message_getrecipienttable($mapimessage);
915
+        $rows = mapi_table_queryallrows($reciptable, [PR_RECIPIENT_TYPE, PR_DISPLAY_NAME, PR_ADDRTYPE, PR_EMAIL_ADDRESS, PR_SMTP_ADDRESS, PR_ENTRYID, PR_SEARCH_KEY]);
916
+
917
+        foreach ($rows as $row) {
918
+            $address = "";
919
+            $fulladdr = "";
920
+
921
+            $addrtype = isset($row[PR_ADDRTYPE]) ? $row[PR_ADDRTYPE] : "";
922
+
923
+            if (isset($row[PR_SMTP_ADDRESS])) {
924
+                $address = $row[PR_SMTP_ADDRESS];
925
+            }
926
+            elseif ($addrtype == "SMTP" && isset($row[PR_EMAIL_ADDRESS])) {
927
+                $address = $row[PR_EMAIL_ADDRESS];
928
+            }
929
+            elseif ($addrtype == "ZARAFA" && isset($row[PR_ENTRYID])) {
930
+                $address = $this->getSMTPAddressFromEntryID($row[PR_ENTRYID]);
931
+            }
932
+
933
+            // if the user was not found, do a fallback to PR_SEARCH_KEY
934
+            // @see https://jira.z-hub.io/browse/ZP-1178
935
+            if (empty($address) && isset($row[PR_SEARCH_KEY])) {
936
+                $address = $this->getEmailAddressFromSearchKey($row[PR_SEARCH_KEY]);
937
+            }
938
+
939
+            $name = isset($row[PR_DISPLAY_NAME]) ? $row[PR_DISPLAY_NAME] : "";
940
+
941
+            if ($name == "" || $name == $address) {
942
+                $fulladdr = w2u($address);
943
+            }
944
+            else {
945
+                if (substr($name, 0, 1) != '"' && substr($name, -1) != '"') {
946
+                    $fulladdr = "\"" . w2u($name) . "\" <" . w2u($address) . ">";
947
+                }
948
+                else {
949
+                    $fulladdr = w2u($name) . "<" . w2u($address) . ">";
950
+                }
951
+            }
952
+
953
+            if ($row[PR_RECIPIENT_TYPE] == MAPI_TO) {
954
+                array_push($message->to, $fulladdr);
955
+            }
956
+            elseif ($row[PR_RECIPIENT_TYPE] == MAPI_CC) {
957
+                array_push($message->cc, $fulladdr);
958
+            }
959
+        }
960
+
961
+        if (is_array($message->to) && !empty($message->to)) {
962
+            $message->to = implode(", ", $message->to);
963
+        }
964
+        if (is_array($message->cc) && !empty($message->cc)) {
965
+            $message->cc = implode(", ", $message->cc);
966
+        }
967
+
968
+        // without importance some mobiles assume "0" (low) - Mantis #439
969
+        if (!isset($message->importance)) {
970
+            $message->importance = IMPORTANCE_NORMAL;
971
+        }
972
+
973
+        if (!isset($message->internetcpid)) {
974
+            $message->internetcpid = (defined('STORE_INTERNET_CPID')) ? constant('STORE_INTERNET_CPID') : INTERNET_CPID_WINDOWS1252;
975
+        }
976
+        $this->setFlag($mapimessage, $message);
977
+        // TODO checkcontentclass
978
+        if (!isset($message->contentclass)) {
979
+            $message->contentclass = DEFAULT_EMAIL_CONTENTCLASS;
980
+        }
981
+
982
+        if (!isset($message->nativebodytype)) {
983
+            $message->nativebodytype = MAPIUtils::GetNativeBodyType($messageprops);
984
+        }
985
+        elseif ($message->nativebodytype == SYNC_BODYPREFERENCE_UNDEFINED) {
986
+            $nbt = MAPIUtils::GetNativeBodyType($messageprops);
987
+            SLog::Write(LOGLEVEL_INFO, sprintf("MAPIProvider->getEmail(): native body type is undefined. Set it to %d.", $nbt));
988
+            $message->nativebodytype = $nbt;
989
+        }
990
+
991
+        // reply, reply to all, forward flags
992
+        if (isset($message->lastverbexecuted) && $message->lastverbexecuted) {
993
+            $message->lastverbexecuted = Utils::GetLastVerbExecuted($message->lastverbexecuted);
994
+        }
995
+
996
+        return $message;
997
+    }
998
+
999
+    /**
1000
+     * Reads a note object from MAPI.
1001
+     *
1002
+     * @param mixed             $mapimessage
1003
+     * @param ContentParameters $contentparameters
1004
+     *
1005
+     * @return SyncNote
1006
+     */
1007
+    private function getNote($mapimessage, $contentparameters) {
1008
+        $message = new SyncNote();
1009
+
1010
+        // Standard one-to-one mappings first
1011
+        $this->getPropsFromMAPI($message, $mapimessage, MAPIMapping::GetNoteMapping());
1012
+
1013
+        // set the body according to contentparameters and supported AS version
1014
+        $this->setMessageBody($mapimessage, $contentparameters, $message);
1015
+
1016
+        return $message;
1017
+    }
1018
+
1019
+    /**
1020
+     * Creates a SyncFolder from MAPI properties.
1021
+     *
1022
+     * @param mixed $folderprops
1023
+     *
1024
+     * @return SyncFolder
1025
+     */
1026
+    public function GetFolder($folderprops) {
1027
+        $folder = new SyncFolder();
1028
+
1029
+        $storeprops = $this->GetStoreProps();
1030
+
1031
+        // For ZCP 7.0.x we need to retrieve more properties explicitly, see ZP-780
1032
+        if (isset($folderprops[PR_SOURCE_KEY]) && !isset($folderprops[PR_ENTRYID]) && !isset($folderprops[PR_CONTAINER_CLASS])) {
1033
+            $entryid = mapi_msgstore_entryidfromsourcekey($this->store, $folderprops[PR_SOURCE_KEY]);
1034
+            $mapifolder = mapi_msgstore_openentry($this->store, $entryid);
1035
+            $folderprops = mapi_getprops($mapifolder, [PR_DISPLAY_NAME, PR_PARENT_ENTRYID, PR_ENTRYID, PR_SOURCE_KEY, PR_PARENT_SOURCE_KEY, PR_CONTAINER_CLASS, PR_ATTR_HIDDEN, PR_EXTENDED_FOLDER_FLAGS]);
1036
+            SLog::Write(LOGLEVEL_DEBUG, "MAPIProvider->GetFolder(): received insufficient of data from ICS. Fetching required data.");
1037
+        }
1038
+
1039
+        if (!isset(
1040
+                $folderprops[PR_DISPLAY_NAME],
1041
+                $folderprops[PR_PARENT_ENTRYID],
1042
+                $folderprops[PR_SOURCE_KEY],
1043
+                $folderprops[PR_ENTRYID],
1044
+                $folderprops[PR_PARENT_SOURCE_KEY],
1045
+                $storeprops[PR_IPM_SUBTREE_ENTRYID])) {
1046
+            SLog::Write(LOGLEVEL_ERROR, "MAPIProvider->GetFolder(): invalid folder. Missing properties");
1047
+
1048
+            return false;
1049
+        }
1050
+
1051
+        // ignore hidden folders
1052
+        if (isset($folderprops[PR_ATTR_HIDDEN]) && $folderprops[PR_ATTR_HIDDEN] != false) {
1053
+            SLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->GetFolder(): invalid folder '%s' as it is a hidden folder (PR_ATTR_HIDDEN)", $folderprops[PR_DISPLAY_NAME]));
1054
+
1055
+            return false;
1056
+        }
1057
+
1058
+        // ignore certain undesired folders, like "RSS Feeds" and "Suggested contacts"
1059
+        if ((isset($folderprops[PR_CONTAINER_CLASS]) && $folderprops[PR_CONTAINER_CLASS] == "IPF.Note.OutlookHomepage") ||
1060
+                in_array($folderprops[PR_ENTRYID], $this->getSpecialFoldersData())) {
1061
+            SLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->GetFolder(): folder '%s' should not be synchronized", $folderprops[PR_DISPLAY_NAME]));
1062
+
1063
+            return false;
1064
+        }
1065
+
1066
+        $folder->BackendId = bin2hex($folderprops[PR_SOURCE_KEY]);
1067
+        $folderOrigin = DeviceManager::FLD_ORIGIN_USER;
1068
+        if (GSync::GetBackend()->GetImpersonatedUser()) {
1069
+            $folderOrigin = DeviceManager::FLD_ORIGIN_IMPERSONATED;
1070
+        }
1071
+        $folder->serverid = GSync::GetDeviceManager()->GetFolderIdForBackendId($folder->BackendId, true, $folderOrigin, $folderprops[PR_DISPLAY_NAME]);
1072
+        if ($folderprops[PR_PARENT_ENTRYID] == $storeprops[PR_IPM_SUBTREE_ENTRYID] || $folderprops[PR_PARENT_ENTRYID] == $storeprops[PR_IPM_PUBLIC_FOLDERS_ENTRYID]) {
1073
+            $folder->parentid = "0";
1074
+        }
1075
+        else {
1076
+            $folder->parentid = GSync::GetDeviceManager()->GetFolderIdForBackendId(bin2hex($folderprops[PR_PARENT_SOURCE_KEY]));
1077
+        }
1078
+        $folder->displayname = w2u($folderprops[PR_DISPLAY_NAME]);
1079
+        $folder->type = $this->GetFolderType($folderprops[PR_ENTRYID], isset($folderprops[PR_CONTAINER_CLASS]) ? $folderprops[PR_CONTAINER_CLASS] : false);
1080
+
1081
+        return $folder;
1082
+    }
1083
+
1084
+    /**
1085
+     * Returns the foldertype for an entryid
1086
+     * Gets the folder type by checking the default folders in MAPI.
1087
+     *
1088
+     * @param string $entryid
1089
+     * @param string $class   (opt)
1090
+     *
1091
+     * @return long
1092
+     */
1093
+    public function GetFolderType($entryid, $class = false) {
1094
+        $storeprops = $this->GetStoreProps();
1095
+        $inboxprops = $this->GetInboxProps();
1096
+
1097
+        if ($entryid == $storeprops[PR_IPM_WASTEBASKET_ENTRYID]) {
1098
+            return SYNC_FOLDER_TYPE_WASTEBASKET;
1099
+        }
1100
+        if ($entryid == $storeprops[PR_IPM_SENTMAIL_ENTRYID]) {
1101
+            return SYNC_FOLDER_TYPE_SENTMAIL;
1102
+        }
1103
+        if ($entryid == $storeprops[PR_IPM_OUTBOX_ENTRYID]) {
1104
+            return SYNC_FOLDER_TYPE_OUTBOX;
1105
+        }
1106
+
1107
+        // Public folders do not have inboxprops
1108
+        // @see https://jira.z-hub.io/browse/ZP-995
1109
+        if (!empty($inboxprops)) {
1110
+            if ($entryid == $inboxprops[PR_ENTRYID]) {
1111
+                return SYNC_FOLDER_TYPE_INBOX;
1112
+            }
1113
+            if ($entryid == $inboxprops[PR_IPM_DRAFTS_ENTRYID]) {
1114
+                return SYNC_FOLDER_TYPE_DRAFTS;
1115
+            }
1116
+            if ($entryid == $inboxprops[PR_IPM_TASK_ENTRYID]) {
1117
+                return SYNC_FOLDER_TYPE_TASK;
1118
+            }
1119
+            if ($entryid == $inboxprops[PR_IPM_APPOINTMENT_ENTRYID]) {
1120
+                return SYNC_FOLDER_TYPE_APPOINTMENT;
1121
+            }
1122
+            if ($entryid == $inboxprops[PR_IPM_CONTACT_ENTRYID]) {
1123
+                return SYNC_FOLDER_TYPE_CONTACT;
1124
+            }
1125
+            if ($entryid == $inboxprops[PR_IPM_NOTE_ENTRYID]) {
1126
+                return SYNC_FOLDER_TYPE_NOTE;
1127
+            }
1128
+            if ($entryid == $inboxprops[PR_IPM_JOURNAL_ENTRYID]) {
1129
+                return SYNC_FOLDER_TYPE_JOURNAL;
1130
+            }
1131
+        }
1132
+
1133
+        // user created folders
1134
+        if ($class == "IPF.Note") {
1135
+            return SYNC_FOLDER_TYPE_USER_MAIL;
1136
+        }
1137
+        if ($class == "IPF.Task") {
1138
+            return SYNC_FOLDER_TYPE_USER_TASK;
1139
+        }
1140
+        if ($class == "IPF.Appointment") {
1141
+            return SYNC_FOLDER_TYPE_USER_APPOINTMENT;
1142
+        }
1143
+        if ($class == "IPF.Contact") {
1144
+            return SYNC_FOLDER_TYPE_USER_CONTACT;
1145
+        }
1146
+        if ($class == "IPF.StickyNote") {
1147
+            return SYNC_FOLDER_TYPE_USER_NOTE;
1148
+        }
1149
+        if ($class == "IPF.Journal") {
1150
+            return SYNC_FOLDER_TYPE_USER_JOURNAL;
1151
+        }
1152
+
1153
+        return SYNC_FOLDER_TYPE_OTHER;
1154
+    }
1155
+
1156
+    /**
1157
+     * Indicates if the entry id is a default MAPI folder.
1158
+     *
1159
+     * @param string $entryid
1160
+     *
1161
+     * @return bool
1162
+     */
1163
+    public function IsMAPIDefaultFolder($entryid) {
1164
+        $msgstore_props = mapi_getprops($this->store, [PR_ENTRYID, PR_DISPLAY_NAME, PR_IPM_SUBTREE_ENTRYID, PR_IPM_OUTBOX_ENTRYID, PR_IPM_SENTMAIL_ENTRYID, PR_IPM_WASTEBASKET_ENTRYID, PR_MDB_PROVIDER, PR_IPM_PUBLIC_FOLDERS_ENTRYID, PR_IPM_FAVORITES_ENTRYID, PR_MAILBOX_OWNER_ENTRYID]);
1165
+
1166
+        $inboxProps = [];
1167
+        $inbox = mapi_msgstore_getreceivefolder($this->store);
1168
+        if (!mapi_last_hresult()) {
1169
+            $inboxProps = mapi_getprops($inbox, [PR_ENTRYID]);
1170
+        }
1171
+
1172
+        $root = mapi_msgstore_openentry($this->store, null); // TODO use getRootProps()
1173
+        $rootProps = mapi_getprops($root, [PR_IPM_APPOINTMENT_ENTRYID, PR_IPM_CONTACT_ENTRYID, PR_IPM_DRAFTS_ENTRYID, PR_IPM_JOURNAL_ENTRYID, PR_IPM_NOTE_ENTRYID, PR_IPM_TASK_ENTRYID, PR_ADDITIONAL_REN_ENTRYIDS]);
1174
+
1175
+        $additional_ren_entryids = [];
1176
+        if (isset($rootProps[PR_ADDITIONAL_REN_ENTRYIDS])) {
1177
+            $additional_ren_entryids = $rootProps[PR_ADDITIONAL_REN_ENTRYIDS];
1178
+        }
1179
+
1180
+        $defaultfolders = [
1181
+            "inbox" => ["inbox" => PR_ENTRYID],
1182
+            "outbox" => ["store" => PR_IPM_OUTBOX_ENTRYID],
1183
+            "sent" => ["store" => PR_IPM_SENTMAIL_ENTRYID],
1184
+            "wastebasket" => ["store" => PR_IPM_WASTEBASKET_ENTRYID],
1185
+            "favorites" => ["store" => PR_IPM_FAVORITES_ENTRYID],
1186
+            "publicfolders" => ["store" => PR_IPM_PUBLIC_FOLDERS_ENTRYID],
1187
+            "calendar" => ["root" => PR_IPM_APPOINTMENT_ENTRYID],
1188
+            "contact" => ["root" => PR_IPM_CONTACT_ENTRYID],
1189
+            "drafts" => ["root" => PR_IPM_DRAFTS_ENTRYID],
1190
+            "journal" => ["root" => PR_IPM_JOURNAL_ENTRYID],
1191
+            "note" => ["root" => PR_IPM_NOTE_ENTRYID],
1192
+            "task" => ["root" => PR_IPM_TASK_ENTRYID],
1193
+            "junk" => ["additional" => 4],
1194
+            "syncissues" => ["additional" => 1],
1195
+            "conflicts" => ["additional" => 0],
1196
+            "localfailures" => ["additional" => 2],
1197
+            "serverfailures" => ["additional" => 3],
1198
+        ];
1199
+
1200
+        foreach ($defaultfolders as $key => $prop) {
1201
+            $tag = reset($prop);
1202
+            $from = key($prop);
1203
+
1204
+            switch ($from) {
1205
+                case "inbox":
1206
+                    if (isset($inboxProps[$tag]) && $entryid == $inboxProps[$tag]) {
1207
+                        SLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->IsMAPIFolder(): Inbox found, key '%s'", $key));
1208
+
1209
+                        return true;
1210
+                    }
1211
+                    break;
1212
+
1213
+                case "store":
1214
+                    if (isset($msgstore_props[$tag]) && $entryid == $msgstore_props[$tag]) {
1215
+                        SLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->IsMAPIFolder(): Store folder found, key '%s'", $key));
1216
+
1217
+                        return true;
1218
+                    }
1219
+                    break;
1220
+
1221
+                case "root":
1222
+                    if (isset($rootProps[$tag]) && $entryid == $rootProps[$tag]) {
1223
+                        SLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->IsMAPIFolder(): Root folder found, key '%s'", $key));
1224
+
1225
+                        return true;
1226
+                    }
1227
+                    break;
1228
+
1229
+                case "additional":
1230
+                    if (isset($additional_ren_entryids[$tag]) && $entryid == $additional_ren_entryids[$tag]) {
1231
+                        SLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->IsMAPIFolder(): Additional folder found, key '%s'", $key));
1232
+
1233
+                        return true;
1234
+                    }
1235
+                    break;
1236
+            }
1237
+        }
1238
+
1239
+        return false;
1240
+    }
1241
+
1242
+    /*----------------------------------------------------------------------------------------------------------
1243 1243
 	 * SETTER
1244 1244
 	 */
1245 1245
 
1246
-	/**
1247
-	 * Writes a SyncObject to MAPI
1248
-	 * Depending on the message class, a contact, appointment, task or email is written.
1249
-	 *
1250
-	 * @param mixed      $mapimessage
1251
-	 * @param SyncObject $message
1252
-	 *
1253
-	 * @return bool
1254
-	 */
1255
-	public function SetMessage($mapimessage, $message) {
1256
-		// TODO check with instanceof
1257
-		switch (strtolower(get_class($message))) {
1258
-			case "synccontact":
1259
-				return $this->setContact($mapimessage, $message);
1260
-
1261
-			case "syncappointment":
1262
-				return $this->setAppointment($mapimessage, $message);
1263
-
1264
-			case "synctask":
1265
-				return $this->setTask($mapimessage, $message);
1266
-
1267
-			case "syncnote":
1268
-				return $this->setNote($mapimessage, $message);
1269
-
1270
-			default:
1271
-				// for emails only flag (read and todo) changes are possible
1272
-				return $this->setEmail($mapimessage, $message);
1273
-		}
1274
-	}
1275
-
1276
-	/**
1277
-	 * Writes SyncMail to MAPI (actually flags only).
1278
-	 *
1279
-	 * @param mixed    $mapimessage
1280
-	 * @param SyncMail $message
1281
-	 */
1282
-	private function setEmail($mapimessage, $message) {
1283
-		// update categories
1284
-		if (!isset($message->categories)) {
1285
-			$message->categories = [];
1286
-		}
1287
-		$emailmap = MAPIMapping::GetEmailMapping();
1288
-		$this->setPropsInMAPI($mapimessage, $message, ["categories" => $emailmap["categories"]]);
1289
-
1290
-		$flagmapping = MAPIMapping::GetMailFlagsMapping();
1291
-		$flagprops = MAPIMapping::GetMailFlagsProperties();
1292
-		$flagprops = array_merge($this->getPropIdsFromStrings($flagmapping), $this->getPropIdsFromStrings($flagprops));
1293
-		// flag specific properties to be set
1294
-		$props = $delprops = [];
1295
-		// unset message flags if:
1296
-		// flag is not set
1297
-		if (empty($message->flag) ||
1298
-			// flag status is not set
1299
-			!isset($message->flag->flagstatus) ||
1300
-			// flag status is 0 or empty
1301
-			(isset($message->flag->flagstatus) && ($message->flag->flagstatus == 0 || $message->flag->flagstatus == ""))) {
1302
-			// if message flag is empty, some properties need to be deleted
1303
-			// and some set to 0 or false
1304
-
1305
-			$props[$flagprops["todoitemsflags"]] = 0;
1306
-			$props[$flagprops["status"]] = 0;
1307
-			$props[$flagprops["completion"]] = 0.0;
1308
-			$props[$flagprops["flagtype"]] = "";
1309
-			$props[$flagprops["ordinaldate"]] = 0x7FFFFFFF; // ordinal date is 12am 1.1.4501, set it to max possible value
1310
-			$props[$flagprops["subordinaldate"]] = "";
1311
-			$props[$flagprops["replyrequested"]] = false;
1312
-			$props[$flagprops["responserequested"]] = false;
1313
-			$props[$flagprops["reminderset"]] = false;
1314
-			$props[$flagprops["complete"]] = false;
1315
-
1316
-			$delprops[] = $flagprops["todotitle"];
1317
-			$delprops[] = $flagprops["duedate"];
1318
-			$delprops[] = $flagprops["startdate"];
1319
-			$delprops[] = $flagprops["datecompleted"];
1320
-			$delprops[] = $flagprops["utcstartdate"];
1321
-			$delprops[] = $flagprops["utcduedate"];
1322
-			$delprops[] = $flagprops["completetime"];
1323
-			$delprops[] = $flagprops["flagstatus"];
1324
-			$delprops[] = $flagprops["flagicon"];
1325
-		}
1326
-		else {
1327
-			$this->setPropsInMAPI($mapimessage, $message->flag, $flagmapping);
1328
-			$props[$flagprops["todoitemsflags"]] = 1;
1329
-			if (isset($message->subject) && strlen($message->subject) > 0) {
1330
-				$props[$flagprops["todotitle"]] = $message->subject;
1331
-			}
1332
-			// ordinal date is utc current time
1333
-			if (!isset($message->flag->ordinaldate) || empty($message->flag->ordinaldate)) {
1334
-				$props[$flagprops["ordinaldate"]] = time();
1335
-			}
1336
-			// the default value
1337
-			if (!isset($message->flag->subordinaldate) || empty($message->flag->subordinaldate)) {
1338
-				$props[$flagprops["subordinaldate"]] = "5555555";
1339
-			}
1340
-			$props[$flagprops["flagicon"]] = 6; // red flag icon
1341
-			$props[$flagprops["replyrequested"]] = true;
1342
-			$props[$flagprops["responserequested"]] = true;
1343
-
1344
-			if ($message->flag->flagstatus == SYNC_FLAGSTATUS_COMPLETE) {
1345
-				$props[$flagprops["status"]] = olTaskComplete;
1346
-				$props[$flagprops["completion"]] = 1.0;
1347
-				$props[$flagprops["complete"]] = true;
1348
-				$props[$flagprops["replyrequested"]] = false;
1349
-				$props[$flagprops["responserequested"]] = false;
1350
-				unset($props[$flagprops["flagicon"]]);
1351
-				$delprops[] = $flagprops["flagicon"];
1352
-			}
1353
-		}
1354
-
1355
-		if (!empty($props)) {
1356
-			mapi_setprops($mapimessage, $props);
1357
-		}
1358
-		if (!empty($delprops)) {
1359
-			mapi_deleteprops($mapimessage, $delprops);
1360
-		}
1361
-	}
1362
-
1363
-	/**
1364
-	 * Writes a SyncAppointment to MAPI.
1365
-	 *
1366
-	 * @param mixed           $mapimessage
1367
-	 * @param SyncAppointment $message
1368
-	 * @param mixed           $appointment
1369
-	 *
1370
-	 * @return bool
1371
-	 */
1372
-	private function setAppointment($mapimessage, $appointment) {
1373
-		// Get timezone info
1374
-		if (isset($appointment->timezone)) {
1375
-			$tz = $this->getTZFromSyncBlob(base64_decode($appointment->timezone));
1376
-		}
1377
-		else {
1378
-			$tz = false;
1379
-		}
1380
-
1381
-		// start and end time may not be set - try to get them from the existing appointment for further calculation - see https://jira.z-hub.io/browse/ZP-983
1382
-		if (!isset($appointment->starttime) || !isset($appointment->endtime)) {
1383
-			$amapping = MAPIMapping::GetAppointmentMapping();
1384
-			$amapping = $this->getPropIdsFromStrings($amapping);
1385
-			$existingstartendpropsmap = [$amapping["starttime"], $amapping["endtime"]];
1386
-			$existingstartendprops = $this->getProps($mapimessage, $existingstartendpropsmap);
1387
-
1388
-			if (isset($existingstartendprops[$amapping["starttime"]]) && !isset($appointment->starttime)) {
1389
-				$appointment->starttime = $existingstartendprops[$amapping["starttime"]];
1390
-				SLog::Write(LOGLEVEL_WBXML, sprintf("MAPIProvider->setAppointment(): Parameter 'starttime' was not set, using value from MAPI %d (%s).", $appointment->starttime, gmstrftime("%Y%m%dT%H%M%SZ", $appointment->starttime)));
1391
-			}
1392
-			if (isset($existingstartendprops[$amapping["endtime"]]) && !isset($appointment->endtime)) {
1393
-				$appointment->endtime = $existingstartendprops[$amapping["endtime"]];
1394
-				SLog::Write(LOGLEVEL_WBXML, sprintf("MAPIProvider->setAppointment(): Parameter 'endtime' was not set, using value from MAPI %d (%s).", $appointment->endtime, gmstrftime("%Y%m%dT%H%M%SZ", $appointment->endtime)));
1395
-			}
1396
-		}
1397
-		if (!isset($appointment->starttime) || !isset($appointment->endtime)) {
1398
-			throw new StatusException("MAPIProvider->setAppointment(): Error, start and/or end time not set and can not be retrieved from MAPI.", SYNC_STATUS_SYNCCANNOTBECOMPLETED);
1399
-		}
1400
-
1401
-		// calculate duration because without it some webaccess views are broken. duration is in min
1402
-		$localstart = $this->getLocaltimeByTZ($appointment->starttime, $tz);
1403
-		$localend = $this->getLocaltimeByTZ($appointment->endtime, $tz);
1404
-		$duration = ($localend - $localstart) / 60;
1405
-
1406
-		// nokia sends an yearly event with 0 mins duration but as all day event,
1407
-		// so make it end next day
1408
-		if ($appointment->starttime == $appointment->endtime && isset($appointment->alldayevent) && $appointment->alldayevent) {
1409
-			$duration = 1440;
1410
-			$appointment->endtime = $appointment->starttime + 24 * 60 * 60;
1411
-			$localend = $localstart + 24 * 60 * 60;
1412
-		}
1413
-
1414
-		// is the transmitted UID OL compatible?
1415
-		// if not, encapsulate the transmitted uid
1416
-		$appointment->uid = Utils::GetOLUidFromICalUid($appointment->uid);
1417
-
1418
-		mapi_setprops($mapimessage, [PR_MESSAGE_CLASS => "IPM.Appointment"]);
1419
-
1420
-		$appointmentmapping = MAPIMapping::GetAppointmentMapping();
1421
-		$this->setPropsInMAPI($mapimessage, $appointment, $appointmentmapping);
1422
-		$appointmentprops = MAPIMapping::GetAppointmentProperties();
1423
-		$appointmentprops = array_merge($this->getPropIdsFromStrings($appointmentmapping), $this->getPropIdsFromStrings($appointmentprops));
1424
-		// appointment specific properties to be set
1425
-		$props = [];
1426
-
1427
-		// sensitivity is not enough to mark an appointment as private, so we use another mapi tag
1428
-		$private = (isset($appointment->sensitivity) && $appointment->sensitivity >= SENSITIVITY_PRIVATE) ? true : false;
1429
-
1430
-		// Set commonstart/commonend to start/end and remindertime to start, duration, private and cleanGlobalObjectId
1431
-		$props[$appointmentprops["commonstart"]] = $appointment->starttime;
1432
-		$props[$appointmentprops["commonend"]] = $appointment->endtime;
1433
-		$props[$appointmentprops["reminderstart"]] = $appointment->starttime;
1434
-		// Set reminder boolean to 'true' if reminder is set
1435
-		$props[$appointmentprops["reminderset"]] = isset($appointment->reminder) ? true : false;
1436
-		$props[$appointmentprops["duration"]] = $duration;
1437
-		$props[$appointmentprops["private"]] = $private;
1438
-		$props[$appointmentprops["uid"]] = $appointment->uid;
1439
-		// Set named prop 8510, unknown property, but enables deleting a single occurrence of a recurring
1440
-		// type in OLK2003.
1441
-		$props[$appointmentprops["sideeffects"]] = 369;
1442
-
1443
-		if (isset($appointment->reminder) && $appointment->reminder >= 0) {
1444
-			// Set 'flagdueby' to correct value (start - reminderminutes)
1445
-			$props[$appointmentprops["flagdueby"]] = $appointment->starttime - $appointment->reminder * 60;
1446
-			$props[$appointmentprops["remindertime"]] = $appointment->reminder;
1447
-		}
1448
-		// unset the reminder
1449
-		else {
1450
-			$props[$appointmentprops["reminderset"]] = false;
1451
-		}
1452
-
1453
-		if (isset($appointment->asbody)) {
1454
-			$this->setASbody($appointment->asbody, $props, $appointmentprops);
1455
-		}
1456
-
1457
-		if ($tz !== false) {
1458
-			$props[$appointmentprops["timezonetag"]] = $this->getMAPIBlobFromTZ($tz);
1459
-		}
1460
-
1461
-		if (isset($appointment->recurrence)) {
1462
-			// Set PR_ICON_INDEX to 1025 to show correct icon in category view
1463
-			$props[$appointmentprops["icon"]] = 1025;
1464
-
1465
-			// if there aren't any exceptions, use the 'old style' set recurrence
1466
-			$noexceptions = true;
1467
-
1468
-			$recurrence = new Recurrence($this->store, $mapimessage);
1469
-			$recur = [];
1470
-			$this->setRecurrence($appointment, $recur);
1471
-
1472
-			// set the recurrence type to that of the MAPI
1473
-			$props[$appointmentprops["recurrencetype"]] = $recur["recurrencetype"];
1474
-
1475
-			$starttime = $this->gmtime($localstart);
1476
-			$endtime = $this->gmtime($localend);
1477
-
1478
-			// set recurrence start here because it's calculated differently for tasks and appointments
1479
-			$recur["start"] = $this->getDayStartOfTimestamp($this->getGMTTimeByTZ($localstart, $tz));
1480
-
1481
-			$recur["startocc"] = $starttime["tm_hour"] * 60 + $starttime["tm_min"];
1482
-			$recur["endocc"] = $recur["startocc"] + $duration; // Note that this may be > 24*60 if multi-day
1483
-
1484
-			// only tasks can regenerate
1485
-			$recur["regen"] = false;
1486
-
1487
-			// Process exceptions. The PDA will send all exceptions for this recurring item.
1488
-			if (isset($appointment->exceptions)) {
1489
-				foreach ($appointment->exceptions as $exception) {
1490
-					// we always need the base date
1491
-					if (!isset($exception->exceptionstarttime)) {
1492
-						continue;
1493
-					}
1494
-
1495
-					$basedate = $this->getDayStartOfTimestamp($exception->exceptionstarttime);
1496
-					if (isset($exception->deleted) && $exception->deleted) {
1497
-						$noexceptions = false;
1498
-						// Delete exception
1499
-						$recurrence->createException([], $basedate, true);
1500
-					}
1501
-					else {
1502
-						// Change exception
1503
-						$mapiexception = ["basedate" => $basedate];
1504
-						// other exception properties which are not handled in recurrence
1505
-						$exceptionprops = [];
1506
-
1507
-						if (isset($exception->starttime)) {
1508
-							$mapiexception["start"] = $this->getLocaltimeByTZ($exception->starttime, $tz);
1509
-							$exceptionprops[$appointmentprops["starttime"]] = $exception->starttime;
1510
-						}
1511
-						if (isset($exception->endtime)) {
1512
-							$mapiexception["end"] = $this->getLocaltimeByTZ($exception->endtime, $tz);
1513
-							$exceptionprops[$appointmentprops["endtime"]] = $exception->endtime;
1514
-						}
1515
-						if (isset($exception->subject)) {
1516
-							$exceptionprops[$appointmentprops["subject"]] = $mapiexception["subject"] = u2w($exception->subject);
1517
-						}
1518
-						if (isset($exception->location)) {
1519
-							$exceptionprops[$appointmentprops["location"]] = $mapiexception["location"] = u2w($exception->location);
1520
-						}
1521
-						if (isset($exception->busystatus)) {
1522
-							$exceptionprops[$appointmentprops["busystatus"]] = $mapiexception["busystatus"] = $exception->busystatus;
1523
-						}
1524
-						if (isset($exception->reminder)) {
1525
-							$exceptionprops[$appointmentprops["reminderset"]] = $mapiexception["reminder_set"] = 1;
1526
-							$exceptionprops[$appointmentprops["remindertime"]] = $mapiexception["remind_before"] = $exception->reminder;
1527
-						}
1528
-						if (isset($exception->alldayevent)) {
1529
-							$exceptionprops[$appointmentprops["alldayevent"]] = $mapiexception["alldayevent"] = $exception->alldayevent;
1530
-						}
1531
-
1532
-						if (!isset($recur["changed_occurrences"])) {
1533
-							$recur["changed_occurrences"] = [];
1534
-						}
1535
-
1536
-						if (isset($exception->body)) {
1537
-							$exceptionprops[$appointmentprops["body"]] = u2w($exception->body);
1538
-						}
1539
-
1540
-						if (isset($exception->asbody)) {
1541
-							$this->setASbody($exception->asbody, $exceptionprops, $appointmentprops);
1542
-							$mapiexception["body"] = $exceptionprops[$appointmentprops["body"]] =
1543
-								(isset($exceptionprops[$appointmentprops["body"]])) ? $exceptionprops[$appointmentprops["body"]] :
1544
-								((isset($exceptionprops[$appointmentprops["html"]])) ? $exceptionprops[$appointmentprops["html"]] : "");
1545
-						}
1546
-
1547
-						array_push($recur["changed_occurrences"], $mapiexception);
1548
-
1549
-						if (!empty($exceptionprops)) {
1550
-							$noexceptions = false;
1551
-							if ($recurrence->isException($basedate)) {
1552
-								$recurrence->modifyException($exceptionprops, $basedate);
1553
-							}
1554
-							else {
1555
-								$recurrence->createException($exceptionprops, $basedate);
1556
-							}
1557
-						}
1558
-					}
1559
-				}
1560
-			}
1561
-
1562
-			// setRecurrence deletes the attachments from an appointment
1563
-			if ($noexceptions) {
1564
-				$recurrence->setRecurrence($tz, $recur);
1565
-			}
1566
-		}
1567
-		else {
1568
-			$props[$appointmentprops["isrecurring"]] = false;
1569
-		}
1570
-
1571
-		// always set the PR_SENT_REPRESENTING_* props so that the attendee status update also works with the webaccess
1572
-		$p = [$appointmentprops["representingentryid"], $appointmentprops["representingname"], $appointmentprops["sentrepresentingaddt"],
1573
-			$appointmentprops["sentrepresentingemail"], $appointmentprops["sentrepresentinsrchk"], $appointmentprops["responsestatus"], ];
1574
-		$representingprops = $this->getProps($mapimessage, $p);
1575
-
1576
-		if (!isset($representingprops[$appointmentprops["representingentryid"]])) {
1577
-			// TODO use GetStoreProps
1578
-			$storeProps = mapi_getprops($this->store, [PR_MAILBOX_OWNER_ENTRYID]);
1579
-			$props[$appointmentprops["representingentryid"]] = $storeProps[PR_MAILBOX_OWNER_ENTRYID];
1580
-			$displayname = $this->getFullnameFromEntryID($storeProps[PR_MAILBOX_OWNER_ENTRYID]);
1581
-
1582
-			$props[$appointmentprops["representingname"]] = ($displayname !== false) ? $displayname : Request::GetUser();
1583
-			$props[$appointmentprops["sentrepresentingemail"]] = Request::GetUser();
1584
-			$props[$appointmentprops["sentrepresentingaddt"]] = "ZARAFA";
1585
-			$props[$appointmentprops["sentrepresentinsrchk"]] = $props[$appointmentprops["sentrepresentingaddt"]] . ":" . $props[$appointmentprops["sentrepresentingemail"]];
1586
-
1587
-			if (isset($appointment->attendees) && is_array($appointment->attendees) && !empty($appointment->attendees)) {
1588
-				$props[$appointmentprops["icon"]] = 1026;
1589
-				// the user is the organizer
1590
-				// set these properties to show tracking tab in webapp
1591
-
1592
-				$props[$appointmentprops["mrwassent"]] = true;
1593
-				$props[$appointmentprops["responsestatus"]] = olResponseOrganized;
1594
-				$props[$appointmentprops["meetingstatus"]] = olMeeting;
1595
-			}
1596
-		}
1597
-		// we also have to set the responsestatus and not only meetingstatus, so we use another mapi tag
1598
-		if (!isset($props[$appointmentprops["responsestatus"]])) {
1599
-			if (isset($appointment->responsetype)) {
1600
-				$props[$appointmentprops["responsestatus"]] = $appointment->responsetype;
1601
-			}
1602
-			// only set responsestatus to none if it is not set on the server
1603
-			elseif (!isset($representingprops[$appointmentprops["responsestatus"]])) {
1604
-				$props[$appointmentprops["responsestatus"]] = olResponseNone;
1605
-			}
1606
-		}
1607
-
1608
-		// Do attendees
1609
-		if (isset($appointment->attendees) && is_array($appointment->attendees)) {
1610
-			$recips = [];
1611
-
1612
-			// Outlook XP requires organizer in the attendee list as well
1613
-			$org = [];
1614
-			$org[PR_ENTRYID] = isset($representingprops[$appointmentprops["representingentryid"]]) ? $representingprops[$appointmentprops["representingentryid"]] : $props[$appointmentprops["representingentryid"]];
1615
-			$org[PR_DISPLAY_NAME] = isset($representingprops[$appointmentprops["representingname"]]) ? $representingprops[$appointmentprops["representingname"]] : $props[$appointmentprops["representingname"]];
1616
-			$org[PR_ADDRTYPE] = isset($representingprops[$appointmentprops["sentrepresentingaddt"]]) ? $representingprops[$appointmentprops["sentrepresentingaddt"]] : $props[$appointmentprops["sentrepresentingaddt"]];
1617
-			$org[PR_SMTP_ADDRESS] = $org[PR_EMAIL_ADDRESS] = isset($representingprops[$appointmentprops["sentrepresentingemail"]]) ? $representingprops[$appointmentprops["sentrepresentingemail"]] : $props[$appointmentprops["sentrepresentingemail"]];
1618
-			$org[PR_SEARCH_KEY] = isset($representingprops[$appointmentprops["sentrepresentinsrchk"]]) ? $representingprops[$appointmentprops["sentrepresentinsrchk"]] : $props[$appointmentprops["sentrepresentinsrchk"]];
1619
-			$org[PR_RECIPIENT_FLAGS] = recipOrganizer | recipSendable;
1620
-			$org[PR_RECIPIENT_TYPE] = MAPI_ORIG;
1621
-
1622
-			array_push($recips, $org);
1623
-
1624
-			// Open address book for user resolve
1625
-			$addrbook = $this->getAddressbook();
1626
-			foreach ($appointment->attendees as $attendee) {
1627
-				$recip = [];
1628
-				$recip[PR_EMAIL_ADDRESS] = u2w($attendee->email);
1629
-				$recip[PR_SMTP_ADDRESS] = u2w($attendee->email);
1630
-
1631
-				// lookup information in GAB if possible so we have up-to-date name for given address
1632
-				$userinfo = [[PR_DISPLAY_NAME => $recip[PR_EMAIL_ADDRESS]]];
1633
-				$userinfo = mapi_ab_resolvename($addrbook, $userinfo, EMS_AB_ADDRESS_LOOKUP);
1634
-				if (mapi_last_hresult() == NOERROR) {
1635
-					$recip[PR_DISPLAY_NAME] = $userinfo[0][PR_DISPLAY_NAME];
1636
-					$recip[PR_EMAIL_ADDRESS] = $userinfo[0][PR_EMAIL_ADDRESS];
1637
-					$recip[PR_SEARCH_KEY] = $userinfo[0][PR_SEARCH_KEY];
1638
-					$recip[PR_ADDRTYPE] = $userinfo[0][PR_ADDRTYPE];
1639
-					$recip[PR_ENTRYID] = $userinfo[0][PR_ENTRYID];
1640
-					$recip[PR_RECIPIENT_TYPE] = isset($attendee->attendeetype) ? $attendee->attendeetype : MAPI_TO;
1641
-					$recip[PR_RECIPIENT_FLAGS] = recipSendable;
1642
-					$recip[PR_RECIPIENT_TRACKSTATUS] = isset($attendee->attendeestatus) ? $attendee->attendeestatus : olResponseNone;
1643
-				}
1644
-				else {
1645
-					$recip[PR_DISPLAY_NAME] = u2w($attendee->name);
1646
-					$recip[PR_SEARCH_KEY] = "SMTP:" . $recip[PR_EMAIL_ADDRESS] . "\0";
1647
-					$recip[PR_ADDRTYPE] = "SMTP";
1648
-					$recip[PR_RECIPIENT_TYPE] = isset($attendee->attendeetype) ? $attendee->attendeetype : MAPI_TO;
1649
-					$recip[PR_ENTRYID] = mapi_createoneoff($recip[PR_DISPLAY_NAME], $recip[PR_ADDRTYPE], $recip[PR_EMAIL_ADDRESS]);
1650
-				}
1651
-
1652
-				array_push($recips, $recip);
1653
-			}
1654
-
1655
-			mapi_message_modifyrecipients($mapimessage, 0, $recips);
1656
-		}
1657
-		mapi_setprops($mapimessage, $props);
1658
-
1659
-		return true;
1660
-	}
1661
-
1662
-	/**
1663
-	 * Writes a SyncContact to MAPI.
1664
-	 *
1665
-	 * @param mixed       $mapimessage
1666
-	 * @param SyncContact $contact
1667
-	 *
1668
-	 * @return bool
1669
-	 */
1670
-	private function setContact($mapimessage, $contact) {
1671
-		mapi_setprops($mapimessage, [PR_MESSAGE_CLASS => "IPM.Contact"]);
1672
-
1673
-		// normalize email addresses
1674
-		if (isset($contact->email1address) && (($contact->email1address = $this->extractEmailAddress($contact->email1address)) === false)) {
1675
-			unset($contact->email1address);
1676
-		}
1677
-
1678
-		if (isset($contact->email2address) && (($contact->email2address = $this->extractEmailAddress($contact->email2address)) === false)) {
1679
-			unset($contact->email2address);
1680
-		}
1681
-
1682
-		if (isset($contact->email3address) && (($contact->email3address = $this->extractEmailAddress($contact->email3address)) === false)) {
1683
-			unset($contact->email3address);
1684
-		}
1685
-
1686
-		$contactmapping = MAPIMapping::GetContactMapping();
1687
-		$contactprops = MAPIMapping::GetContactProperties();
1688
-		$this->setPropsInMAPI($mapimessage, $contact, $contactmapping);
1689
-
1690
-		// /set display name from contact's properties
1691
-		$cname = $this->composeDisplayName($contact);
1692
-
1693
-		// get contact specific mapi properties and merge them with the AS properties
1694
-		$contactprops = array_merge($this->getPropIdsFromStrings($contactmapping), $this->getPropIdsFromStrings($contactprops));
1695
-
1696
-		// contact specific properties to be set
1697
-		$props = [];
1698
-
1699
-		// need to be set in order to show contacts properly in outlook and wa
1700
-		$nremails = [];
1701
-		$abprovidertype = 0;
1702
-
1703
-		if (isset($contact->email1address)) {
1704
-			$this->setEmailAddress($contact->email1address, $cname, 1, $props, $contactprops, $nremails, $abprovidertype);
1705
-		}
1706
-		if (isset($contact->email2address)) {
1707
-			$this->setEmailAddress($contact->email2address, $cname, 2, $props, $contactprops, $nremails, $abprovidertype);
1708
-		}
1709
-		if (isset($contact->email3address)) {
1710
-			$this->setEmailAddress($contact->email3address, $cname, 3, $props, $contactprops, $nremails, $abprovidertype);
1711
-		}
1712
-
1713
-		$props[$contactprops["addressbooklong"]] = $abprovidertype;
1714
-		$props[$contactprops["displayname"]] = $props[$contactprops["subject"]] = $cname;
1715
-
1716
-		// pda multiple e-mail addresses bug fix for the contact
1717
-		if (!empty($nremails)) {
1718
-			$props[$contactprops["addressbookmv"]] = $nremails;
1719
-		}
1720
-
1721
-		// set addresses
1722
-		$this->setAddress("home", $contact->homecity, $contact->homecountry, $contact->homepostalcode, $contact->homestate, $contact->homestreet, $props, $contactprops);
1723
-		$this->setAddress("business", $contact->businesscity, $contact->businesscountry, $contact->businesspostalcode, $contact->businessstate, $contact->businessstreet, $props, $contactprops);
1724
-		$this->setAddress("other", $contact->othercity, $contact->othercountry, $contact->otherpostalcode, $contact->otherstate, $contact->otherstreet, $props, $contactprops);
1725
-
1726
-		// set the mailing address and its type
1727
-		if (isset($props[$contactprops["businessaddress"]])) {
1728
-			$props[$contactprops["mailingaddress"]] = 2;
1729
-			$this->setMailingAddress($contact->businesscity, $contact->businesscountry, $contact->businesspostalcode, $contact->businessstate, $contact->businessstreet, $props[$contactprops["businessaddress"]], $props, $contactprops);
1730
-		}
1731
-		elseif (isset($props[$contactprops["homeaddress"]])) {
1732
-			$props[$contactprops["mailingaddress"]] = 1;
1733
-			$this->setMailingAddress($contact->homecity, $contact->homecountry, $contact->homepostalcode, $contact->homestate, $contact->homestreet, $props[$contactprops["homeaddress"]], $props, $contactprops);
1734
-		}
1735
-		elseif (isset($props[$contactprops["otheraddress"]])) {
1736
-			$props[$contactprops["mailingaddress"]] = 3;
1737
-			$this->setMailingAddress($contact->othercity, $contact->othercountry, $contact->otherpostalcode, $contact->otherstate, $contact->otherstreet, $props[$contactprops["otheraddress"]], $props, $contactprops);
1738
-		}
1739
-
1740
-		if (isset($contact->picture)) {
1741
-			$picbinary = base64_decode($contact->picture);
1742
-			$picsize = strlen($picbinary);
1743
-			$props[$contactprops["haspic"]] = false;
1744
-
1745
-			// TODO contact picture handling
1746
-			// check if contact has already got a picture. delete it first in that case
1747
-			// delete it also if it was removed on a mobile
1748
-			$picprops = mapi_getprops($mapimessage, [$contactprops["haspic"]]);
1749
-			if (isset($picprops[$contactprops["haspic"]]) && $picprops[$contactprops["haspic"]]) {
1750
-				SLog::Write(LOGLEVEL_DEBUG, "Contact already has a picture. Delete it");
1751
-
1752
-				$attachtable = mapi_message_getattachmenttable($mapimessage);
1753
-				mapi_table_restrict($attachtable, MAPIUtils::GetContactPicRestriction());
1754
-				$rows = mapi_table_queryallrows($attachtable, [PR_ATTACH_NUM]);
1755
-				if (isset($rows) && is_array($rows)) {
1756
-					foreach ($rows as $row) {
1757
-						mapi_message_deleteattach($mapimessage, $row[PR_ATTACH_NUM]);
1758
-					}
1759
-				}
1760
-			}
1761
-
1762
-			// only set picture if there's data in the request
1763
-			if ($picbinary !== false && $picsize > 0) {
1764
-				$props[$contactprops["haspic"]] = true;
1765
-				$pic = mapi_message_createattach($mapimessage);
1766
-				// Set properties of the attachment
1767
-				$picprops = [
1768
-					PR_ATTACH_LONG_FILENAME => "ContactPicture.jpg",
1769
-					PR_DISPLAY_NAME => "ContactPicture.jpg",
1770
-					0x7FFF000B => true,
1771
-					PR_ATTACHMENT_HIDDEN => false,
1772
-					PR_ATTACHMENT_FLAGS => 1,
1773
-					PR_ATTACH_METHOD => ATTACH_BY_VALUE,
1774
-					PR_ATTACH_EXTENSION => ".jpg",
1775
-					PR_ATTACH_NUM => 1,
1776
-					PR_ATTACH_SIZE => $picsize,
1777
-					PR_ATTACH_DATA_BIN => $picbinary,
1778
-				];
1779
-
1780
-				mapi_setprops($pic, $picprops);
1781
-				mapi_savechanges($pic);
1782
-			}
1783
-		}
1784
-
1785
-		if (isset($contact->asbody)) {
1786
-			$this->setASbody($contact->asbody, $props, $contactprops);
1787
-		}
1788
-
1789
-		// set fileas
1790
-		if (defined('FILEAS_ORDER')) {
1791
-			$lastname = (isset($contact->lastname)) ? $contact->lastname : "";
1792
-			$firstname = (isset($contact->firstname)) ? $contact->firstname : "";
1793
-			$middlename = (isset($contact->middlename)) ? $contact->middlename : "";
1794
-			$company = (isset($contact->companyname)) ? $contact->companyname : "";
1795
-			$props[$contactprops["fileas"]] = Utils::BuildFileAs($lastname, $firstname, $middlename, $company);
1796
-		}
1797
-		else {
1798
-			SLog::Write(LOGLEVEL_DEBUG, "FILEAS_ORDER not defined");
1799
-		}
1800
-
1801
-		mapi_setprops($mapimessage, $props);
1802
-
1803
-		return true;
1804
-	}
1805
-
1806
-	/**
1807
-	 * Writes a SyncTask to MAPI.
1808
-	 *
1809
-	 * @param mixed    $mapimessage
1810
-	 * @param SyncTask $task
1811
-	 *
1812
-	 * @return bool
1813
-	 */
1814
-	private function setTask($mapimessage, $task) {
1815
-		mapi_setprops($mapimessage, [PR_MESSAGE_CLASS => "IPM.Task"]);
1816
-
1817
-		$taskmapping = MAPIMapping::GetTaskMapping();
1818
-		$taskprops = MAPIMapping::GetTaskProperties();
1819
-		$this->setPropsInMAPI($mapimessage, $task, $taskmapping);
1820
-		$taskprops = array_merge($this->getPropIdsFromStrings($taskmapping), $this->getPropIdsFromStrings($taskprops));
1821
-
1822
-		// task specific properties to be set
1823
-		$props = [];
1824
-
1825
-		if (isset($task->asbody)) {
1826
-			$this->setASbody($task->asbody, $props, $taskprops);
1827
-		}
1828
-
1829
-		if (isset($task->complete)) {
1830
-			if ($task->complete) {
1831
-				// Set completion to 100%
1832
-				// Set status to 'complete'
1833
-				$props[$taskprops["completion"]] = 1.0;
1834
-				$props[$taskprops["status"]] = 2;
1835
-				$props[$taskprops["reminderset"]] = false;
1836
-			}
1837
-			else {
1838
-				// Set completion to 0%
1839
-				// Set status to 'not started'
1840
-				$props[$taskprops["completion"]] = 0.0;
1841
-				$props[$taskprops["status"]] = 0;
1842
-			}
1843
-		}
1844
-		if (isset($task->recurrence) && class_exists('TaskRecurrence')) {
1845
-			$deadoccur = false;
1846
-			if ((isset($task->recurrence->occurrences) && $task->recurrence->occurrences == 1) ||
1847
-				(isset($task->recurrence->deadoccur) && $task->recurrence->deadoccur == 1)) { // ios5 sends deadoccur inside the recurrence
1848
-				$deadoccur = true;
1849
-			}
1850
-
1851
-			// Set PR_ICON_INDEX to 1281 to show correct icon in category view
1852
-			$props[$taskprops["icon"]] = 1281;
1853
-			// dead occur - false if new occurrences should be generated from the task
1854
-			// true - if it is the last occurrence of the task
1855
-			$props[$taskprops["deadoccur"]] = $deadoccur;
1856
-			$props[$taskprops["isrecurringtag"]] = true;
1857
-
1858
-			$recurrence = new TaskRecurrence($this->store, $mapimessage);
1859
-			$recur = [];
1860
-			$this->setRecurrence($task, $recur);
1861
-
1862
-			// task specific recurrence properties which we need to set here
1863
-			// "start" and "end" are in GMT when passing to class.recurrence
1864
-			// set recurrence start here because it's calculated differently for tasks and appointments
1865
-			$recur["start"] = $task->recurrence->start;
1866
-			$recur["regen"] = (isset($task->recurrence->regenerate) && $task->recurrence->regenerate) ? 1 : 0;
1867
-			// OL regenerates recurring task itself, but setting deleteOccurrence is required so that PHP-MAPI doesn't regenerate
1868
-			// completed occurrence of a task.
1869
-			if ($recur["regen"] == 0) {
1870
-				$recur["deleteOccurrence"] = 0;
1871
-			}
1872
-			// Also add dates to $recur
1873
-			$recur["duedate"] = $task->duedate;
1874
-			$recur["complete"] = (isset($task->complete) && $task->complete) ? 1 : 0;
1875
-			if (isset($task->datecompleted)) {
1876
-				$recur["datecompleted"] = $task->datecompleted;
1877
-			}
1878
-			$recurrence->setRecurrence($recur);
1879
-		}
1880
-
1881
-		$props[$taskprops["private"]] = (isset($task->sensitivity) && $task->sensitivity >= SENSITIVITY_PRIVATE) ? true : false;
1882
-
1883
-		// Open address book for user resolve to set the owner
1884
-		$addrbook = $this->getAddressbook();
1885
-
1886
-		// check if there is already an owner for the task, set current user if not
1887
-		$p = [$taskprops["owner"]];
1888
-		$owner = $this->getProps($mapimessage, $p);
1889
-		if (!isset($owner[$taskprops["owner"]])) {
1890
-			$userinfo = nsp_getuserinfo(Request::GetUser());
1891
-			if (mapi_last_hresult() == NOERROR && isset($userinfo["fullname"])) {
1892
-				$props[$taskprops["owner"]] = $userinfo["fullname"];
1893
-			}
1894
-		}
1895
-		mapi_setprops($mapimessage, $props);
1896
-
1897
-		return true;
1898
-	}
1899
-
1900
-	/**
1901
-	 * Writes a SyncNote to MAPI.
1902
-	 *
1903
-	 * @param mixed    $mapimessage
1904
-	 * @param SyncNote $note
1905
-	 *
1906
-	 * @return bool
1907
-	 */
1908
-	private function setNote($mapimessage, $note) {
1909
-		// Touchdown does not send categories if all are unset or there is none.
1910
-		// Setting it to an empty array will unset the property in KC as well
1911
-		if (!isset($note->categories)) {
1912
-			$note->categories = [];
1913
-		}
1914
-
1915
-		// update icon index to correspond to the color
1916
-		if (isset($note->Color) && $note->Color > -1 && $note->Color < 5) {
1917
-			$note->Iconindex = 768 + $note->Color;
1918
-		}
1919
-
1920
-		$this->setPropsInMAPI($mapimessage, $note, MAPIMapping::GetNoteMapping());
1921
-
1922
-		$noteprops = MAPIMapping::GetNoteProperties();
1923
-		$noteprops = $this->getPropIdsFromStrings($noteprops);
1924
-
1925
-		// note specific properties to be set
1926
-		$props = [];
1927
-		$props[$noteprops["messageclass"]] = "IPM.StickyNote";
1928
-		// set body otherwise the note will be "broken" when editing it in outlook
1929
-		if (isset($note->asbody)) {
1930
-			$this->setASbody($note->asbody, $props, $noteprops);
1931
-		}
1932
-
1933
-		$props[$noteprops["internetcpid"]] = INTERNET_CPID_UTF8;
1934
-		mapi_setprops($mapimessage, $props);
1935
-
1936
-		return true;
1937
-	}
1938
-
1939
-	/*----------------------------------------------------------------------------------------------------------
1246
+    /**
1247
+     * Writes a SyncObject to MAPI
1248
+     * Depending on the message class, a contact, appointment, task or email is written.
1249
+     *
1250
+     * @param mixed      $mapimessage
1251
+     * @param SyncObject $message
1252
+     *
1253
+     * @return bool
1254
+     */
1255
+    public function SetMessage($mapimessage, $message) {
1256
+        // TODO check with instanceof
1257
+        switch (strtolower(get_class($message))) {
1258
+            case "synccontact":
1259
+                return $this->setContact($mapimessage, $message);
1260
+
1261
+            case "syncappointment":
1262
+                return $this->setAppointment($mapimessage, $message);
1263
+
1264
+            case "synctask":
1265
+                return $this->setTask($mapimessage, $message);
1266
+
1267
+            case "syncnote":
1268
+                return $this->setNote($mapimessage, $message);
1269
+
1270
+            default:
1271
+                // for emails only flag (read and todo) changes are possible
1272
+                return $this->setEmail($mapimessage, $message);
1273
+        }
1274
+    }
1275
+
1276
+    /**
1277
+     * Writes SyncMail to MAPI (actually flags only).
1278
+     *
1279
+     * @param mixed    $mapimessage
1280
+     * @param SyncMail $message
1281
+     */
1282
+    private function setEmail($mapimessage, $message) {
1283
+        // update categories
1284
+        if (!isset($message->categories)) {
1285
+            $message->categories = [];
1286
+        }
1287
+        $emailmap = MAPIMapping::GetEmailMapping();
1288
+        $this->setPropsInMAPI($mapimessage, $message, ["categories" => $emailmap["categories"]]);
1289
+
1290
+        $flagmapping = MAPIMapping::GetMailFlagsMapping();
1291
+        $flagprops = MAPIMapping::GetMailFlagsProperties();
1292
+        $flagprops = array_merge($this->getPropIdsFromStrings($flagmapping), $this->getPropIdsFromStrings($flagprops));
1293
+        // flag specific properties to be set
1294
+        $props = $delprops = [];
1295
+        // unset message flags if:
1296
+        // flag is not set
1297
+        if (empty($message->flag) ||
1298
+            // flag status is not set
1299
+            !isset($message->flag->flagstatus) ||
1300
+            // flag status is 0 or empty
1301
+            (isset($message->flag->flagstatus) && ($message->flag->flagstatus == 0 || $message->flag->flagstatus == ""))) {
1302
+            // if message flag is empty, some properties need to be deleted
1303
+            // and some set to 0 or false
1304
+
1305
+            $props[$flagprops["todoitemsflags"]] = 0;
1306
+            $props[$flagprops["status"]] = 0;
1307
+            $props[$flagprops["completion"]] = 0.0;
1308
+            $props[$flagprops["flagtype"]] = "";
1309
+            $props[$flagprops["ordinaldate"]] = 0x7FFFFFFF; // ordinal date is 12am 1.1.4501, set it to max possible value
1310
+            $props[$flagprops["subordinaldate"]] = "";
1311
+            $props[$flagprops["replyrequested"]] = false;
1312
+            $props[$flagprops["responserequested"]] = false;
1313
+            $props[$flagprops["reminderset"]] = false;
1314
+            $props[$flagprops["complete"]] = false;
1315
+
1316
+            $delprops[] = $flagprops["todotitle"];
1317
+            $delprops[] = $flagprops["duedate"];
1318
+            $delprops[] = $flagprops["startdate"];
1319
+            $delprops[] = $flagprops["datecompleted"];
1320
+            $delprops[] = $flagprops["utcstartdate"];
1321
+            $delprops[] = $flagprops["utcduedate"];
1322
+            $delprops[] = $flagprops["completetime"];
1323
+            $delprops[] = $flagprops["flagstatus"];
1324
+            $delprops[] = $flagprops["flagicon"];
1325
+        }
1326
+        else {
1327
+            $this->setPropsInMAPI($mapimessage, $message->flag, $flagmapping);
1328
+            $props[$flagprops["todoitemsflags"]] = 1;
1329
+            if (isset($message->subject) && strlen($message->subject) > 0) {
1330
+                $props[$flagprops["todotitle"]] = $message->subject;
1331
+            }
1332
+            // ordinal date is utc current time
1333
+            if (!isset($message->flag->ordinaldate) || empty($message->flag->ordinaldate)) {
1334
+                $props[$flagprops["ordinaldate"]] = time();
1335
+            }
1336
+            // the default value
1337
+            if (!isset($message->flag->subordinaldate) || empty($message->flag->subordinaldate)) {
1338
+                $props[$flagprops["subordinaldate"]] = "5555555";
1339
+            }
1340
+            $props[$flagprops["flagicon"]] = 6; // red flag icon
1341
+            $props[$flagprops["replyrequested"]] = true;
1342
+            $props[$flagprops["responserequested"]] = true;
1343
+
1344
+            if ($message->flag->flagstatus == SYNC_FLAGSTATUS_COMPLETE) {
1345
+                $props[$flagprops["status"]] = olTaskComplete;
1346
+                $props[$flagprops["completion"]] = 1.0;
1347
+                $props[$flagprops["complete"]] = true;
1348
+                $props[$flagprops["replyrequested"]] = false;
1349
+                $props[$flagprops["responserequested"]] = false;
1350
+                unset($props[$flagprops["flagicon"]]);
1351
+                $delprops[] = $flagprops["flagicon"];
1352
+            }
1353
+        }
1354
+
1355
+        if (!empty($props)) {
1356
+            mapi_setprops($mapimessage, $props);
1357
+        }
1358
+        if (!empty($delprops)) {
1359
+            mapi_deleteprops($mapimessage, $delprops);
1360
+        }
1361
+    }
1362
+
1363
+    /**
1364
+     * Writes a SyncAppointment to MAPI.
1365
+     *
1366
+     * @param mixed           $mapimessage
1367
+     * @param SyncAppointment $message
1368
+     * @param mixed           $appointment
1369
+     *
1370
+     * @return bool
1371
+     */
1372
+    private function setAppointment($mapimessage, $appointment) {
1373
+        // Get timezone info
1374
+        if (isset($appointment->timezone)) {
1375
+            $tz = $this->getTZFromSyncBlob(base64_decode($appointment->timezone));
1376
+        }
1377
+        else {
1378
+            $tz = false;
1379
+        }
1380
+
1381
+        // start and end time may not be set - try to get them from the existing appointment for further calculation - see https://jira.z-hub.io/browse/ZP-983
1382
+        if (!isset($appointment->starttime) || !isset($appointment->endtime)) {
1383
+            $amapping = MAPIMapping::GetAppointmentMapping();
1384
+            $amapping = $this->getPropIdsFromStrings($amapping);
1385
+            $existingstartendpropsmap = [$amapping["starttime"], $amapping["endtime"]];
1386
+            $existingstartendprops = $this->getProps($mapimessage, $existingstartendpropsmap);
1387
+
1388
+            if (isset($existingstartendprops[$amapping["starttime"]]) && !isset($appointment->starttime)) {
1389
+                $appointment->starttime = $existingstartendprops[$amapping["starttime"]];
1390
+                SLog::Write(LOGLEVEL_WBXML, sprintf("MAPIProvider->setAppointment(): Parameter 'starttime' was not set, using value from MAPI %d (%s).", $appointment->starttime, gmstrftime("%Y%m%dT%H%M%SZ", $appointment->starttime)));
1391
+            }
1392
+            if (isset($existingstartendprops[$amapping["endtime"]]) && !isset($appointment->endtime)) {
1393
+                $appointment->endtime = $existingstartendprops[$amapping["endtime"]];
1394
+                SLog::Write(LOGLEVEL_WBXML, sprintf("MAPIProvider->setAppointment(): Parameter 'endtime' was not set, using value from MAPI %d (%s).", $appointment->endtime, gmstrftime("%Y%m%dT%H%M%SZ", $appointment->endtime)));
1395
+            }
1396
+        }
1397
+        if (!isset($appointment->starttime) || !isset($appointment->endtime)) {
1398
+            throw new StatusException("MAPIProvider->setAppointment(): Error, start and/or end time not set and can not be retrieved from MAPI.", SYNC_STATUS_SYNCCANNOTBECOMPLETED);
1399
+        }
1400
+
1401
+        // calculate duration because without it some webaccess views are broken. duration is in min
1402
+        $localstart = $this->getLocaltimeByTZ($appointment->starttime, $tz);
1403
+        $localend = $this->getLocaltimeByTZ($appointment->endtime, $tz);
1404
+        $duration = ($localend - $localstart) / 60;
1405
+
1406
+        // nokia sends an yearly event with 0 mins duration but as all day event,
1407
+        // so make it end next day
1408
+        if ($appointment->starttime == $appointment->endtime && isset($appointment->alldayevent) && $appointment->alldayevent) {
1409
+            $duration = 1440;
1410
+            $appointment->endtime = $appointment->starttime + 24 * 60 * 60;
1411
+            $localend = $localstart + 24 * 60 * 60;
1412
+        }
1413
+
1414
+        // is the transmitted UID OL compatible?
1415
+        // if not, encapsulate the transmitted uid
1416
+        $appointment->uid = Utils::GetOLUidFromICalUid($appointment->uid);
1417
+
1418
+        mapi_setprops($mapimessage, [PR_MESSAGE_CLASS => "IPM.Appointment"]);
1419
+
1420
+        $appointmentmapping = MAPIMapping::GetAppointmentMapping();
1421
+        $this->setPropsInMAPI($mapimessage, $appointment, $appointmentmapping);
1422
+        $appointmentprops = MAPIMapping::GetAppointmentProperties();
1423
+        $appointmentprops = array_merge($this->getPropIdsFromStrings($appointmentmapping), $this->getPropIdsFromStrings($appointmentprops));
1424
+        // appointment specific properties to be set
1425
+        $props = [];
1426
+
1427
+        // sensitivity is not enough to mark an appointment as private, so we use another mapi tag
1428
+        $private = (isset($appointment->sensitivity) && $appointment->sensitivity >= SENSITIVITY_PRIVATE) ? true : false;
1429
+
1430
+        // Set commonstart/commonend to start/end and remindertime to start, duration, private and cleanGlobalObjectId
1431
+        $props[$appointmentprops["commonstart"]] = $appointment->starttime;
1432
+        $props[$appointmentprops["commonend"]] = $appointment->endtime;
1433
+        $props[$appointmentprops["reminderstart"]] = $appointment->starttime;
1434
+        // Set reminder boolean to 'true' if reminder is set
1435
+        $props[$appointmentprops["reminderset"]] = isset($appointment->reminder) ? true : false;
1436
+        $props[$appointmentprops["duration"]] = $duration;
1437
+        $props[$appointmentprops["private"]] = $private;
1438
+        $props[$appointmentprops["uid"]] = $appointment->uid;
1439
+        // Set named prop 8510, unknown property, but enables deleting a single occurrence of a recurring
1440
+        // type in OLK2003.
1441
+        $props[$appointmentprops["sideeffects"]] = 369;
1442
+
1443
+        if (isset($appointment->reminder) && $appointment->reminder >= 0) {
1444
+            // Set 'flagdueby' to correct value (start - reminderminutes)
1445
+            $props[$appointmentprops["flagdueby"]] = $appointment->starttime - $appointment->reminder * 60;
1446
+            $props[$appointmentprops["remindertime"]] = $appointment->reminder;
1447
+        }
1448
+        // unset the reminder
1449
+        else {
1450
+            $props[$appointmentprops["reminderset"]] = false;
1451
+        }
1452
+
1453
+        if (isset($appointment->asbody)) {
1454
+            $this->setASbody($appointment->asbody, $props, $appointmentprops);
1455
+        }
1456
+
1457
+        if ($tz !== false) {
1458
+            $props[$appointmentprops["timezonetag"]] = $this->getMAPIBlobFromTZ($tz);
1459
+        }
1460
+
1461
+        if (isset($appointment->recurrence)) {
1462
+            // Set PR_ICON_INDEX to 1025 to show correct icon in category view
1463
+            $props[$appointmentprops["icon"]] = 1025;
1464
+
1465
+            // if there aren't any exceptions, use the 'old style' set recurrence
1466
+            $noexceptions = true;
1467
+
1468
+            $recurrence = new Recurrence($this->store, $mapimessage);
1469
+            $recur = [];
1470
+            $this->setRecurrence($appointment, $recur);
1471
+
1472
+            // set the recurrence type to that of the MAPI
1473
+            $props[$appointmentprops["recurrencetype"]] = $recur["recurrencetype"];
1474
+
1475
+            $starttime = $this->gmtime($localstart);
1476
+            $endtime = $this->gmtime($localend);
1477
+
1478
+            // set recurrence start here because it's calculated differently for tasks and appointments
1479
+            $recur["start"] = $this->getDayStartOfTimestamp($this->getGMTTimeByTZ($localstart, $tz));
1480
+
1481
+            $recur["startocc"] = $starttime["tm_hour"] * 60 + $starttime["tm_min"];
1482
+            $recur["endocc"] = $recur["startocc"] + $duration; // Note that this may be > 24*60 if multi-day
1483
+
1484
+            // only tasks can regenerate
1485
+            $recur["regen"] = false;
1486
+
1487
+            // Process exceptions. The PDA will send all exceptions for this recurring item.
1488
+            if (isset($appointment->exceptions)) {
1489
+                foreach ($appointment->exceptions as $exception) {
1490
+                    // we always need the base date
1491
+                    if (!isset($exception->exceptionstarttime)) {
1492
+                        continue;
1493
+                    }
1494
+
1495
+                    $basedate = $this->getDayStartOfTimestamp($exception->exceptionstarttime);
1496
+                    if (isset($exception->deleted) && $exception->deleted) {
1497
+                        $noexceptions = false;
1498
+                        // Delete exception
1499
+                        $recurrence->createException([], $basedate, true);
1500
+                    }
1501
+                    else {
1502
+                        // Change exception
1503
+                        $mapiexception = ["basedate" => $basedate];
1504
+                        // other exception properties which are not handled in recurrence
1505
+                        $exceptionprops = [];
1506
+
1507
+                        if (isset($exception->starttime)) {
1508
+                            $mapiexception["start"] = $this->getLocaltimeByTZ($exception->starttime, $tz);
1509
+                            $exceptionprops[$appointmentprops["starttime"]] = $exception->starttime;
1510
+                        }
1511
+                        if (isset($exception->endtime)) {
1512
+                            $mapiexception["end"] = $this->getLocaltimeByTZ($exception->endtime, $tz);
1513
+                            $exceptionprops[$appointmentprops["endtime"]] = $exception->endtime;
1514
+                        }
1515
+                        if (isset($exception->subject)) {
1516
+                            $exceptionprops[$appointmentprops["subject"]] = $mapiexception["subject"] = u2w($exception->subject);
1517
+                        }
1518
+                        if (isset($exception->location)) {
1519
+                            $exceptionprops[$appointmentprops["location"]] = $mapiexception["location"] = u2w($exception->location);
1520
+                        }
1521
+                        if (isset($exception->busystatus)) {
1522
+                            $exceptionprops[$appointmentprops["busystatus"]] = $mapiexception["busystatus"] = $exception->busystatus;
1523
+                        }
1524
+                        if (isset($exception->reminder)) {
1525
+                            $exceptionprops[$appointmentprops["reminderset"]] = $mapiexception["reminder_set"] = 1;
1526
+                            $exceptionprops[$appointmentprops["remindertime"]] = $mapiexception["remind_before"] = $exception->reminder;
1527
+                        }
1528
+                        if (isset($exception->alldayevent)) {
1529
+                            $exceptionprops[$appointmentprops["alldayevent"]] = $mapiexception["alldayevent"] = $exception->alldayevent;
1530
+                        }
1531
+
1532
+                        if (!isset($recur["changed_occurrences"])) {
1533
+                            $recur["changed_occurrences"] = [];
1534
+                        }
1535
+
1536
+                        if (isset($exception->body)) {
1537
+                            $exceptionprops[$appointmentprops["body"]] = u2w($exception->body);
1538
+                        }
1539
+
1540
+                        if (isset($exception->asbody)) {
1541
+                            $this->setASbody($exception->asbody, $exceptionprops, $appointmentprops);
1542
+                            $mapiexception["body"] = $exceptionprops[$appointmentprops["body"]] =
1543
+                                (isset($exceptionprops[$appointmentprops["body"]])) ? $exceptionprops[$appointmentprops["body"]] :
1544
+                                ((isset($exceptionprops[$appointmentprops["html"]])) ? $exceptionprops[$appointmentprops["html"]] : "");
1545
+                        }
1546
+
1547
+                        array_push($recur["changed_occurrences"], $mapiexception);
1548
+
1549
+                        if (!empty($exceptionprops)) {
1550
+                            $noexceptions = false;
1551
+                            if ($recurrence->isException($basedate)) {
1552
+                                $recurrence->modifyException($exceptionprops, $basedate);
1553
+                            }
1554
+                            else {
1555
+                                $recurrence->createException($exceptionprops, $basedate);
1556
+                            }
1557
+                        }
1558
+                    }
1559
+                }
1560
+            }
1561
+
1562
+            // setRecurrence deletes the attachments from an appointment
1563
+            if ($noexceptions) {
1564
+                $recurrence->setRecurrence($tz, $recur);
1565
+            }
1566
+        }
1567
+        else {
1568
+            $props[$appointmentprops["isrecurring"]] = false;
1569
+        }
1570
+
1571
+        // always set the PR_SENT_REPRESENTING_* props so that the attendee status update also works with the webaccess
1572
+        $p = [$appointmentprops["representingentryid"], $appointmentprops["representingname"], $appointmentprops["sentrepresentingaddt"],
1573
+            $appointmentprops["sentrepresentingemail"], $appointmentprops["sentrepresentinsrchk"], $appointmentprops["responsestatus"], ];
1574
+        $representingprops = $this->getProps($mapimessage, $p);
1575
+
1576
+        if (!isset($representingprops[$appointmentprops["representingentryid"]])) {
1577
+            // TODO use GetStoreProps
1578
+            $storeProps = mapi_getprops($this->store, [PR_MAILBOX_OWNER_ENTRYID]);
1579
+            $props[$appointmentprops["representingentryid"]] = $storeProps[PR_MAILBOX_OWNER_ENTRYID];
1580
+            $displayname = $this->getFullnameFromEntryID($storeProps[PR_MAILBOX_OWNER_ENTRYID]);
1581
+
1582
+            $props[$appointmentprops["representingname"]] = ($displayname !== false) ? $displayname : Request::GetUser();
1583
+            $props[$appointmentprops["sentrepresentingemail"]] = Request::GetUser();
1584
+            $props[$appointmentprops["sentrepresentingaddt"]] = "ZARAFA";
1585
+            $props[$appointmentprops["sentrepresentinsrchk"]] = $props[$appointmentprops["sentrepresentingaddt"]] . ":" . $props[$appointmentprops["sentrepresentingemail"]];
1586
+
1587
+            if (isset($appointment->attendees) && is_array($appointment->attendees) && !empty($appointment->attendees)) {
1588
+                $props[$appointmentprops["icon"]] = 1026;
1589
+                // the user is the organizer
1590
+                // set these properties to show tracking tab in webapp
1591
+
1592
+                $props[$appointmentprops["mrwassent"]] = true;
1593
+                $props[$appointmentprops["responsestatus"]] = olResponseOrganized;
1594
+                $props[$appointmentprops["meetingstatus"]] = olMeeting;
1595
+            }
1596
+        }
1597
+        // we also have to set the responsestatus and not only meetingstatus, so we use another mapi tag
1598
+        if (!isset($props[$appointmentprops["responsestatus"]])) {
1599
+            if (isset($appointment->responsetype)) {
1600
+                $props[$appointmentprops["responsestatus"]] = $appointment->responsetype;
1601
+            }
1602
+            // only set responsestatus to none if it is not set on the server
1603
+            elseif (!isset($representingprops[$appointmentprops["responsestatus"]])) {
1604
+                $props[$appointmentprops["responsestatus"]] = olResponseNone;
1605
+            }
1606
+        }
1607
+
1608
+        // Do attendees
1609
+        if (isset($appointment->attendees) && is_array($appointment->attendees)) {
1610
+            $recips = [];
1611
+
1612
+            // Outlook XP requires organizer in the attendee list as well
1613
+            $org = [];
1614
+            $org[PR_ENTRYID] = isset($representingprops[$appointmentprops["representingentryid"]]) ? $representingprops[$appointmentprops["representingentryid"]] : $props[$appointmentprops["representingentryid"]];
1615
+            $org[PR_DISPLAY_NAME] = isset($representingprops[$appointmentprops["representingname"]]) ? $representingprops[$appointmentprops["representingname"]] : $props[$appointmentprops["representingname"]];
1616
+            $org[PR_ADDRTYPE] = isset($representingprops[$appointmentprops["sentrepresentingaddt"]]) ? $representingprops[$appointmentprops["sentrepresentingaddt"]] : $props[$appointmentprops["sentrepresentingaddt"]];
1617
+            $org[PR_SMTP_ADDRESS] = $org[PR_EMAIL_ADDRESS] = isset($representingprops[$appointmentprops["sentrepresentingemail"]]) ? $representingprops[$appointmentprops["sentrepresentingemail"]] : $props[$appointmentprops["sentrepresentingemail"]];
1618
+            $org[PR_SEARCH_KEY] = isset($representingprops[$appointmentprops["sentrepresentinsrchk"]]) ? $representingprops[$appointmentprops["sentrepresentinsrchk"]] : $props[$appointmentprops["sentrepresentinsrchk"]];
1619
+            $org[PR_RECIPIENT_FLAGS] = recipOrganizer | recipSendable;
1620
+            $org[PR_RECIPIENT_TYPE] = MAPI_ORIG;
1621
+
1622
+            array_push($recips, $org);
1623
+
1624
+            // Open address book for user resolve
1625
+            $addrbook = $this->getAddressbook();
1626
+            foreach ($appointment->attendees as $attendee) {
1627
+                $recip = [];
1628
+                $recip[PR_EMAIL_ADDRESS] = u2w($attendee->email);
1629
+                $recip[PR_SMTP_ADDRESS] = u2w($attendee->email);
1630
+
1631
+                // lookup information in GAB if possible so we have up-to-date name for given address
1632
+                $userinfo = [[PR_DISPLAY_NAME => $recip[PR_EMAIL_ADDRESS]]];
1633
+                $userinfo = mapi_ab_resolvename($addrbook, $userinfo, EMS_AB_ADDRESS_LOOKUP);
1634
+                if (mapi_last_hresult() == NOERROR) {
1635
+                    $recip[PR_DISPLAY_NAME] = $userinfo[0][PR_DISPLAY_NAME];
1636
+                    $recip[PR_EMAIL_ADDRESS] = $userinfo[0][PR_EMAIL_ADDRESS];
1637
+                    $recip[PR_SEARCH_KEY] = $userinfo[0][PR_SEARCH_KEY];
1638
+                    $recip[PR_ADDRTYPE] = $userinfo[0][PR_ADDRTYPE];
1639
+                    $recip[PR_ENTRYID] = $userinfo[0][PR_ENTRYID];
1640
+                    $recip[PR_RECIPIENT_TYPE] = isset($attendee->attendeetype) ? $attendee->attendeetype : MAPI_TO;
1641
+                    $recip[PR_RECIPIENT_FLAGS] = recipSendable;
1642
+                    $recip[PR_RECIPIENT_TRACKSTATUS] = isset($attendee->attendeestatus) ? $attendee->attendeestatus : olResponseNone;
1643
+                }
1644
+                else {
1645
+                    $recip[PR_DISPLAY_NAME] = u2w($attendee->name);
1646
+                    $recip[PR_SEARCH_KEY] = "SMTP:" . $recip[PR_EMAIL_ADDRESS] . "\0";
1647
+                    $recip[PR_ADDRTYPE] = "SMTP";
1648
+                    $recip[PR_RECIPIENT_TYPE] = isset($attendee->attendeetype) ? $attendee->attendeetype : MAPI_TO;
1649
+                    $recip[PR_ENTRYID] = mapi_createoneoff($recip[PR_DISPLAY_NAME], $recip[PR_ADDRTYPE], $recip[PR_EMAIL_ADDRESS]);
1650
+                }
1651
+
1652
+                array_push($recips, $recip);
1653
+            }
1654
+
1655
+            mapi_message_modifyrecipients($mapimessage, 0, $recips);
1656
+        }
1657
+        mapi_setprops($mapimessage, $props);
1658
+
1659
+        return true;
1660
+    }
1661
+
1662
+    /**
1663
+     * Writes a SyncContact to MAPI.
1664
+     *
1665
+     * @param mixed       $mapimessage
1666
+     * @param SyncContact $contact
1667
+     *
1668
+     * @return bool
1669
+     */
1670
+    private function setContact($mapimessage, $contact) {
1671
+        mapi_setprops($mapimessage, [PR_MESSAGE_CLASS => "IPM.Contact"]);
1672
+
1673
+        // normalize email addresses
1674
+        if (isset($contact->email1address) && (($contact->email1address = $this->extractEmailAddress($contact->email1address)) === false)) {
1675
+            unset($contact->email1address);
1676
+        }
1677
+
1678
+        if (isset($contact->email2address) && (($contact->email2address = $this->extractEmailAddress($contact->email2address)) === false)) {
1679
+            unset($contact->email2address);
1680
+        }
1681
+
1682
+        if (isset($contact->email3address) && (($contact->email3address = $this->extractEmailAddress($contact->email3address)) === false)) {
1683
+            unset($contact->email3address);
1684
+        }
1685
+
1686
+        $contactmapping = MAPIMapping::GetContactMapping();
1687
+        $contactprops = MAPIMapping::GetContactProperties();
1688
+        $this->setPropsInMAPI($mapimessage, $contact, $contactmapping);
1689
+
1690
+        // /set display name from contact's properties
1691
+        $cname = $this->composeDisplayName($contact);
1692
+
1693
+        // get contact specific mapi properties and merge them with the AS properties
1694
+        $contactprops = array_merge($this->getPropIdsFromStrings($contactmapping), $this->getPropIdsFromStrings($contactprops));
1695
+
1696
+        // contact specific properties to be set
1697
+        $props = [];
1698
+
1699
+        // need to be set in order to show contacts properly in outlook and wa
1700
+        $nremails = [];
1701
+        $abprovidertype = 0;
1702
+
1703
+        if (isset($contact->email1address)) {
1704
+            $this->setEmailAddress($contact->email1address, $cname, 1, $props, $contactprops, $nremails, $abprovidertype);
1705
+        }
1706
+        if (isset($contact->email2address)) {
1707
+            $this->setEmailAddress($contact->email2address, $cname, 2, $props, $contactprops, $nremails, $abprovidertype);
1708
+        }
1709
+        if (isset($contact->email3address)) {
1710
+            $this->setEmailAddress($contact->email3address, $cname, 3, $props, $contactprops, $nremails, $abprovidertype);
1711
+        }
1712
+
1713
+        $props[$contactprops["addressbooklong"]] = $abprovidertype;
1714
+        $props[$contactprops["displayname"]] = $props[$contactprops["subject"]] = $cname;
1715
+
1716
+        // pda multiple e-mail addresses bug fix for the contact
1717
+        if (!empty($nremails)) {
1718
+            $props[$contactprops["addressbookmv"]] = $nremails;
1719
+        }
1720
+
1721
+        // set addresses
1722
+        $this->setAddress("home", $contact->homecity, $contact->homecountry, $contact->homepostalcode, $contact->homestate, $contact->homestreet, $props, $contactprops);
1723
+        $this->setAddress("business", $contact->businesscity, $contact->businesscountry, $contact->businesspostalcode, $contact->businessstate, $contact->businessstreet, $props, $contactprops);
1724
+        $this->setAddress("other", $contact->othercity, $contact->othercountry, $contact->otherpostalcode, $contact->otherstate, $contact->otherstreet, $props, $contactprops);
1725
+
1726
+        // set the mailing address and its type
1727
+        if (isset($props[$contactprops["businessaddress"]])) {
1728
+            $props[$contactprops["mailingaddress"]] = 2;
1729
+            $this->setMailingAddress($contact->businesscity, $contact->businesscountry, $contact->businesspostalcode, $contact->businessstate, $contact->businessstreet, $props[$contactprops["businessaddress"]], $props, $contactprops);
1730
+        }
1731
+        elseif (isset($props[$contactprops["homeaddress"]])) {
1732
+            $props[$contactprops["mailingaddress"]] = 1;
1733
+            $this->setMailingAddress($contact->homecity, $contact->homecountry, $contact->homepostalcode, $contact->homestate, $contact->homestreet, $props[$contactprops["homeaddress"]], $props, $contactprops);
1734
+        }
1735
+        elseif (isset($props[$contactprops["otheraddress"]])) {
1736
+            $props[$contactprops["mailingaddress"]] = 3;
1737
+            $this->setMailingAddress($contact->othercity, $contact->othercountry, $contact->otherpostalcode, $contact->otherstate, $contact->otherstreet, $props[$contactprops["otheraddress"]], $props, $contactprops);
1738
+        }
1739
+
1740
+        if (isset($contact->picture)) {
1741
+            $picbinary = base64_decode($contact->picture);
1742
+            $picsize = strlen($picbinary);
1743
+            $props[$contactprops["haspic"]] = false;
1744
+
1745
+            // TODO contact picture handling
1746
+            // check if contact has already got a picture. delete it first in that case
1747
+            // delete it also if it was removed on a mobile
1748
+            $picprops = mapi_getprops($mapimessage, [$contactprops["haspic"]]);
1749
+            if (isset($picprops[$contactprops["haspic"]]) && $picprops[$contactprops["haspic"]]) {
1750
+                SLog::Write(LOGLEVEL_DEBUG, "Contact already has a picture. Delete it");
1751
+
1752
+                $attachtable = mapi_message_getattachmenttable($mapimessage);
1753
+                mapi_table_restrict($attachtable, MAPIUtils::GetContactPicRestriction());
1754
+                $rows = mapi_table_queryallrows($attachtable, [PR_ATTACH_NUM]);
1755
+                if (isset($rows) && is_array($rows)) {
1756
+                    foreach ($rows as $row) {
1757
+                        mapi_message_deleteattach($mapimessage, $row[PR_ATTACH_NUM]);
1758
+                    }
1759
+                }
1760
+            }
1761
+
1762
+            // only set picture if there's data in the request
1763
+            if ($picbinary !== false && $picsize > 0) {
1764
+                $props[$contactprops["haspic"]] = true;
1765
+                $pic = mapi_message_createattach($mapimessage);
1766
+                // Set properties of the attachment
1767
+                $picprops = [
1768
+                    PR_ATTACH_LONG_FILENAME => "ContactPicture.jpg",
1769
+                    PR_DISPLAY_NAME => "ContactPicture.jpg",
1770
+                    0x7FFF000B => true,
1771
+                    PR_ATTACHMENT_HIDDEN => false,
1772
+                    PR_ATTACHMENT_FLAGS => 1,
1773
+                    PR_ATTACH_METHOD => ATTACH_BY_VALUE,
1774
+                    PR_ATTACH_EXTENSION => ".jpg",
1775
+                    PR_ATTACH_NUM => 1,
1776
+                    PR_ATTACH_SIZE => $picsize,
1777
+                    PR_ATTACH_DATA_BIN => $picbinary,
1778
+                ];
1779
+
1780
+                mapi_setprops($pic, $picprops);
1781
+                mapi_savechanges($pic);
1782
+            }
1783
+        }
1784
+
1785
+        if (isset($contact->asbody)) {
1786
+            $this->setASbody($contact->asbody, $props, $contactprops);
1787
+        }
1788
+
1789
+        // set fileas
1790
+        if (defined('FILEAS_ORDER')) {
1791
+            $lastname = (isset($contact->lastname)) ? $contact->lastname : "";
1792
+            $firstname = (isset($contact->firstname)) ? $contact->firstname : "";
1793
+            $middlename = (isset($contact->middlename)) ? $contact->middlename : "";
1794
+            $company = (isset($contact->companyname)) ? $contact->companyname : "";
1795
+            $props[$contactprops["fileas"]] = Utils::BuildFileAs($lastname, $firstname, $middlename, $company);
1796
+        }
1797
+        else {
1798
+            SLog::Write(LOGLEVEL_DEBUG, "FILEAS_ORDER not defined");
1799
+        }
1800
+
1801
+        mapi_setprops($mapimessage, $props);
1802
+
1803
+        return true;
1804
+    }
1805
+
1806
+    /**
1807
+     * Writes a SyncTask to MAPI.
1808
+     *
1809
+     * @param mixed    $mapimessage
1810
+     * @param SyncTask $task
1811
+     *
1812
+     * @return bool
1813
+     */
1814
+    private function setTask($mapimessage, $task) {
1815
+        mapi_setprops($mapimessage, [PR_MESSAGE_CLASS => "IPM.Task"]);
1816
+
1817
+        $taskmapping = MAPIMapping::GetTaskMapping();
1818
+        $taskprops = MAPIMapping::GetTaskProperties();
1819
+        $this->setPropsInMAPI($mapimessage, $task, $taskmapping);
1820
+        $taskprops = array_merge($this->getPropIdsFromStrings($taskmapping), $this->getPropIdsFromStrings($taskprops));
1821
+
1822
+        // task specific properties to be set
1823
+        $props = [];
1824
+
1825
+        if (isset($task->asbody)) {
1826
+            $this->setASbody($task->asbody, $props, $taskprops);
1827
+        }
1828
+
1829
+        if (isset($task->complete)) {
1830
+            if ($task->complete) {
1831
+                // Set completion to 100%
1832
+                // Set status to 'complete'
1833
+                $props[$taskprops["completion"]] = 1.0;
1834
+                $props[$taskprops["status"]] = 2;
1835
+                $props[$taskprops["reminderset"]] = false;
1836
+            }
1837
+            else {
1838
+                // Set completion to 0%
1839
+                // Set status to 'not started'
1840
+                $props[$taskprops["completion"]] = 0.0;
1841
+                $props[$taskprops["status"]] = 0;
1842
+            }
1843
+        }
1844
+        if (isset($task->recurrence) && class_exists('TaskRecurrence')) {
1845
+            $deadoccur = false;
1846
+            if ((isset($task->recurrence->occurrences) && $task->recurrence->occurrences == 1) ||
1847
+                (isset($task->recurrence->deadoccur) && $task->recurrence->deadoccur == 1)) { // ios5 sends deadoccur inside the recurrence
1848
+                $deadoccur = true;
1849
+            }
1850
+
1851
+            // Set PR_ICON_INDEX to 1281 to show correct icon in category view
1852
+            $props[$taskprops["icon"]] = 1281;
1853
+            // dead occur - false if new occurrences should be generated from the task
1854
+            // true - if it is the last occurrence of the task
1855
+            $props[$taskprops["deadoccur"]] = $deadoccur;
1856
+            $props[$taskprops["isrecurringtag"]] = true;
1857
+
1858
+            $recurrence = new TaskRecurrence($this->store, $mapimessage);
1859
+            $recur = [];
1860
+            $this->setRecurrence($task, $recur);
1861
+
1862
+            // task specific recurrence properties which we need to set here
1863
+            // "start" and "end" are in GMT when passing to class.recurrence
1864
+            // set recurrence start here because it's calculated differently for tasks and appointments
1865
+            $recur["start"] = $task->recurrence->start;
1866
+            $recur["regen"] = (isset($task->recurrence->regenerate) && $task->recurrence->regenerate) ? 1 : 0;
1867
+            // OL regenerates recurring task itself, but setting deleteOccurrence is required so that PHP-MAPI doesn't regenerate
1868
+            // completed occurrence of a task.
1869
+            if ($recur["regen"] == 0) {
1870
+                $recur["deleteOccurrence"] = 0;
1871
+            }
1872
+            // Also add dates to $recur
1873
+            $recur["duedate"] = $task->duedate;
1874
+            $recur["complete"] = (isset($task->complete) && $task->complete) ? 1 : 0;
1875
+            if (isset($task->datecompleted)) {
1876
+                $recur["datecompleted"] = $task->datecompleted;
1877
+            }
1878
+            $recurrence->setRecurrence($recur);
1879
+        }
1880
+
1881
+        $props[$taskprops["private"]] = (isset($task->sensitivity) && $task->sensitivity >= SENSITIVITY_PRIVATE) ? true : false;
1882
+
1883
+        // Open address book for user resolve to set the owner
1884
+        $addrbook = $this->getAddressbook();
1885
+
1886
+        // check if there is already an owner for the task, set current user if not
1887
+        $p = [$taskprops["owner"]];
1888
+        $owner = $this->getProps($mapimessage, $p);
1889
+        if (!isset($owner[$taskprops["owner"]])) {
1890
+            $userinfo = nsp_getuserinfo(Request::GetUser());
1891
+            if (mapi_last_hresult() == NOERROR && isset($userinfo["fullname"])) {
1892
+                $props[$taskprops["owner"]] = $userinfo["fullname"];
1893
+            }
1894
+        }
1895
+        mapi_setprops($mapimessage, $props);
1896
+
1897
+        return true;
1898
+    }
1899
+
1900
+    /**
1901
+     * Writes a SyncNote to MAPI.
1902
+     *
1903
+     * @param mixed    $mapimessage
1904
+     * @param SyncNote $note
1905
+     *
1906
+     * @return bool
1907
+     */
1908
+    private function setNote($mapimessage, $note) {
1909
+        // Touchdown does not send categories if all are unset or there is none.
1910
+        // Setting it to an empty array will unset the property in KC as well
1911
+        if (!isset($note->categories)) {
1912
+            $note->categories = [];
1913
+        }
1914
+
1915
+        // update icon index to correspond to the color
1916
+        if (isset($note->Color) && $note->Color > -1 && $note->Color < 5) {
1917
+            $note->Iconindex = 768 + $note->Color;
1918
+        }
1919
+
1920
+        $this->setPropsInMAPI($mapimessage, $note, MAPIMapping::GetNoteMapping());
1921
+
1922
+        $noteprops = MAPIMapping::GetNoteProperties();
1923
+        $noteprops = $this->getPropIdsFromStrings($noteprops);
1924
+
1925
+        // note specific properties to be set
1926
+        $props = [];
1927
+        $props[$noteprops["messageclass"]] = "IPM.StickyNote";
1928
+        // set body otherwise the note will be "broken" when editing it in outlook
1929
+        if (isset($note->asbody)) {
1930
+            $this->setASbody($note->asbody, $props, $noteprops);
1931
+        }
1932
+
1933
+        $props[$noteprops["internetcpid"]] = INTERNET_CPID_UTF8;
1934
+        mapi_setprops($mapimessage, $props);
1935
+
1936
+        return true;
1937
+    }
1938
+
1939
+    /*----------------------------------------------------------------------------------------------------------
1940 1940
 	 * HELPER
1941 1941
 	 */
1942 1942
 
1943
-	/**
1944
-	 * Returns the timestamp offset.
1945
-	 *
1946
-	 * @param string $ts
1947
-	 *
1948
-	 * @return long
1949
-	 */
1950
-	private function GetTZOffset($ts) {
1951
-		$Offset = date("O", $ts);
1952
-
1953
-		$Parity = $Offset < 0 ? -1 : 1;
1954
-		$Offset = $Parity * $Offset;
1955
-		$Offset = ($Offset - ($Offset % 100)) / 100 * 60 + $Offset % 100;
1956
-
1957
-		return $Parity * $Offset;
1958
-	}
1959
-
1960
-	/**
1961
-	 * Localtime of the timestamp.
1962
-	 *
1963
-	 * @param long $time
1964
-	 *
1965
-	 * @return array
1966
-	 */
1967
-	private function gmtime($time) {
1968
-		$TZOffset = $this->GetTZOffset($time);
1969
-
1970
-		$t_time = $time - $TZOffset * 60; # Counter adjust for localtime()
1971
-
1972
-		return localtime($t_time, 1);
1973
-	}
1974
-
1975
-	/**
1976
-	 * Sets the properties in a MAPI object according to an Sync object and a property mapping.
1977
-	 *
1978
-	 * @param mixed      $mapimessage
1979
-	 * @param SyncObject $message
1980
-	 * @param array      $mapping
1981
-	 *
1982
-	 * @return
1983
-	 */
1984
-	private function setPropsInMAPI($mapimessage, $message, $mapping) {
1985
-		$mapiprops = $this->getPropIdsFromStrings($mapping);
1986
-		$unsetVars = $message->getUnsetVars();
1987
-		$propsToDelete = [];
1988
-		$propsToSet = [];
1989
-
1990
-		foreach ($mapiprops as $asprop => $mapiprop) {
1991
-			if (isset($message->{$asprop})) {
1992
-				// UTF8->windows1252.. this is ok for all numerical values
1993
-				if (mapi_prop_type($mapiprop) != PT_BINARY && mapi_prop_type($mapiprop) != PT_MV_BINARY) {
1994
-					if (is_array($message->{$asprop})) {
1995
-						$value = array_map("u2wi", $message->{$asprop});
1996
-					}
1997
-					else {
1998
-						$value = u2wi($message->{$asprop});
1999
-					}
2000
-				}
2001
-				else {
2002
-					$value = $message->{$asprop};
2003
-				}
2004
-
2005
-				// Make sure the php values are the correct type
2006
-				switch (mapi_prop_type($mapiprop)) {
2007
-					case PT_BINARY:
2008
-					case PT_STRING8:
2009
-						settype($value, "string");
2010
-						break;
2011
-
2012
-					case PT_BOOLEAN:
2013
-						settype($value, "boolean");
2014
-						break;
2015
-
2016
-					case PT_SYSTIME:
2017
-					case PT_LONG:
2018
-						settype($value, "integer");
2019
-						break;
2020
-				}
2021
-
2022
-				// decode base64 value
2023
-				if ($mapiprop == PR_RTF_COMPRESSED) {
2024
-					$value = base64_decode($value);
2025
-					if (strlen($value) == 0) {
2026
-						continue;
2027
-					} // PDA will sometimes give us an empty RTF, which we'll ignore.
2028
-
2029
-					// Note that you can still remove notes because when you remove notes it gives
2030
-					// a valid compressed RTF with nothing in it.
2031
-				}
2032
-				// if an "empty array" is to be saved, it the mvprop should be deleted - fixes Mantis #468
2033
-				if (is_array($value) && empty($value)) {
2034
-					$propsToDelete[] = $mapiprop;
2035
-					SLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->setPropsInMAPI(): Property '%s' to be deleted as it is an empty array", $asprop));
2036
-				}
2037
-				else {
2038
-					// all properties will be set at once
2039
-					$propsToSet[$mapiprop] = $value;
2040
-				}
2041
-			}
2042
-			elseif (in_array($asprop, $unsetVars)) {
2043
-				$propsToDelete[] = $mapiprop;
2044
-			}
2045
-		}
2046
-
2047
-		mapi_setprops($mapimessage, $propsToSet);
2048
-		if (mapi_last_hresult()) {
2049
-			SLog::Write(LOGLEVEL_WARN, sprintf("Failed to set properties, trying to set them separately. Error code was:%x", mapi_last_hresult()));
2050
-			$this->setPropsIndividually($mapimessage, $propsToSet, $mapiprops);
2051
-		}
2052
-
2053
-		mapi_deleteprops($mapimessage, $propsToDelete);
2054
-
2055
-		// clean up
2056
-		unset($unsetVars, $propsToDelete);
2057
-	}
2058
-
2059
-	/**
2060
-	 * Sets the properties one by one in a MAPI object.
2061
-	 *
2062
-	 * @param mixed &$mapimessage
2063
-	 * @param array &$propsToSet
2064
-	 * @param array &$mapiprops
2065
-	 *
2066
-	 * @return
2067
-	 */
2068
-	private function setPropsIndividually(&$mapimessage, &$propsToSet, &$mapiprops) {
2069
-		foreach ($propsToSet as $prop => $value) {
2070
-			mapi_setprops($mapimessage, [$prop => $value]);
2071
-			if (mapi_last_hresult()) {
2072
-				SLog::Write(LOGLEVEL_ERROR, sprintf("Failed setting property [%s] with value [%s], error code was:%x", array_search($prop, $mapiprops), $value, mapi_last_hresult()));
2073
-			}
2074
-		}
2075
-	}
2076
-
2077
-	/**
2078
-	 * Gets the properties from a MAPI object and sets them in the Sync object according to mapping.
2079
-	 *
2080
-	 * @param SyncObject &$message
2081
-	 * @param mixed      $mapimessage
2082
-	 * @param array      $mapping
2083
-	 *
2084
-	 * @return
2085
-	 */
2086
-	private function getPropsFromMAPI(&$message, $mapimessage, $mapping) {
2087
-		$messageprops = $this->getProps($mapimessage, $mapping);
2088
-		foreach ($mapping as $asprop => $mapiprop) {
2089
-			// Get long strings via openproperty
2090
-			if (isset($messageprops[mapi_prop_tag(PT_ERROR, mapi_prop_id($mapiprop))])) {
2091
-				if ($messageprops[mapi_prop_tag(PT_ERROR, mapi_prop_id($mapiprop))] == MAPI_E_NOT_ENOUGH_MEMORY_32BIT ||
2092
-					$messageprops[mapi_prop_tag(PT_ERROR, mapi_prop_id($mapiprop))] == MAPI_E_NOT_ENOUGH_MEMORY_64BIT) {
2093
-					$messageprops[$mapiprop] = MAPIUtils::readPropStream($mapimessage, $mapiprop);
2094
-				}
2095
-			}
2096
-
2097
-			if (isset($messageprops[$mapiprop])) {
2098
-				if (mapi_prop_type($mapiprop) == PT_BOOLEAN) {
2099
-					// Force to actual '0' or '1'
2100
-					if ($messageprops[$mapiprop]) {
2101
-						$message->{$asprop} = 1;
2102
-					}
2103
-					else {
2104
-						$message->{$asprop} = 0;
2105
-					}
2106
-				}
2107
-				else {
2108
-					// Special handling for PR_MESSAGE_FLAGS
2109
-					if ($mapiprop == PR_MESSAGE_FLAGS) {
2110
-						$message->{$asprop} = $messageprops[$mapiprop] & 1;
2111
-					} // only look at 'read' flag
2112
-					elseif ($mapiprop == PR_RTF_COMPRESSED) {
2113
-						// do not send rtf to the mobile
2114
-						continue;
2115
-					}
2116
-					elseif (is_array($messageprops[$mapiprop])) {
2117
-						$message->{$asprop} = array_map("w2u", $messageprops[$mapiprop]);
2118
-					}
2119
-					else {
2120
-						if (mapi_prop_type($mapiprop) != PT_BINARY && mapi_prop_type($mapiprop) != PT_MV_BINARY) {
2121
-							$message->{$asprop} = w2u($messageprops[$mapiprop]);
2122
-						}
2123
-						else {
2124
-							$message->{$asprop} = $messageprops[$mapiprop];
2125
-						}
2126
-					}
2127
-				}
2128
-			}
2129
-		}
2130
-	}
2131
-
2132
-	/**
2133
-	 * Wraps getPropIdsFromStrings() calls.
2134
-	 *
2135
-	 * @param mixed &$mapiprops
2136
-	 *
2137
-	 * @return
2138
-	 */
2139
-	private function getPropIdsFromStrings(&$mapiprops) {
2140
-		return getPropIdsFromStrings($this->store, $mapiprops);
2141
-	}
2142
-
2143
-	/**
2144
-	 * Wraps mapi_getprops() calls.
2145
-	 *
2146
-	 * @param mixed &$mapiprops
2147
-	 * @param mixed $mapimessage
2148
-	 * @param mixed $mapiproperties
2149
-	 *
2150
-	 * @return
2151
-	 */
2152
-	protected function getProps($mapimessage, &$mapiproperties) {
2153
-		$mapiproperties = $this->getPropIdsFromStrings($mapiproperties);
2154
-
2155
-		return mapi_getprops($mapimessage, $mapiproperties);
2156
-	}
2157
-
2158
-	/**
2159
-	 * Returns an GMT timezone array.
2160
-	 *
2161
-	 * @return array
2162
-	 */
2163
-	private function getGMTTZ() {
2164
-		return [
2165
-			"bias" => 0,
2166
-			"tzname" => "",
2167
-			"dstendyear" => 0,
2168
-			"dstendmonth" => 10,
2169
-			"dstendday" => 0,
2170
-			"dstendweek" => 5,
2171
-			"dstendhour" => 2,
2172
-			"dstendminute" => 0,
2173
-			"dstendsecond" => 0,
2174
-			"dstendmillis" => 0,
2175
-			"stdbias" => 0,
2176
-			"tznamedst" => "",
2177
-			"dststartyear" => 0,
2178
-			"dststartmonth" => 3,
2179
-			"dststartday" => 0,
2180
-			"dststartweek" => 5,
2181
-			"dststarthour" => 1,
2182
-			"dststartminute" => 0,
2183
-			"dststartsecond" => 0,
2184
-			"dststartmillis" => 0,
2185
-			"dstbias" => -60,
2186
-		];
2187
-	}
2188
-
2189
-	/**
2190
-	 * Unpack timezone info from MAPI.
2191
-	 *
2192
-	 * @param string $data
2193
-	 *
2194
-	 * @return array
2195
-	 */
2196
-	private function getTZFromMAPIBlob($data) {
2197
-		return unpack("lbias/lstdbias/ldstbias/" .
2198
-						   "vconst1/vdstendyear/vdstendmonth/vdstendday/vdstendweek/vdstendhour/vdstendminute/vdstendsecond/vdstendmillis/" .
2199
-						   "vconst2/vdststartyear/vdststartmonth/vdststartday/vdststartweek/vdststarthour/vdststartminute/vdststartsecond/vdststartmillis", $data);
2200
-	}
2201
-
2202
-	/**
2203
-	 * Unpack timezone info from Sync.
2204
-	 *
2205
-	 * @param string $data
2206
-	 *
2207
-	 * @return array
2208
-	 */
2209
-	private function getTZFromSyncBlob($data) {
2210
-		$tz = unpack("lbias/a64tzname/vdstendyear/vdstendmonth/vdstendday/vdstendweek/vdstendhour/vdstendminute/vdstendsecond/vdstendmillis/" .
2211
-						"lstdbias/a64tznamedst/vdststartyear/vdststartmonth/vdststartday/vdststartweek/vdststarthour/vdststartminute/vdststartsecond/vdststartmillis/" .
2212
-						"ldstbias", $data);
2213
-
2214
-		// Make the structure compatible with class.recurrence.php
2215
-		$tz["timezone"] = $tz["bias"];
2216
-		$tz["timezonedst"] = $tz["dstbias"];
2217
-
2218
-		return $tz;
2219
-	}
2220
-
2221
-	/**
2222
-	 * Pack timezone info for MAPI.
2223
-	 *
2224
-	 * @param array $tz
2225
-	 *
2226
-	 * @return string
2227
-	 */
2228
-	private function getMAPIBlobFromTZ($tz) {
2229
-		return pack(
2230
-			"lll" . "vvvvvvvvv" . "vvvvvvvvv",
2231
-			$tz["bias"],
2232
-			$tz["stdbias"],
2233
-			$tz["dstbias"],
2234
-			0,
2235
-			0,
2236
-			$tz["dstendmonth"],
2237
-			$tz["dstendday"],
2238
-			$tz["dstendweek"],
2239
-			$tz["dstendhour"],
2240
-			$tz["dstendminute"],
2241
-			$tz["dstendsecond"],
2242
-			$tz["dstendmillis"],
2243
-			0,
2244
-			0,
2245
-			$tz["dststartmonth"],
2246
-			$tz["dststartday"],
2247
-			$tz["dststartweek"],
2248
-			$tz["dststarthour"],
2249
-			$tz["dststartminute"],
2250
-			$tz["dststartsecond"],
2251
-			$tz["dststartmillis"]
2252
-		);
2253
-	}
2254
-
2255
-	/**
2256
-	 * Checks the date to see if it is in DST, and returns correct GMT date accordingly.
2257
-	 *
2258
-	 * @param long  $localtime
2259
-	 * @param array $tz
2260
-	 *
2261
-	 * @return long
2262
-	 */
2263
-	private function getGMTTimeByTZ($localtime, $tz) {
2264
-		if (!isset($tz) || !is_array($tz)) {
2265
-			return $localtime;
2266
-		}
2267
-
2268
-		if ($this->isDST($localtime, $tz)) {
2269
-			return $localtime + $tz["bias"] * 60 + $tz["dstbias"] * 60;
2270
-		}
2271
-
2272
-		return $localtime + $tz["bias"] * 60;
2273
-	}
2274
-
2275
-	/**
2276
-	 * Returns the local time for the given GMT time, taking account of the given timezone.
2277
-	 *
2278
-	 * @param long  $gmttime
2279
-	 * @param array $tz
2280
-	 *
2281
-	 * @return long
2282
-	 */
2283
-	private function getLocaltimeByTZ($gmttime, $tz) {
2284
-		if (!isset($tz) || !is_array($tz)) {
2285
-			return $gmttime;
2286
-		}
2287
-
2288
-		if ($this->isDST($gmttime - $tz["bias"] * 60, $tz)) { // may bug around the switch time because it may have to be 'gmttime - bias - dstbias'
2289
-			return $gmttime - $tz["bias"] * 60 - $tz["dstbias"] * 60;
2290
-		}
2291
-
2292
-		return $gmttime - $tz["bias"] * 60;
2293
-	}
2294
-
2295
-	/**
2296
-	 * Returns TRUE if it is the summer and therefore DST is in effect.
2297
-	 *
2298
-	 * @param long  $localtime
2299
-	 * @param array $tz
2300
-	 *
2301
-	 * @return bool
2302
-	 */
2303
-	private function isDST($localtime, $tz) {
2304
-		if (!isset($tz) || !is_array($tz) ||
2305
-			!isset($tz["dstbias"]) || $tz["dstbias"] == 0 ||
2306
-			!isset($tz["dststartmonth"]) || $tz["dststartmonth"] == 0 ||
2307
-			!isset($tz["dstendmonth"]) || $tz["dstendmonth"] == 0) {
2308
-			return false;
2309
-		}
2310
-
2311
-		$year = gmdate("Y", $localtime);
2312
-		$start = $this->getTimestampOfWeek($year, $tz["dststartmonth"], $tz["dststartweek"], $tz["dststartday"], $tz["dststarthour"], $tz["dststartminute"], $tz["dststartsecond"]);
2313
-		$end = $this->getTimestampOfWeek($year, $tz["dstendmonth"], $tz["dstendweek"], $tz["dstendday"], $tz["dstendhour"], $tz["dstendminute"], $tz["dstendsecond"]);
2314
-
2315
-		if ($start < $end) {
2316
-			// northern hemisphere (july = dst)
2317
-			if ($localtime >= $start && $localtime < $end) {
2318
-				$dst = true;
2319
-			}
2320
-			else {
2321
-				$dst = false;
2322
-			}
2323
-		}
2324
-		else {
2325
-			// southern hemisphere (january = dst)
2326
-			if ($localtime >= $end && $localtime < $start) {
2327
-				$dst = false;
2328
-			}
2329
-			else {
2330
-				$dst = true;
2331
-			}
2332
-		}
2333
-
2334
-		return $dst;
2335
-	}
2336
-
2337
-	/**
2338
-	 * Returns the local timestamp for the $week'th $wday of $month in $year at $hour:$minute:$second.
2339
-	 *
2340
-	 * @param int $year
2341
-	 * @param int $month
2342
-	 * @param int $week
2343
-	 * @param int $wday
2344
-	 * @param int $hour
2345
-	 * @param int $minute
2346
-	 * @param int $second
2347
-	 *
2348
-	 * @return long
2349
-	 */
2350
-	private function getTimestampOfWeek($year, $month, $week, $wday, $hour, $minute, $second) {
2351
-		if ($month == 0) {
2352
-			return;
2353
-		}
2354
-
2355
-		$date = gmmktime($hour, $minute, $second, $month, 1, $year);
2356
-
2357
-		// Find first day in month which matches day of the week
2358
-		while (1) {
2359
-			$wdaynow = gmdate("w", $date);
2360
-			if ($wdaynow == $wday) {
2361
-				break;
2362
-			}
2363
-			$date += 24 * 60 * 60;
2364
-		}
2365
-
2366
-		// Forward $week weeks (may 'overflow' into the next month)
2367
-		$date = $date + $week * (24 * 60 * 60 * 7);
2368
-
2369
-		// Reverse 'overflow'. Eg week '10' will always be the last week of the month in which the
2370
-		// specified weekday exists
2371
-		while (1) {
2372
-			$monthnow = gmdate("n", $date); // gmdate returns 1-12
2373
-			if ($monthnow > $month) {
2374
-				$date = $date - (24 * 7 * 60 * 60);
2375
-			}
2376
-			else {
2377
-				break;
2378
-			}
2379
-		}
2380
-
2381
-		return $date;
2382
-	}
2383
-
2384
-	/**
2385
-	 * Normalize the given timestamp to the start of the day.
2386
-	 *
2387
-	 * @param long $timestamp
2388
-	 *
2389
-	 * @return long
2390
-	 */
2391
-	private function getDayStartOfTimestamp($timestamp) {
2392
-		return $timestamp - ($timestamp % (60 * 60 * 24));
2393
-	}
2394
-
2395
-	/**
2396
-	 * Returns an SMTP address from an entry id.
2397
-	 *
2398
-	 * @param string $entryid
2399
-	 *
2400
-	 * @return string
2401
-	 */
2402
-	private function getSMTPAddressFromEntryID($entryid) {
2403
-		$addrbook = $this->getAddressbook();
2404
-
2405
-		$mailuser = mapi_ab_openentry($addrbook, $entryid);
2406
-		if (!$mailuser) {
2407
-			return "";
2408
-		}
2409
-
2410
-		$props = mapi_getprops($mailuser, [PR_ADDRTYPE, PR_SMTP_ADDRESS, PR_EMAIL_ADDRESS]);
2411
-
2412
-		$addrtype = isset($props[PR_ADDRTYPE]) ? $props[PR_ADDRTYPE] : "";
2413
-
2414
-		if (isset($props[PR_SMTP_ADDRESS])) {
2415
-			return $props[PR_SMTP_ADDRESS];
2416
-		}
2417
-
2418
-		if ($addrtype == "SMTP" && isset($props[PR_EMAIL_ADDRESS])) {
2419
-			return $props[PR_EMAIL_ADDRESS];
2420
-		}
2421
-		if ($addrtype == "ZARAFA" && isset($props[PR_EMAIL_ADDRESS])) {
2422
-			$userinfo = nsp_getuserinfo($props[PR_EMAIL_ADDRESS]);
2423
-			if (is_array($userinfo) && isset($userinfo["primary_email"])) {
2424
-				return $userinfo["primary_email"];
2425
-			}
2426
-		}
2427
-
2428
-		return "";
2429
-	}
2430
-
2431
-	/**
2432
-	 * Returns fullname from an entryid.
2433
-	 *
2434
-	 * @param binary $entryid
2435
-	 *
2436
-	 * @return string fullname or false on error
2437
-	 */
2438
-	private function getFullnameFromEntryID($entryid) {
2439
-		$addrbook = $this->getAddressbook();
2440
-		$mailuser = mapi_ab_openentry($addrbook, $entryid);
2441
-		if (!$mailuser) {
2442
-			SLog::Write(LOGLEVEL_ERROR, sprintf("Unable to get mailuser for getFullnameFromEntryID (0x%X)", mapi_last_hresult()));
2443
-
2444
-			return false;
2445
-		}
2446
-
2447
-		$props = mapi_getprops($mailuser, [PR_DISPLAY_NAME]);
2448
-		if (isset($props[PR_DISPLAY_NAME])) {
2449
-			return $props[PR_DISPLAY_NAME];
2450
-		}
2451
-		SLog::Write(LOGLEVEL_ERROR, sprintf("Unable to get fullname for getFullnameFromEntryID (0x%X)", mapi_last_hresult()));
2452
-
2453
-		return false;
2454
-	}
2455
-
2456
-	/**
2457
-	 * Builds a displayname from several separated values.
2458
-	 *
2459
-	 * @param SyncContact $contact
2460
-	 *
2461
-	 * @return string
2462
-	 */
2463
-	private function composeDisplayName(&$contact) {
2464
-		// Set display name and subject to a combined value of firstname and lastname
2465
-		$cname = (isset($contact->prefix)) ? u2w($contact->prefix) . " " : "";
2466
-		$cname .= u2w($contact->firstname);
2467
-		$cname .= (isset($contact->middlename)) ? " " . u2w($contact->middlename) : "";
2468
-		$cname .= " " . u2w($contact->lastname);
2469
-		$cname .= (isset($contact->suffix)) ? " " . u2w($contact->suffix) : "";
2470
-
2471
-		return trim($cname);
2472
-	}
2473
-
2474
-	/**
2475
-	 * Sets all dependent properties for an email address.
2476
-	 *
2477
-	 * @param string $emailAddress
2478
-	 * @param string $displayName
2479
-	 * @param int    $cnt
2480
-	 * @param array  &$props
2481
-	 * @param array  &$properties
2482
-	 * @param array  &$nremails
2483
-	 * @param int    &$abprovidertype
2484
-	 *
2485
-	 * @return
2486
-	 */
2487
-	private function setEmailAddress($emailAddress, $displayName, $cnt, &$props, &$properties, &$nremails, &$abprovidertype) {
2488
-		if (isset($emailAddress)) {
2489
-			$name = (isset($displayName)) ? $displayName : $emailAddress;
2490
-
2491
-			$props[$properties["emailaddress{$cnt}"]] = $emailAddress;
2492
-			$props[$properties["emailaddressdemail{$cnt}"]] = $emailAddress;
2493
-			$props[$properties["emailaddressdname{$cnt}"]] = $name;
2494
-			$props[$properties["emailaddresstype{$cnt}"]] = "SMTP";
2495
-			$props[$properties["emailaddressentryid{$cnt}"]] = mapi_createoneoff($name, "SMTP", $emailAddress);
2496
-			$nremails[] = $cnt - 1;
2497
-			$abprovidertype |= 2 ^ ($cnt - 1);
2498
-		}
2499
-	}
2500
-
2501
-	/**
2502
-	 * Sets the properties for an address string.
2503
-	 *
2504
-	 * @param string $type        which address is being set
2505
-	 * @param string $city
2506
-	 * @param string $country
2507
-	 * @param string $postalcode
2508
-	 * @param string $state
2509
-	 * @param string $street
2510
-	 * @param array  &$props
2511
-	 * @param array  &$properties
2512
-	 *
2513
-	 * @return
2514
-	 */
2515
-	private function setAddress($type, &$city, &$country, &$postalcode, &$state, &$street, &$props, &$properties) {
2516
-		if (isset($city)) {
2517
-			$props[$properties[$type . "city"]] = $city = u2w($city);
2518
-		}
2519
-
2520
-		if (isset($country)) {
2521
-			$props[$properties[$type . "country"]] = $country = u2w($country);
2522
-		}
2523
-
2524
-		if (isset($postalcode)) {
2525
-			$props[$properties[$type . "postalcode"]] = $postalcode = u2w($postalcode);
2526
-		}
2527
-
2528
-		if (isset($state)) {
2529
-			$props[$properties[$type . "state"]] = $state = u2w($state);
2530
-		}
2531
-
2532
-		if (isset($street)) {
2533
-			$props[$properties[$type . "street"]] = $street = u2w($street);
2534
-		}
2535
-
2536
-		// set composed address
2537
-		$address = Utils::BuildAddressString($street, $postalcode, $city, $state, $country);
2538
-		if ($address) {
2539
-			$props[$properties[$type . "address"]] = $address;
2540
-		}
2541
-	}
2542
-
2543
-	/**
2544
-	 * Sets the properties for a mailing address.
2545
-	 *
2546
-	 * @param string $city
2547
-	 * @param string $country
2548
-	 * @param string $postalcode
2549
-	 * @param string $state
2550
-	 * @param string $street
2551
-	 * @param string $address
2552
-	 * @param array  &$props
2553
-	 * @param array  &$properties
2554
-	 *
2555
-	 * @return
2556
-	 */
2557
-	private function setMailingAddress($city, $country, $postalcode, $state, $street, $address, &$props, &$properties) {
2558
-		if (isset($city)) {
2559
-			$props[$properties["city"]] = $city;
2560
-		}
2561
-		if (isset($country)) {
2562
-			$props[$properties["country"]] = $country;
2563
-		}
2564
-		if (isset($postalcode)) {
2565
-			$props[$properties["postalcode"]] = $postalcode;
2566
-		}
2567
-		if (isset($state)) {
2568
-			$props[$properties["state"]] = $state;
2569
-		}
2570
-		if (isset($street)) {
2571
-			$props[$properties["street"]] = $street;
2572
-		}
2573
-		if (isset($address)) {
2574
-			$props[$properties["postaladdress"]] = $address;
2575
-		}
2576
-	}
2577
-
2578
-	/**
2579
-	 * Sets data in a recurrence array.
2580
-	 *
2581
-	 * @param SyncObject $message
2582
-	 * @param array      &$recur
2583
-	 *
2584
-	 * @return
2585
-	 */
2586
-	private function setRecurrence($message, &$recur) {
2587
-		if (isset($message->complete)) {
2588
-			$recur["complete"] = $message->complete;
2589
-		}
2590
-
2591
-		if (!isset($message->recurrence->interval)) {
2592
-			$message->recurrence->interval = 1;
2593
-		}
2594
-
2595
-		// set the default value of numoccur
2596
-		$recur["numoccur"] = 0;
2597
-		// a place holder for recurrencetype property
2598
-		$recur["recurrencetype"] = 0;
2599
-
2600
-		switch ($message->recurrence->type) {
2601
-			case 0:
2602
-				$recur["type"] = 10;
2603
-				if (isset($message->recurrence->dayofweek)) {
2604
-					$recur["subtype"] = 1;
2605
-				}
2606
-				else {
2607
-					$recur["subtype"] = 0;
2608
-				}
2609
-
2610
-				$recur["everyn"] = $message->recurrence->interval * (60 * 24);
2611
-				$recur["recurrencetype"] = 1;
2612
-				break;
2613
-
2614
-			case 1:
2615
-				$recur["type"] = 11;
2616
-				$recur["subtype"] = 1;
2617
-				$recur["everyn"] = $message->recurrence->interval;
2618
-				$recur["recurrencetype"] = 2;
2619
-				break;
2620
-
2621
-			case 2:
2622
-				$recur["type"] = 12;
2623
-				$recur["subtype"] = 2;
2624
-				$recur["everyn"] = $message->recurrence->interval;
2625
-				$recur["recurrencetype"] = 3;
2626
-				break;
2627
-
2628
-			case 3:
2629
-				$recur["type"] = 12;
2630
-				$recur["subtype"] = 3;
2631
-				$recur["everyn"] = $message->recurrence->interval;
2632
-				$recur["recurrencetype"] = 3;
2633
-				break;
2634
-
2635
-			case 4:
2636
-				$recur["type"] = 13;
2637
-				$recur["subtype"] = 1;
2638
-				$recur["everyn"] = $message->recurrence->interval * 12;
2639
-				$recur["recurrencetype"] = 4;
2640
-				break;
2641
-
2642
-			case 5:
2643
-				$recur["type"] = 13;
2644
-				$recur["subtype"] = 2;
2645
-				$recur["everyn"] = $message->recurrence->interval * 12;
2646
-				$recur["recurrencetype"] = 4;
2647
-				break;
2648
-
2649
-			case 6:
2650
-				$recur["type"] = 13;
2651
-				$recur["subtype"] = 3;
2652
-				$recur["everyn"] = $message->recurrence->interval * 12;
2653
-				$recur["recurrencetype"] = 4;
2654
-				break;
2655
-		}
2656
-
2657
-		// "start" and "end" are in GMT when passing to class.recurrence
2658
-		$recur["end"] = $this->getDayStartOfTimestamp(0x7FFFFFFF); // Maximum GMT value for end by default
2659
-
2660
-		if (isset($message->recurrence->until)) {
2661
-			$recur["term"] = 0x21;
2662
-			$recur["end"] = $message->recurrence->until;
2663
-		}
2664
-		elseif (isset($message->recurrence->occurrences)) {
2665
-			$recur["term"] = 0x22;
2666
-			$recur["numoccur"] = $message->recurrence->occurrences;
2667
-		}
2668
-		else {
2669
-			$recur["term"] = 0x23;
2670
-		}
2671
-
2672
-		if (isset($message->recurrence->dayofweek)) {
2673
-			$recur["weekdays"] = $message->recurrence->dayofweek;
2674
-		}
2675
-		if (isset($message->recurrence->weekofmonth)) {
2676
-			$recur["nday"] = $message->recurrence->weekofmonth;
2677
-		}
2678
-		if (isset($message->recurrence->monthofyear)) {
2679
-			// MAPI stores months as the amount of minutes until the beginning of the month in a
2680
-			// non-leapyear. Why this is, is totally unclear.
2681
-			$monthminutes = [0, 44640, 84960, 129600, 172800, 217440, 260640, 305280, 348480, 393120, 437760, 480960];
2682
-			$recur["month"] = $monthminutes[$message->recurrence->monthofyear - 1];
2683
-		}
2684
-		if (isset($message->recurrence->dayofmonth)) {
2685
-			$recur["monthday"] = $message->recurrence->dayofmonth;
2686
-		}
2687
-	}
2688
-
2689
-	/**
2690
-	 * Extracts the email address (mailbox@host) from an email address because
2691
-	 * some devices send email address as "Firstname Lastname" <[email protected]>.
2692
-	 *
2693
-	 *  @see http://developer.berlios.de/mantis/view.php?id=486
2694
-	 *
2695
-	 *  @param string           $email
2696
-	 *
2697
-	 *  @return string or false on error
2698
-	 */
2699
-	private function extractEmailAddress($email) {
2700
-		if (!isset($this->zRFC822)) {
2701
-			$this->zRFC822 = new Mail_RFC822();
2702
-		}
2703
-		$parsedAddress = $this->zRFC822->parseAddressList($email);
2704
-		if (!isset($parsedAddress[0]->mailbox) || !isset($parsedAddress[0]->host)) {
2705
-			return false;
2706
-		}
2707
-
2708
-		return $parsedAddress[0]->mailbox . '@' . $parsedAddress[0]->host;
2709
-	}
2710
-
2711
-	/**
2712
-	 * Returns the message body for a required format.
2713
-	 *
2714
-	 * @param MAPIMessage $mapimessage
2715
-	 * @param int         $bpReturnType
2716
-	 * @param SyncObject  $message
2717
-	 *
2718
-	 * @return bool
2719
-	 */
2720
-	private function setMessageBodyForType($mapimessage, $bpReturnType, &$message) {
2721
-		$truncateHtmlSafe = false;
2722
-		// default value is PR_BODY
2723
-		$property = PR_BODY;
2724
-
2725
-		switch ($bpReturnType) {
2726
-			case SYNC_BODYPREFERENCE_HTML:
2727
-				$property = PR_HTML;
2728
-				$truncateHtmlSafe = true;
2729
-				break;
2730
-
2731
-			case SYNC_BODYPREFERENCE_RTF:
2732
-				$property = PR_RTF_COMPRESSED;
2733
-				break;
2734
-
2735
-			case SYNC_BODYPREFERENCE_MIME:
2736
-				$stat = $this->imtoinet($mapimessage, $message);
2737
-				if (isset($message->asbody)) {
2738
-					$message->asbody->type = $bpReturnType;
2739
-				}
2740
-
2741
-				return $stat;
2742
-		}
2743
-
2744
-		$stream = mapi_openproperty($mapimessage, $property, IID_IStream, 0, 0);
2745
-		if ($stream) {
2746
-			$stat = mapi_stream_stat($stream);
2747
-			$streamsize = $stat['cb'];
2748
-		}
2749
-		else {
2750
-			$streamsize = 0;
2751
-		}
2752
-
2753
-		// set the properties according to supported AS version
2754
-		if (Request::GetProtocolVersion() >= 12.0) {
2755
-			$message->asbody = new SyncBaseBody();
2756
-			$message->asbody->type = $bpReturnType;
2757
-			if ($bpReturnType == SYNC_BODYPREFERENCE_RTF) {
2758
-				$body = $this->mapiReadStream($stream, $streamsize);
2759
-				$message->asbody->data = StringStreamWrapper::Open(base64_encode($body));
2760
-			}
2761
-			elseif (isset($message->internetcpid) && $bpReturnType == SYNC_BODYPREFERENCE_HTML) {
2762
-				// if PR_HTML is UTF-8 we can stream it directly, else we have to convert to UTF-8 & wrap it
2763
-				if ($message->internetcpid == INTERNET_CPID_UTF8) {
2764
-					$message->asbody->data = MAPIStreamWrapper::Open($stream, $truncateHtmlSafe);
2765
-				}
2766
-				else {
2767
-					$body = $this->mapiReadStream($stream, $streamsize);
2768
-					$message->asbody->data = StringStreamWrapper::Open(Utils::ConvertCodepageStringToUtf8($message->internetcpid, $body), $truncateHtmlSafe);
2769
-					$message->internetcpid = INTERNET_CPID_UTF8;
2770
-				}
2771
-			}
2772
-			else {
2773
-				$message->asbody->data = MAPIStreamWrapper::Open($stream);
2774
-			}
2775
-			$message->asbody->estimatedDataSize = $streamsize;
2776
-		}
2777
-		else {
2778
-			$body = $this->mapiReadStream($stream, $streamsize);
2779
-			$message->body = str_replace("\n", "\r\n", w2u(str_replace("\r", "", $body)));
2780
-			$message->bodysize = $streamsize;
2781
-			$message->bodytruncated = 0;
2782
-		}
2783
-
2784
-		return true;
2785
-	}
2786
-
2787
-	/**
2788
-	 * Reads from a mapi stream, if it's set. If not, returns an empty string.
2789
-	 *
2790
-	 * @param resource $stream
2791
-	 * @param int      $size
2792
-	 *
2793
-	 * @return string
2794
-	 */
2795
-	private function mapiReadStream($stream, $size) {
2796
-		if (!$stream || $size == 0) {
2797
-			return "";
2798
-		}
2799
-
2800
-		return mapi_stream_read($stream, $size);
2801
-	}
2802
-
2803
-	/**
2804
-	 * A wrapper for mapi_inetmapi_imtoinet function.
2805
-	 *
2806
-	 * @param MAPIMessage $mapimessage
2807
-	 * @param SyncObject  $message
2808
-	 *
2809
-	 * @return bool
2810
-	 */
2811
-	private function imtoinet($mapimessage, &$message) {
2812
-		$mapiEmail = mapi_getprops($mapimessage, [PR_EC_IMAP_EMAIL]);
2813
-		$stream = false;
2814
-		if (isset($mapiEmail[PR_EC_IMAP_EMAIL]) || MAPIUtils::GetError(PR_EC_IMAP_EMAIL, $mapiEmail) == MAPI_E_NOT_ENOUGH_MEMORY) {
2815
-			$stream = mapi_openproperty($mapimessage, PR_EC_IMAP_EMAIL, IID_IStream, 0, 0);
2816
-			SLog::Write(LOGLEVEL_DEBUG, "MAPIProvider->imtoinet(): using PR_EC_IMAP_EMAIL as full RFC822 message");
2817
-		}
2818
-		else {
2819
-			$addrbook = $this->getAddressbook();
2820
-			$stream = mapi_inetmapi_imtoinet($this->session, $addrbook, $mapimessage, ['use_tnef' => -1, 'ignore_missing_attachments' => 1]);
2821
-		}
2822
-		if (is_resource($stream)) {
2823
-			$mstreamstat = mapi_stream_stat($stream);
2824
-			$streamsize = $mstreamstat["cb"];
2825
-			if (isset($streamsize)) {
2826
-				if (Request::GetProtocolVersion() >= 12.0) {
2827
-					if (!isset($message->asbody)) {
2828
-						$message->asbody = new SyncBaseBody();
2829
-					}
2830
-					$message->asbody->data = MAPIStreamWrapper::Open($stream);
2831
-					$message->asbody->estimatedDataSize = $streamsize;
2832
-					$message->asbody->truncated = 0;
2833
-				}
2834
-				else {
2835
-					$message->mimedata = MAPIStreamWrapper::Open($stream);
2836
-					$message->mimesize = $streamsize;
2837
-					$message->mimetruncated = 0;
2838
-				}
2839
-				unset($message->body, $message->bodytruncated);
2840
-
2841
-				return true;
2842
-			}
2843
-		}
2844
-		SLog::Write(LOGLEVEL_ERROR, "MAPIProvider->imtoinet(): got no stream or content from mapi_inetmapi_imtoinet()");
2845
-
2846
-		return false;
2847
-	}
2848
-
2849
-	/**
2850
-	 * Sets the message body.
2851
-	 *
2852
-	 * @param MAPIMessage       $mapimessage
2853
-	 * @param ContentParameters $contentparameters
2854
-	 * @param SyncObject        $message
2855
-	 */
2856
-	private function setMessageBody($mapimessage, $contentparameters, &$message) {
2857
-		// get the available body preference types
2858
-		$bpTypes = $contentparameters->GetBodyPreference();
2859
-		if ($bpTypes !== false) {
2860
-			SLog::Write(LOGLEVEL_DEBUG, sprintf("BodyPreference types: %s", implode(', ', $bpTypes)));
2861
-			// do not send mime data if the client requests it
2862
-			if (($contentparameters->GetMimeSupport() == SYNC_MIMESUPPORT_NEVER) && ($key = array_search(SYNC_BODYPREFERENCE_MIME, $bpTypes) !== false)) {
2863
-				unset($bpTypes[$key]);
2864
-				SLog::Write(LOGLEVEL_DEBUG, sprintf("Remove mime body preference type because the device required no mime support. BodyPreference types: %s", implode(', ', $bpTypes)));
2865
-			}
2866
-			// get the best fitting preference type
2867
-			$bpReturnType = Utils::GetBodyPreferenceBestMatch($bpTypes);
2868
-			SLog::Write(LOGLEVEL_DEBUG, sprintf("GetBodyPreferenceBestMatch: %d", $bpReturnType));
2869
-			$bpo = $contentparameters->BodyPreference($bpReturnType);
2870
-			SLog::Write(LOGLEVEL_DEBUG, sprintf("bpo: truncation size:'%d', allornone:'%d', preview:'%d'", $bpo->GetTruncationSize(), $bpo->GetAllOrNone(), $bpo->GetPreview()));
2871
-
2872
-			// Android Blackberry expects a full mime message for signed emails
2873
-			// @see https://jira.z-hub.io/projects/ZP/issues/ZP-1154
2874
-			// @TODO change this when refactoring
2875
-			$props = mapi_getprops($mapimessage, [PR_MESSAGE_CLASS]);
2876
-			if (isset($props[PR_MESSAGE_CLASS]) &&
2877
-					stripos($props[PR_MESSAGE_CLASS], 'IPM.Note.SMIME.MultipartSigned') !== false &&
2878
-					($key = array_search(SYNC_BODYPREFERENCE_MIME, $bpTypes) !== false)) {
2879
-				SLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->setMessageBody(): enforcing SYNC_BODYPREFERENCE_MIME type for a signed message"));
2880
-				$bpReturnType = SYNC_BODYPREFERENCE_MIME;
2881
-			}
2882
-
2883
-			$this->setMessageBodyForType($mapimessage, $bpReturnType, $message);
2884
-			// only set the truncation size data if device set it in request
2885
-			if ($bpo->GetTruncationSize() != false &&
2886
-					$bpReturnType != SYNC_BODYPREFERENCE_MIME &&
2887
-					$message->asbody->estimatedDataSize > $bpo->GetTruncationSize()
2888
-				) {
2889
-				// Truncated plaintext requests are used on iOS for the preview in the email list. All images and links should be removed - see https://jira.z-hub.io/browse/ZP-1025
2890
-				if ($bpReturnType == SYNC_BODYPREFERENCE_PLAIN) {
2891
-					SLog::Write(LOGLEVEL_DEBUG, "MAPIProvider->setMessageBody(): truncated plain-text body requested, stripping all links and images");
2892
-					// Get more data because of the filtering it's most probably going down in size. It's going to be truncated to the correct size below.
2893
-					$plainbody = stream_get_contents($message->asbody->data, $bpo->GetTruncationSize() * 5);
2894
-					$message->asbody->data = StringStreamWrapper::Open(preg_replace('/<http(s){0,1}:\/\/.*?>/i', '', $plainbody));
2895
-				}
2896
-
2897
-				// truncate data stream
2898
-				ftruncate($message->asbody->data, $bpo->GetTruncationSize());
2899
-				$message->asbody->truncated = 1;
2900
-			}
2901
-			// set the preview or windows phones won't show the preview of an email
2902
-			if (Request::GetProtocolVersion() >= 14.0 && $bpo->GetPreview()) {
2903
-				$message->asbody->preview = Utils::Utf8_truncate(MAPIUtils::readPropStream($mapimessage, PR_BODY), $bpo->GetPreview());
2904
-			}
2905
-		}
2906
-		else {
2907
-			// Override 'body' for truncation
2908
-			$truncsize = Utils::GetTruncSize($contentparameters->GetTruncation());
2909
-			$this->setMessageBodyForType($mapimessage, SYNC_BODYPREFERENCE_PLAIN, $message);
2910
-
2911
-			if ($message->bodysize > $truncsize) {
2912
-				$message->body = Utils::Utf8_truncate($message->body, $truncsize);
2913
-				$message->bodytruncated = 1;
2914
-			}
2915
-
2916
-			if (!isset($message->body) || strlen($message->body) == 0) {
2917
-				$message->body = " ";
2918
-			}
2919
-
2920
-			if ($contentparameters->GetMimeSupport() == SYNC_MIMESUPPORT_ALWAYS) {
2921
-				// set the html body for iphone in AS 2.5 version
2922
-				$this->imtoinet($mapimessage, $message);
2923
-			}
2924
-		}
2925
-	}
2926
-
2927
-	/**
2928
-	 * Sets properties for an email message.
2929
-	 *
2930
-	 * @param mixed    $mapimessage
2931
-	 * @param SyncMail $message
2932
-	 */
2933
-	private function setFlag($mapimessage, &$message) {
2934
-		// do nothing if protocol version is lower than 12.0 as flags haven't been defined before
2935
-		if (Request::GetProtocolVersion() < 12.0) {
2936
-			return;
2937
-		}
2938
-
2939
-		$message->flag = new SyncMailFlags();
2940
-
2941
-		$this->getPropsFromMAPI($message->flag, $mapimessage, MAPIMapping::GetMailFlagsMapping());
2942
-	}
2943
-
2944
-	/**
2945
-	 * Sets information from SyncBaseBody type for a MAPI message.
2946
-	 *
2947
-	 * @param SyncBaseBody $asbody
2948
-	 * @param array        $props
2949
-	 * @param array        $appointmentprops
2950
-	 */
2951
-	private function setASbody($asbody, &$props, $appointmentprops) {
2952
-		// TODO: fix checking for the length
2953
-		if (isset($asbody->type, $asbody->data)   /* && strlen($asbody->data) > 0 */) {
2954
-			switch ($asbody->type) {
2955
-				case SYNC_BODYPREFERENCE_PLAIN:
2956
-				default:
2957
-				// set plain body if the type is not in valid range
2958
-					$props[$appointmentprops["body"]] = stream_get_contents($asbody->data);
2959
-					break;
2960
-
2961
-				case SYNC_BODYPREFERENCE_HTML:
2962
-					$props[$appointmentprops["html"]] = stream_get_contents($asbody->data);
2963
-					break;
2964
-
2965
-				case SYNC_BODYPREFERENCE_RTF:
2966
-					break;
2967
-
2968
-				case SYNC_BODYPREFERENCE_MIME:
2969
-					break;
2970
-			}
2971
-		}
2972
-		else {
2973
-			SLog::Write(LOGLEVEL_DEBUG, "MAPIProvider->setASbody either type or data are not set. Setting to empty body");
2974
-			$props[$appointmentprops["body"]] = "";
2975
-		}
2976
-	}
2977
-
2978
-	/**
2979
-	 * Get MAPI addressbook object.
2980
-	 *
2981
-	 * @return MAPIAddressbook object to be used with mapi_ab_* or false on failure
2982
-	 */
2983
-	private function getAddressbook() {
2984
-		if (isset($this->addressbook) && $this->addressbook) {
2985
-			return $this->addressbook;
2986
-		}
2987
-		$this->addressbook = mapi_openaddressbook($this->session);
2988
-		$result = mapi_last_hresult();
2989
-		if ($result && $this->addressbook === false) {
2990
-			SLog::Write(LOGLEVEL_ERROR, sprintf("MAPIProvider->getAddressbook error opening addressbook 0x%X", $result));
2991
-
2992
-			return false;
2993
-		}
2994
-
2995
-		return $this->addressbook;
2996
-	}
2997
-
2998
-	/**
2999
-	 * Gets the required store properties.
3000
-	 *
3001
-	 * @return array
3002
-	 */
3003
-	public function GetStoreProps() {
3004
-		if (!isset($this->storeProps) || empty($this->storeProps)) {
3005
-			SLog::Write(LOGLEVEL_DEBUG, "MAPIProvider->GetStoreProps(): Getting store properties.");
3006
-			$this->storeProps = mapi_getprops($this->store, [PR_IPM_SUBTREE_ENTRYID, PR_IPM_OUTBOX_ENTRYID, PR_IPM_WASTEBASKET_ENTRYID, PR_IPM_SENTMAIL_ENTRYID, PR_ENTRYID, PR_IPM_PUBLIC_FOLDERS_ENTRYID, PR_IPM_FAVORITES_ENTRYID, PR_MAILBOX_OWNER_ENTRYID]);
3007
-			// make sure all properties are set
3008
-			if (!isset($this->storeProps[PR_IPM_WASTEBASKET_ENTRYID])) {
3009
-				$this->storeProps[PR_IPM_WASTEBASKET_ENTRYID] = false;
3010
-			}
3011
-			if (!isset($this->storeProps[PR_IPM_SENTMAIL_ENTRYID])) {
3012
-				$this->storeProps[PR_IPM_SENTMAIL_ENTRYID] = false;
3013
-			}
3014
-			if (!isset($this->storeProps[PR_IPM_OUTBOX_ENTRYID])) {
3015
-				$this->storeProps[PR_IPM_OUTBOX_ENTRYID] = false;
3016
-			}
3017
-			if (!isset($this->storeProps[PR_IPM_PUBLIC_FOLDERS_ENTRYID])) {
3018
-				$this->storeProps[PR_IPM_PUBLIC_FOLDERS_ENTRYID] = false;
3019
-			}
3020
-		}
3021
-
3022
-		return $this->storeProps;
3023
-	}
3024
-
3025
-	/**
3026
-	 * Gets the required inbox properties.
3027
-	 *
3028
-	 * @return array
3029
-	 */
3030
-	public function GetInboxProps() {
3031
-		if (!isset($this->inboxProps) || empty($this->inboxProps)) {
3032
-			SLog::Write(LOGLEVEL_DEBUG, "MAPIProvider->GetInboxProps(): Getting inbox properties.");
3033
-			$this->inboxProps = [];
3034
-			$inbox = mapi_msgstore_getreceivefolder($this->store);
3035
-			if ($inbox) {
3036
-				$this->inboxProps = mapi_getprops($inbox, [PR_ENTRYID, PR_IPM_DRAFTS_ENTRYID, PR_IPM_TASK_ENTRYID, PR_IPM_APPOINTMENT_ENTRYID, PR_IPM_CONTACT_ENTRYID, PR_IPM_NOTE_ENTRYID, PR_IPM_JOURNAL_ENTRYID]);
3037
-				// make sure all properties are set
3038
-				if (!isset($this->inboxProps[PR_ENTRYID])) {
3039
-					$this->inboxProps[PR_ENTRYID] = false;
3040
-				}
3041
-				if (!isset($this->inboxProps[PR_IPM_DRAFTS_ENTRYID])) {
3042
-					$this->inboxProps[PR_IPM_DRAFTS_ENTRYID] = false;
3043
-				}
3044
-				if (!isset($this->inboxProps[PR_IPM_TASK_ENTRYID])) {
3045
-					$this->inboxProps[PR_IPM_TASK_ENTRYID] = false;
3046
-				}
3047
-				if (!isset($this->inboxProps[PR_IPM_APPOINTMENT_ENTRYID])) {
3048
-					$this->inboxProps[PR_IPM_APPOINTMENT_ENTRYID] = false;
3049
-				}
3050
-				if (!isset($this->inboxProps[PR_IPM_CONTACT_ENTRYID])) {
3051
-					$this->inboxProps[PR_IPM_CONTACT_ENTRYID] = false;
3052
-				}
3053
-				if (!isset($this->inboxProps[PR_IPM_NOTE_ENTRYID])) {
3054
-					$this->inboxProps[PR_IPM_NOTE_ENTRYID] = false;
3055
-				}
3056
-				if (!isset($this->inboxProps[PR_IPM_JOURNAL_ENTRYID])) {
3057
-					$this->inboxProps[PR_IPM_JOURNAL_ENTRYID] = false;
3058
-				}
3059
-			}
3060
-		}
3061
-
3062
-		return $this->inboxProps;
3063
-	}
3064
-
3065
-	/**
3066
-	 * Gets the required store root properties.
3067
-	 *
3068
-	 * @return array
3069
-	 */
3070
-	private function getRootProps() {
3071
-		if (!isset($this->rootProps)) {
3072
-			$root = mapi_msgstore_openentry($this->store, null);
3073
-			$this->rootProps = mapi_getprops($root, [PR_IPM_OL2007_ENTRYIDS]);
3074
-		}
3075
-
3076
-		return $this->rootProps;
3077
-	}
3078
-
3079
-	/**
3080
-	 * Returns an array with entryids of some special folders.
3081
-	 *
3082
-	 * @return array
3083
-	 */
3084
-	private function getSpecialFoldersData() {
3085
-		// The persist data of an entry in PR_IPM_OL2007_ENTRYIDS consists of:
3086
-		//      PersistId - e.g. RSF_PID_SUGGESTED_CONTACTS (2 bytes)
3087
-		//      DataElementsSize - size of DataElements field (2 bytes)
3088
-		//      DataElements - array of PersistElement structures (variable size)
3089
-		//          PersistElement Structure consists of
3090
-		//              ElementID - e.g. RSF_ELID_ENTRYID (2 bytes)
3091
-		//              ElementDataSize - size of ElementData (2 bytes)
3092
-		//              ElementData - The data for the special folder identified by the PersistID (variable size)
3093
-		if (empty($this->specialFoldersData)) {
3094
-			$this->specialFoldersData = [];
3095
-			$rootProps = $this->getRootProps();
3096
-			if (isset($rootProps[PR_IPM_OL2007_ENTRYIDS])) {
3097
-				$persistData = $rootProps[PR_IPM_OL2007_ENTRYIDS];
3098
-				while (strlen($persistData) > 0) {
3099
-					// PERSIST_SENTINEL marks the end of the persist data
3100
-					if (strlen($persistData) == 4 && $persistData == PERSIST_SENTINEL) {
3101
-						break;
3102
-					}
3103
-					$unpackedData = unpack("vdataSize/velementID/velDataSize", substr($persistData, 2, 6));
3104
-					if (isset($unpackedData['dataSize'], $unpackedData['elementID']) && $unpackedData['elementID'] == RSF_ELID_ENTRYID && isset($unpackedData['elDataSize'])) {
3105
-						$this->specialFoldersData[] = substr($persistData, 8, $unpackedData['elDataSize']);
3106
-						// Add PersistId and DataElementsSize lengths to the data size as they're not part of it
3107
-						$persistData = substr($persistData, $unpackedData['dataSize'] + 4);
3108
-					}
3109
-					else {
3110
-						SLog::Write(LOGLEVEL_INFO, "MAPIProvider->getSpecialFoldersData(): persistent data is not valid");
3111
-						break;
3112
-					}
3113
-				}
3114
-			}
3115
-		}
3116
-
3117
-		return $this->specialFoldersData;
3118
-	}
3119
-
3120
-	/**
3121
-	 * Extracts email address from PR_SEARCH_KEY property if possible.
3122
-	 *
3123
-	 * @param string $searchKey
3124
-	 *
3125
-	 * @see https://jira.z-hub.io/browse/ZP-1178
3126
-	 *
3127
-	 * @return string
3128
-	 */
3129
-	private function getEmailAddressFromSearchKey($searchKey) {
3130
-		if (strpos($searchKey, ':') !== false && strpos($searchKey, '@') !== false) {
3131
-			SLog::Write(LOGLEVEL_INFO, "MAPIProvider->getEmailAddressFromSearchKey(): fall back to PR_SEARCH_KEY or PR_SENT_REPRESENTING_SEARCH_KEY to resolve user and get email address");
3132
-
3133
-			return trim(strtolower(explode(':', $searchKey)[1]));
3134
-		}
3135
-
3136
-		return "";
3137
-	}
3138
-
3139
-	/**
3140
-	 * Returns categories for a message.
3141
-	 *
3142
-	 * @param binary $parentsourcekey
3143
-	 * @param binary $sourcekey
3144
-	 *
3145
-	 * @return array or false on failure
3146
-	 */
3147
-	public function GetMessageCategories($parentsourcekey, $sourcekey) {
3148
-		$entryid = mapi_msgstore_entryidfromsourcekey($this->store, $parentsourcekey, $sourcekey);
3149
-		if (!$entryid) {
3150
-			SLog::Write(LOGLEVEL_INFO, sprintf("MAPIProvider->GetMessageCategories(): Couldn't retrieve message, sourcekey: '%s', parentsourcekey: '%s'", bin2hex($sourcekey), bin2hex($parentsourcekey)));
3151
-
3152
-			return false;
3153
-		}
3154
-		$mapimessage = mapi_msgstore_openentry($this->store, $entryid);
3155
-		$emailMapping = MAPIMapping::GetEmailMapping();
3156
-		$emailMapping = ["categories" => $emailMapping["categories"]];
3157
-		$messageCategories = $this->getProps($mapimessage, $emailMapping);
3158
-		if (isset($messageCategories[$emailMapping["categories"]]) && is_array($messageCategories[$emailMapping["categories"]])) {
3159
-			return $messageCategories[$emailMapping["categories"]];
3160
-		}
3161
-
3162
-		return false;
3163
-	}
1943
+    /**
1944
+     * Returns the timestamp offset.
1945
+     *
1946
+     * @param string $ts
1947
+     *
1948
+     * @return long
1949
+     */
1950
+    private function GetTZOffset($ts) {
1951
+        $Offset = date("O", $ts);
1952
+
1953
+        $Parity = $Offset < 0 ? -1 : 1;
1954
+        $Offset = $Parity * $Offset;
1955
+        $Offset = ($Offset - ($Offset % 100)) / 100 * 60 + $Offset % 100;
1956
+
1957
+        return $Parity * $Offset;
1958
+    }
1959
+
1960
+    /**
1961
+     * Localtime of the timestamp.
1962
+     *
1963
+     * @param long $time
1964
+     *
1965
+     * @return array
1966
+     */
1967
+    private function gmtime($time) {
1968
+        $TZOffset = $this->GetTZOffset($time);
1969
+
1970
+        $t_time = $time - $TZOffset * 60; # Counter adjust for localtime()
1971
+
1972
+        return localtime($t_time, 1);
1973
+    }
1974
+
1975
+    /**
1976
+     * Sets the properties in a MAPI object according to an Sync object and a property mapping.
1977
+     *
1978
+     * @param mixed      $mapimessage
1979
+     * @param SyncObject $message
1980
+     * @param array      $mapping
1981
+     *
1982
+     * @return
1983
+     */
1984
+    private function setPropsInMAPI($mapimessage, $message, $mapping) {
1985
+        $mapiprops = $this->getPropIdsFromStrings($mapping);
1986
+        $unsetVars = $message->getUnsetVars();
1987
+        $propsToDelete = [];
1988
+        $propsToSet = [];
1989
+
1990
+        foreach ($mapiprops as $asprop => $mapiprop) {
1991
+            if (isset($message->{$asprop})) {
1992
+                // UTF8->windows1252.. this is ok for all numerical values
1993
+                if (mapi_prop_type($mapiprop) != PT_BINARY && mapi_prop_type($mapiprop) != PT_MV_BINARY) {
1994
+                    if (is_array($message->{$asprop})) {
1995
+                        $value = array_map("u2wi", $message->{$asprop});
1996
+                    }
1997
+                    else {
1998
+                        $value = u2wi($message->{$asprop});
1999
+                    }
2000
+                }
2001
+                else {
2002
+                    $value = $message->{$asprop};
2003
+                }
2004
+
2005
+                // Make sure the php values are the correct type
2006
+                switch (mapi_prop_type($mapiprop)) {
2007
+                    case PT_BINARY:
2008
+                    case PT_STRING8:
2009
+                        settype($value, "string");
2010
+                        break;
2011
+
2012
+                    case PT_BOOLEAN:
2013
+                        settype($value, "boolean");
2014
+                        break;
2015
+
2016
+                    case PT_SYSTIME:
2017
+                    case PT_LONG:
2018
+                        settype($value, "integer");
2019
+                        break;
2020
+                }
2021
+
2022
+                // decode base64 value
2023
+                if ($mapiprop == PR_RTF_COMPRESSED) {
2024
+                    $value = base64_decode($value);
2025
+                    if (strlen($value) == 0) {
2026
+                        continue;
2027
+                    } // PDA will sometimes give us an empty RTF, which we'll ignore.
2028
+
2029
+                    // Note that you can still remove notes because when you remove notes it gives
2030
+                    // a valid compressed RTF with nothing in it.
2031
+                }
2032
+                // if an "empty array" is to be saved, it the mvprop should be deleted - fixes Mantis #468
2033
+                if (is_array($value) && empty($value)) {
2034
+                    $propsToDelete[] = $mapiprop;
2035
+                    SLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->setPropsInMAPI(): Property '%s' to be deleted as it is an empty array", $asprop));
2036
+                }
2037
+                else {
2038
+                    // all properties will be set at once
2039
+                    $propsToSet[$mapiprop] = $value;
2040
+                }
2041
+            }
2042
+            elseif (in_array($asprop, $unsetVars)) {
2043
+                $propsToDelete[] = $mapiprop;
2044
+            }
2045
+        }
2046
+
2047
+        mapi_setprops($mapimessage, $propsToSet);
2048
+        if (mapi_last_hresult()) {
2049
+            SLog::Write(LOGLEVEL_WARN, sprintf("Failed to set properties, trying to set them separately. Error code was:%x", mapi_last_hresult()));
2050
+            $this->setPropsIndividually($mapimessage, $propsToSet, $mapiprops);
2051
+        }
2052
+
2053
+        mapi_deleteprops($mapimessage, $propsToDelete);
2054
+
2055
+        // clean up
2056
+        unset($unsetVars, $propsToDelete);
2057
+    }
2058
+
2059
+    /**
2060
+     * Sets the properties one by one in a MAPI object.
2061
+     *
2062
+     * @param mixed &$mapimessage
2063
+     * @param array &$propsToSet
2064
+     * @param array &$mapiprops
2065
+     *
2066
+     * @return
2067
+     */
2068
+    private function setPropsIndividually(&$mapimessage, &$propsToSet, &$mapiprops) {
2069
+        foreach ($propsToSet as $prop => $value) {
2070
+            mapi_setprops($mapimessage, [$prop => $value]);
2071
+            if (mapi_last_hresult()) {
2072
+                SLog::Write(LOGLEVEL_ERROR, sprintf("Failed setting property [%s] with value [%s], error code was:%x", array_search($prop, $mapiprops), $value, mapi_last_hresult()));
2073
+            }
2074
+        }
2075
+    }
2076
+
2077
+    /**
2078
+     * Gets the properties from a MAPI object and sets them in the Sync object according to mapping.
2079
+     *
2080
+     * @param SyncObject &$message
2081
+     * @param mixed      $mapimessage
2082
+     * @param array      $mapping
2083
+     *
2084
+     * @return
2085
+     */
2086
+    private function getPropsFromMAPI(&$message, $mapimessage, $mapping) {
2087
+        $messageprops = $this->getProps($mapimessage, $mapping);
2088
+        foreach ($mapping as $asprop => $mapiprop) {
2089
+            // Get long strings via openproperty
2090
+            if (isset($messageprops[mapi_prop_tag(PT_ERROR, mapi_prop_id($mapiprop))])) {
2091
+                if ($messageprops[mapi_prop_tag(PT_ERROR, mapi_prop_id($mapiprop))] == MAPI_E_NOT_ENOUGH_MEMORY_32BIT ||
2092
+                    $messageprops[mapi_prop_tag(PT_ERROR, mapi_prop_id($mapiprop))] == MAPI_E_NOT_ENOUGH_MEMORY_64BIT) {
2093
+                    $messageprops[$mapiprop] = MAPIUtils::readPropStream($mapimessage, $mapiprop);
2094
+                }
2095
+            }
2096
+
2097
+            if (isset($messageprops[$mapiprop])) {
2098
+                if (mapi_prop_type($mapiprop) == PT_BOOLEAN) {
2099
+                    // Force to actual '0' or '1'
2100
+                    if ($messageprops[$mapiprop]) {
2101
+                        $message->{$asprop} = 1;
2102
+                    }
2103
+                    else {
2104
+                        $message->{$asprop} = 0;
2105
+                    }
2106
+                }
2107
+                else {
2108
+                    // Special handling for PR_MESSAGE_FLAGS
2109
+                    if ($mapiprop == PR_MESSAGE_FLAGS) {
2110
+                        $message->{$asprop} = $messageprops[$mapiprop] & 1;
2111
+                    } // only look at 'read' flag
2112
+                    elseif ($mapiprop == PR_RTF_COMPRESSED) {
2113
+                        // do not send rtf to the mobile
2114
+                        continue;
2115
+                    }
2116
+                    elseif (is_array($messageprops[$mapiprop])) {
2117
+                        $message->{$asprop} = array_map("w2u", $messageprops[$mapiprop]);
2118
+                    }
2119
+                    else {
2120
+                        if (mapi_prop_type($mapiprop) != PT_BINARY && mapi_prop_type($mapiprop) != PT_MV_BINARY) {
2121
+                            $message->{$asprop} = w2u($messageprops[$mapiprop]);
2122
+                        }
2123
+                        else {
2124
+                            $message->{$asprop} = $messageprops[$mapiprop];
2125
+                        }
2126
+                    }
2127
+                }
2128
+            }
2129
+        }
2130
+    }
2131
+
2132
+    /**
2133
+     * Wraps getPropIdsFromStrings() calls.
2134
+     *
2135
+     * @param mixed &$mapiprops
2136
+     *
2137
+     * @return
2138
+     */
2139
+    private function getPropIdsFromStrings(&$mapiprops) {
2140
+        return getPropIdsFromStrings($this->store, $mapiprops);
2141
+    }
2142
+
2143
+    /**
2144
+     * Wraps mapi_getprops() calls.
2145
+     *
2146
+     * @param mixed &$mapiprops
2147
+     * @param mixed $mapimessage
2148
+     * @param mixed $mapiproperties
2149
+     *
2150
+     * @return
2151
+     */
2152
+    protected function getProps($mapimessage, &$mapiproperties) {
2153
+        $mapiproperties = $this->getPropIdsFromStrings($mapiproperties);
2154
+
2155
+        return mapi_getprops($mapimessage, $mapiproperties);
2156
+    }
2157
+
2158
+    /**
2159
+     * Returns an GMT timezone array.
2160
+     *
2161
+     * @return array
2162
+     */
2163
+    private function getGMTTZ() {
2164
+        return [
2165
+            "bias" => 0,
2166
+            "tzname" => "",
2167
+            "dstendyear" => 0,
2168
+            "dstendmonth" => 10,
2169
+            "dstendday" => 0,
2170
+            "dstendweek" => 5,
2171
+            "dstendhour" => 2,
2172
+            "dstendminute" => 0,
2173
+            "dstendsecond" => 0,
2174
+            "dstendmillis" => 0,
2175
+            "stdbias" => 0,
2176
+            "tznamedst" => "",
2177
+            "dststartyear" => 0,
2178
+            "dststartmonth" => 3,
2179
+            "dststartday" => 0,
2180
+            "dststartweek" => 5,
2181
+            "dststarthour" => 1,
2182
+            "dststartminute" => 0,
2183
+            "dststartsecond" => 0,
2184
+            "dststartmillis" => 0,
2185
+            "dstbias" => -60,
2186
+        ];
2187
+    }
2188
+
2189
+    /**
2190
+     * Unpack timezone info from MAPI.
2191
+     *
2192
+     * @param string $data
2193
+     *
2194
+     * @return array
2195
+     */
2196
+    private function getTZFromMAPIBlob($data) {
2197
+        return unpack("lbias/lstdbias/ldstbias/" .
2198
+                            "vconst1/vdstendyear/vdstendmonth/vdstendday/vdstendweek/vdstendhour/vdstendminute/vdstendsecond/vdstendmillis/" .
2199
+                            "vconst2/vdststartyear/vdststartmonth/vdststartday/vdststartweek/vdststarthour/vdststartminute/vdststartsecond/vdststartmillis", $data);
2200
+    }
2201
+
2202
+    /**
2203
+     * Unpack timezone info from Sync.
2204
+     *
2205
+     * @param string $data
2206
+     *
2207
+     * @return array
2208
+     */
2209
+    private function getTZFromSyncBlob($data) {
2210
+        $tz = unpack("lbias/a64tzname/vdstendyear/vdstendmonth/vdstendday/vdstendweek/vdstendhour/vdstendminute/vdstendsecond/vdstendmillis/" .
2211
+                        "lstdbias/a64tznamedst/vdststartyear/vdststartmonth/vdststartday/vdststartweek/vdststarthour/vdststartminute/vdststartsecond/vdststartmillis/" .
2212
+                        "ldstbias", $data);
2213
+
2214
+        // Make the structure compatible with class.recurrence.php
2215
+        $tz["timezone"] = $tz["bias"];
2216
+        $tz["timezonedst"] = $tz["dstbias"];
2217
+
2218
+        return $tz;
2219
+    }
2220
+
2221
+    /**
2222
+     * Pack timezone info for MAPI.
2223
+     *
2224
+     * @param array $tz
2225
+     *
2226
+     * @return string
2227
+     */
2228
+    private function getMAPIBlobFromTZ($tz) {
2229
+        return pack(
2230
+            "lll" . "vvvvvvvvv" . "vvvvvvvvv",
2231
+            $tz["bias"],
2232
+            $tz["stdbias"],
2233
+            $tz["dstbias"],
2234
+            0,
2235
+            0,
2236
+            $tz["dstendmonth"],
2237
+            $tz["dstendday"],
2238
+            $tz["dstendweek"],
2239
+            $tz["dstendhour"],
2240
+            $tz["dstendminute"],
2241
+            $tz["dstendsecond"],
2242
+            $tz["dstendmillis"],
2243
+            0,
2244
+            0,
2245
+            $tz["dststartmonth"],
2246
+            $tz["dststartday"],
2247
+            $tz["dststartweek"],
2248
+            $tz["dststarthour"],
2249
+            $tz["dststartminute"],
2250
+            $tz["dststartsecond"],
2251
+            $tz["dststartmillis"]
2252
+        );
2253
+    }
2254
+
2255
+    /**
2256
+     * Checks the date to see if it is in DST, and returns correct GMT date accordingly.
2257
+     *
2258
+     * @param long  $localtime
2259
+     * @param array $tz
2260
+     *
2261
+     * @return long
2262
+     */
2263
+    private function getGMTTimeByTZ($localtime, $tz) {
2264
+        if (!isset($tz) || !is_array($tz)) {
2265
+            return $localtime;
2266
+        }
2267
+
2268
+        if ($this->isDST($localtime, $tz)) {
2269
+            return $localtime + $tz["bias"] * 60 + $tz["dstbias"] * 60;
2270
+        }
2271
+
2272
+        return $localtime + $tz["bias"] * 60;
2273
+    }
2274
+
2275
+    /**
2276
+     * Returns the local time for the given GMT time, taking account of the given timezone.
2277
+     *
2278
+     * @param long  $gmttime
2279
+     * @param array $tz
2280
+     *
2281
+     * @return long
2282
+     */
2283
+    private function getLocaltimeByTZ($gmttime, $tz) {
2284
+        if (!isset($tz) || !is_array($tz)) {
2285
+            return $gmttime;
2286
+        }
2287
+
2288
+        if ($this->isDST($gmttime - $tz["bias"] * 60, $tz)) { // may bug around the switch time because it may have to be 'gmttime - bias - dstbias'
2289
+            return $gmttime - $tz["bias"] * 60 - $tz["dstbias"] * 60;
2290
+        }
2291
+
2292
+        return $gmttime - $tz["bias"] * 60;
2293
+    }
2294
+
2295
+    /**
2296
+     * Returns TRUE if it is the summer and therefore DST is in effect.
2297
+     *
2298
+     * @param long  $localtime
2299
+     * @param array $tz
2300
+     *
2301
+     * @return bool
2302
+     */
2303
+    private function isDST($localtime, $tz) {
2304
+        if (!isset($tz) || !is_array($tz) ||
2305
+            !isset($tz["dstbias"]) || $tz["dstbias"] == 0 ||
2306
+            !isset($tz["dststartmonth"]) || $tz["dststartmonth"] == 0 ||
2307
+            !isset($tz["dstendmonth"]) || $tz["dstendmonth"] == 0) {
2308
+            return false;
2309
+        }
2310
+
2311
+        $year = gmdate("Y", $localtime);
2312
+        $start = $this->getTimestampOfWeek($year, $tz["dststartmonth"], $tz["dststartweek"], $tz["dststartday"], $tz["dststarthour"], $tz["dststartminute"], $tz["dststartsecond"]);
2313
+        $end = $this->getTimestampOfWeek($year, $tz["dstendmonth"], $tz["dstendweek"], $tz["dstendday"], $tz["dstendhour"], $tz["dstendminute"], $tz["dstendsecond"]);
2314
+
2315
+        if ($start < $end) {
2316
+            // northern hemisphere (july = dst)
2317
+            if ($localtime >= $start && $localtime < $end) {
2318
+                $dst = true;
2319
+            }
2320
+            else {
2321
+                $dst = false;
2322
+            }
2323
+        }
2324
+        else {
2325
+            // southern hemisphere (january = dst)
2326
+            if ($localtime >= $end && $localtime < $start) {
2327
+                $dst = false;
2328
+            }
2329
+            else {
2330
+                $dst = true;
2331
+            }
2332
+        }
2333
+
2334
+        return $dst;
2335
+    }
2336
+
2337
+    /**
2338
+     * Returns the local timestamp for the $week'th $wday of $month in $year at $hour:$minute:$second.
2339
+     *
2340
+     * @param int $year
2341
+     * @param int $month
2342
+     * @param int $week
2343
+     * @param int $wday
2344
+     * @param int $hour
2345
+     * @param int $minute
2346
+     * @param int $second
2347
+     *
2348
+     * @return long
2349
+     */
2350
+    private function getTimestampOfWeek($year, $month, $week, $wday, $hour, $minute, $second) {
2351
+        if ($month == 0) {
2352
+            return;
2353
+        }
2354
+
2355
+        $date = gmmktime($hour, $minute, $second, $month, 1, $year);
2356
+
2357
+        // Find first day in month which matches day of the week
2358
+        while (1) {
2359
+            $wdaynow = gmdate("w", $date);
2360
+            if ($wdaynow == $wday) {
2361
+                break;
2362
+            }
2363
+            $date += 24 * 60 * 60;
2364
+        }
2365
+
2366
+        // Forward $week weeks (may 'overflow' into the next month)
2367
+        $date = $date + $week * (24 * 60 * 60 * 7);
2368
+
2369
+        // Reverse 'overflow'. Eg week '10' will always be the last week of the month in which the
2370
+        // specified weekday exists
2371
+        while (1) {
2372
+            $monthnow = gmdate("n", $date); // gmdate returns 1-12
2373
+            if ($monthnow > $month) {
2374
+                $date = $date - (24 * 7 * 60 * 60);
2375
+            }
2376
+            else {
2377
+                break;
2378
+            }
2379
+        }
2380
+
2381
+        return $date;
2382
+    }
2383
+
2384
+    /**
2385
+     * Normalize the given timestamp to the start of the day.
2386
+     *
2387
+     * @param long $timestamp
2388
+     *
2389
+     * @return long
2390
+     */
2391
+    private function getDayStartOfTimestamp($timestamp) {
2392
+        return $timestamp - ($timestamp % (60 * 60 * 24));
2393
+    }
2394
+
2395
+    /**
2396
+     * Returns an SMTP address from an entry id.
2397
+     *
2398
+     * @param string $entryid
2399
+     *
2400
+     * @return string
2401
+     */
2402
+    private function getSMTPAddressFromEntryID($entryid) {
2403
+        $addrbook = $this->getAddressbook();
2404
+
2405
+        $mailuser = mapi_ab_openentry($addrbook, $entryid);
2406
+        if (!$mailuser) {
2407
+            return "";
2408
+        }
2409
+
2410
+        $props = mapi_getprops($mailuser, [PR_ADDRTYPE, PR_SMTP_ADDRESS, PR_EMAIL_ADDRESS]);
2411
+
2412
+        $addrtype = isset($props[PR_ADDRTYPE]) ? $props[PR_ADDRTYPE] : "";
2413
+
2414
+        if (isset($props[PR_SMTP_ADDRESS])) {
2415
+            return $props[PR_SMTP_ADDRESS];
2416
+        }
2417
+
2418
+        if ($addrtype == "SMTP" && isset($props[PR_EMAIL_ADDRESS])) {
2419
+            return $props[PR_EMAIL_ADDRESS];
2420
+        }
2421
+        if ($addrtype == "ZARAFA" && isset($props[PR_EMAIL_ADDRESS])) {
2422
+            $userinfo = nsp_getuserinfo($props[PR_EMAIL_ADDRESS]);
2423
+            if (is_array($userinfo) && isset($userinfo["primary_email"])) {
2424
+                return $userinfo["primary_email"];
2425
+            }
2426
+        }
2427
+
2428
+        return "";
2429
+    }
2430
+
2431
+    /**
2432
+     * Returns fullname from an entryid.
2433
+     *
2434
+     * @param binary $entryid
2435
+     *
2436
+     * @return string fullname or false on error
2437
+     */
2438
+    private function getFullnameFromEntryID($entryid) {
2439
+        $addrbook = $this->getAddressbook();
2440
+        $mailuser = mapi_ab_openentry($addrbook, $entryid);
2441
+        if (!$mailuser) {
2442
+            SLog::Write(LOGLEVEL_ERROR, sprintf("Unable to get mailuser for getFullnameFromEntryID (0x%X)", mapi_last_hresult()));
2443
+
2444
+            return false;
2445
+        }
2446
+
2447
+        $props = mapi_getprops($mailuser, [PR_DISPLAY_NAME]);
2448
+        if (isset($props[PR_DISPLAY_NAME])) {
2449
+            return $props[PR_DISPLAY_NAME];
2450
+        }
2451
+        SLog::Write(LOGLEVEL_ERROR, sprintf("Unable to get fullname for getFullnameFromEntryID (0x%X)", mapi_last_hresult()));
2452
+
2453
+        return false;
2454
+    }
2455
+
2456
+    /**
2457
+     * Builds a displayname from several separated values.
2458
+     *
2459
+     * @param SyncContact $contact
2460
+     *
2461
+     * @return string
2462
+     */
2463
+    private function composeDisplayName(&$contact) {
2464
+        // Set display name and subject to a combined value of firstname and lastname
2465
+        $cname = (isset($contact->prefix)) ? u2w($contact->prefix) . " " : "";
2466
+        $cname .= u2w($contact->firstname);
2467
+        $cname .= (isset($contact->middlename)) ? " " . u2w($contact->middlename) : "";
2468
+        $cname .= " " . u2w($contact->lastname);
2469
+        $cname .= (isset($contact->suffix)) ? " " . u2w($contact->suffix) : "";
2470
+
2471
+        return trim($cname);
2472
+    }
2473
+
2474
+    /**
2475
+     * Sets all dependent properties for an email address.
2476
+     *
2477
+     * @param string $emailAddress
2478
+     * @param string $displayName
2479
+     * @param int    $cnt
2480
+     * @param array  &$props
2481
+     * @param array  &$properties
2482
+     * @param array  &$nremails
2483
+     * @param int    &$abprovidertype
2484
+     *
2485
+     * @return
2486
+     */
2487
+    private function setEmailAddress($emailAddress, $displayName, $cnt, &$props, &$properties, &$nremails, &$abprovidertype) {
2488
+        if (isset($emailAddress)) {
2489
+            $name = (isset($displayName)) ? $displayName : $emailAddress;
2490
+
2491
+            $props[$properties["emailaddress{$cnt}"]] = $emailAddress;
2492
+            $props[$properties["emailaddressdemail{$cnt}"]] = $emailAddress;
2493
+            $props[$properties["emailaddressdname{$cnt}"]] = $name;
2494
+            $props[$properties["emailaddresstype{$cnt}"]] = "SMTP";
2495
+            $props[$properties["emailaddressentryid{$cnt}"]] = mapi_createoneoff($name, "SMTP", $emailAddress);
2496
+            $nremails[] = $cnt - 1;
2497
+            $abprovidertype |= 2 ^ ($cnt - 1);
2498
+        }
2499
+    }
2500
+
2501
+    /**
2502
+     * Sets the properties for an address string.
2503
+     *
2504
+     * @param string $type        which address is being set
2505
+     * @param string $city
2506
+     * @param string $country
2507
+     * @param string $postalcode
2508
+     * @param string $state
2509
+     * @param string $street
2510
+     * @param array  &$props
2511
+     * @param array  &$properties
2512
+     *
2513
+     * @return
2514
+     */
2515
+    private function setAddress($type, &$city, &$country, &$postalcode, &$state, &$street, &$props, &$properties) {
2516
+        if (isset($city)) {
2517
+            $props[$properties[$type . "city"]] = $city = u2w($city);
2518
+        }
2519
+
2520
+        if (isset($country)) {
2521
+            $props[$properties[$type . "country"]] = $country = u2w($country);
2522
+        }
2523
+
2524
+        if (isset($postalcode)) {
2525
+            $props[$properties[$type . "postalcode"]] = $postalcode = u2w($postalcode);
2526
+        }
2527
+
2528
+        if (isset($state)) {
2529
+            $props[$properties[$type . "state"]] = $state = u2w($state);
2530
+        }
2531
+
2532
+        if (isset($street)) {
2533
+            $props[$properties[$type . "street"]] = $street = u2w($street);
2534
+        }
2535
+
2536
+        // set composed address
2537
+        $address = Utils::BuildAddressString($street, $postalcode, $city, $state, $country);
2538
+        if ($address) {
2539
+            $props[$properties[$type . "address"]] = $address;
2540
+        }
2541
+    }
2542
+
2543
+    /**
2544
+     * Sets the properties for a mailing address.
2545
+     *
2546
+     * @param string $city
2547
+     * @param string $country
2548
+     * @param string $postalcode
2549
+     * @param string $state
2550
+     * @param string $street
2551
+     * @param string $address
2552
+     * @param array  &$props
2553
+     * @param array  &$properties
2554
+     *
2555
+     * @return
2556
+     */
2557
+    private function setMailingAddress($city, $country, $postalcode, $state, $street, $address, &$props, &$properties) {
2558
+        if (isset($city)) {
2559
+            $props[$properties["city"]] = $city;
2560
+        }
2561
+        if (isset($country)) {
2562
+            $props[$properties["country"]] = $country;
2563
+        }
2564
+        if (isset($postalcode)) {
2565
+            $props[$properties["postalcode"]] = $postalcode;
2566
+        }
2567
+        if (isset($state)) {
2568
+            $props[$properties["state"]] = $state;
2569
+        }
2570
+        if (isset($street)) {
2571
+            $props[$properties["street"]] = $street;
2572
+        }
2573
+        if (isset($address)) {
2574
+            $props[$properties["postaladdress"]] = $address;
2575
+        }
2576
+    }
2577
+
2578
+    /**
2579
+     * Sets data in a recurrence array.
2580
+     *
2581
+     * @param SyncObject $message
2582
+     * @param array      &$recur
2583
+     *
2584
+     * @return
2585
+     */
2586
+    private function setRecurrence($message, &$recur) {
2587
+        if (isset($message->complete)) {
2588
+            $recur["complete"] = $message->complete;
2589
+        }
2590
+
2591
+        if (!isset($message->recurrence->interval)) {
2592
+            $message->recurrence->interval = 1;
2593
+        }
2594
+
2595
+        // set the default value of numoccur
2596
+        $recur["numoccur"] = 0;
2597
+        // a place holder for recurrencetype property
2598
+        $recur["recurrencetype"] = 0;
2599
+
2600
+        switch ($message->recurrence->type) {
2601
+            case 0:
2602
+                $recur["type"] = 10;
2603
+                if (isset($message->recurrence->dayofweek)) {
2604
+                    $recur["subtype"] = 1;
2605
+                }
2606
+                else {
2607
+                    $recur["subtype"] = 0;
2608
+                }
2609
+
2610
+                $recur["everyn"] = $message->recurrence->interval * (60 * 24);
2611
+                $recur["recurrencetype"] = 1;
2612
+                break;
2613
+
2614
+            case 1:
2615
+                $recur["type"] = 11;
2616
+                $recur["subtype"] = 1;
2617
+                $recur["everyn"] = $message->recurrence->interval;
2618
+                $recur["recurrencetype"] = 2;
2619
+                break;
2620
+
2621
+            case 2:
2622
+                $recur["type"] = 12;
2623
+                $recur["subtype"] = 2;
2624
+                $recur["everyn"] = $message->recurrence->interval;
2625
+                $recur["recurrencetype"] = 3;
2626
+                break;
2627
+
2628
+            case 3:
2629
+                $recur["type"] = 12;
2630
+                $recur["subtype"] = 3;
2631
+                $recur["everyn"] = $message->recurrence->interval;
2632
+                $recur["recurrencetype"] = 3;
2633
+                break;
2634
+
2635
+            case 4:
2636
+                $recur["type"] = 13;
2637
+                $recur["subtype"] = 1;
2638
+                $recur["everyn"] = $message->recurrence->interval * 12;
2639
+                $recur["recurrencetype"] = 4;
2640
+                break;
2641
+
2642
+            case 5:
2643
+                $recur["type"] = 13;
2644
+                $recur["subtype"] = 2;
2645
+                $recur["everyn"] = $message->recurrence->interval * 12;
2646
+                $recur["recurrencetype"] = 4;
2647
+                break;
2648
+
2649
+            case 6:
2650
+                $recur["type"] = 13;
2651
+                $recur["subtype"] = 3;
2652
+                $recur["everyn"] = $message->recurrence->interval * 12;
2653
+                $recur["recurrencetype"] = 4;
2654
+                break;
2655
+        }
2656
+
2657
+        // "start" and "end" are in GMT when passing to class.recurrence
2658
+        $recur["end"] = $this->getDayStartOfTimestamp(0x7FFFFFFF); // Maximum GMT value for end by default
2659
+
2660
+        if (isset($message->recurrence->until)) {
2661
+            $recur["term"] = 0x21;
2662
+            $recur["end"] = $message->recurrence->until;
2663
+        }
2664
+        elseif (isset($message->recurrence->occurrences)) {
2665
+            $recur["term"] = 0x22;
2666
+            $recur["numoccur"] = $message->recurrence->occurrences;
2667
+        }
2668
+        else {
2669
+            $recur["term"] = 0x23;
2670
+        }
2671
+
2672
+        if (isset($message->recurrence->dayofweek)) {
2673
+            $recur["weekdays"] = $message->recurrence->dayofweek;
2674
+        }
2675
+        if (isset($message->recurrence->weekofmonth)) {
2676
+            $recur["nday"] = $message->recurrence->weekofmonth;
2677
+        }
2678
+        if (isset($message->recurrence->monthofyear)) {
2679
+            // MAPI stores months as the amount of minutes until the beginning of the month in a
2680
+            // non-leapyear. Why this is, is totally unclear.
2681
+            $monthminutes = [0, 44640, 84960, 129600, 172800, 217440, 260640, 305280, 348480, 393120, 437760, 480960];
2682
+            $recur["month"] = $monthminutes[$message->recurrence->monthofyear - 1];
2683
+        }
2684
+        if (isset($message->recurrence->dayofmonth)) {
2685
+            $recur["monthday"] = $message->recurrence->dayofmonth;
2686
+        }
2687
+    }
2688
+
2689
+    /**
2690
+     * Extracts the email address (mailbox@host) from an email address because
2691
+     * some devices send email address as "Firstname Lastname" <[email protected]>.
2692
+     *
2693
+     *  @see http://developer.berlios.de/mantis/view.php?id=486
2694
+     *
2695
+     *  @param string           $email
2696
+     *
2697
+     *  @return string or false on error
2698
+     */
2699
+    private function extractEmailAddress($email) {
2700
+        if (!isset($this->zRFC822)) {
2701
+            $this->zRFC822 = new Mail_RFC822();
2702
+        }
2703
+        $parsedAddress = $this->zRFC822->parseAddressList($email);
2704
+        if (!isset($parsedAddress[0]->mailbox) || !isset($parsedAddress[0]->host)) {
2705
+            return false;
2706
+        }
2707
+
2708
+        return $parsedAddress[0]->mailbox . '@' . $parsedAddress[0]->host;
2709
+    }
2710
+
2711
+    /**
2712
+     * Returns the message body for a required format.
2713
+     *
2714
+     * @param MAPIMessage $mapimessage
2715
+     * @param int         $bpReturnType
2716
+     * @param SyncObject  $message
2717
+     *
2718
+     * @return bool
2719
+     */
2720
+    private function setMessageBodyForType($mapimessage, $bpReturnType, &$message) {
2721
+        $truncateHtmlSafe = false;
2722
+        // default value is PR_BODY
2723
+        $property = PR_BODY;
2724
+
2725
+        switch ($bpReturnType) {
2726
+            case SYNC_BODYPREFERENCE_HTML:
2727
+                $property = PR_HTML;
2728
+                $truncateHtmlSafe = true;
2729
+                break;
2730
+
2731
+            case SYNC_BODYPREFERENCE_RTF:
2732
+                $property = PR_RTF_COMPRESSED;
2733
+                break;
2734
+
2735
+            case SYNC_BODYPREFERENCE_MIME:
2736
+                $stat = $this->imtoinet($mapimessage, $message);
2737
+                if (isset($message->asbody)) {
2738
+                    $message->asbody->type = $bpReturnType;
2739
+                }
2740
+
2741
+                return $stat;
2742
+        }
2743
+
2744
+        $stream = mapi_openproperty($mapimessage, $property, IID_IStream, 0, 0);
2745
+        if ($stream) {
2746
+            $stat = mapi_stream_stat($stream);
2747
+            $streamsize = $stat['cb'];
2748
+        }
2749
+        else {
2750
+            $streamsize = 0;
2751
+        }
2752
+
2753
+        // set the properties according to supported AS version
2754
+        if (Request::GetProtocolVersion() >= 12.0) {
2755
+            $message->asbody = new SyncBaseBody();
2756
+            $message->asbody->type = $bpReturnType;
2757
+            if ($bpReturnType == SYNC_BODYPREFERENCE_RTF) {
2758
+                $body = $this->mapiReadStream($stream, $streamsize);
2759
+                $message->asbody->data = StringStreamWrapper::Open(base64_encode($body));
2760
+            }
2761
+            elseif (isset($message->internetcpid) && $bpReturnType == SYNC_BODYPREFERENCE_HTML) {
2762
+                // if PR_HTML is UTF-8 we can stream it directly, else we have to convert to UTF-8 & wrap it
2763
+                if ($message->internetcpid == INTERNET_CPID_UTF8) {
2764
+                    $message->asbody->data = MAPIStreamWrapper::Open($stream, $truncateHtmlSafe);
2765
+                }
2766
+                else {
2767
+                    $body = $this->mapiReadStream($stream, $streamsize);
2768
+                    $message->asbody->data = StringStreamWrapper::Open(Utils::ConvertCodepageStringToUtf8($message->internetcpid, $body), $truncateHtmlSafe);
2769
+                    $message->internetcpid = INTERNET_CPID_UTF8;
2770
+                }
2771
+            }
2772
+            else {
2773
+                $message->asbody->data = MAPIStreamWrapper::Open($stream);
2774
+            }
2775
+            $message->asbody->estimatedDataSize = $streamsize;
2776
+        }
2777
+        else {
2778
+            $body = $this->mapiReadStream($stream, $streamsize);
2779
+            $message->body = str_replace("\n", "\r\n", w2u(str_replace("\r", "", $body)));
2780
+            $message->bodysize = $streamsize;
2781
+            $message->bodytruncated = 0;
2782
+        }
2783
+
2784
+        return true;
2785
+    }
2786
+
2787
+    /**
2788
+     * Reads from a mapi stream, if it's set. If not, returns an empty string.
2789
+     *
2790
+     * @param resource $stream
2791
+     * @param int      $size
2792
+     *
2793
+     * @return string
2794
+     */
2795
+    private function mapiReadStream($stream, $size) {
2796
+        if (!$stream || $size == 0) {
2797
+            return "";
2798
+        }
2799
+
2800
+        return mapi_stream_read($stream, $size);
2801
+    }
2802
+
2803
+    /**
2804
+     * A wrapper for mapi_inetmapi_imtoinet function.
2805
+     *
2806
+     * @param MAPIMessage $mapimessage
2807
+     * @param SyncObject  $message
2808
+     *
2809
+     * @return bool
2810
+     */
2811
+    private function imtoinet($mapimessage, &$message) {
2812
+        $mapiEmail = mapi_getprops($mapimessage, [PR_EC_IMAP_EMAIL]);
2813
+        $stream = false;
2814
+        if (isset($mapiEmail[PR_EC_IMAP_EMAIL]) || MAPIUtils::GetError(PR_EC_IMAP_EMAIL, $mapiEmail) == MAPI_E_NOT_ENOUGH_MEMORY) {
2815
+            $stream = mapi_openproperty($mapimessage, PR_EC_IMAP_EMAIL, IID_IStream, 0, 0);
2816
+            SLog::Write(LOGLEVEL_DEBUG, "MAPIProvider->imtoinet(): using PR_EC_IMAP_EMAIL as full RFC822 message");
2817
+        }
2818
+        else {
2819
+            $addrbook = $this->getAddressbook();
2820
+            $stream = mapi_inetmapi_imtoinet($this->session, $addrbook, $mapimessage, ['use_tnef' => -1, 'ignore_missing_attachments' => 1]);
2821
+        }
2822
+        if (is_resource($stream)) {
2823
+            $mstreamstat = mapi_stream_stat($stream);
2824
+            $streamsize = $mstreamstat["cb"];
2825
+            if (isset($streamsize)) {
2826
+                if (Request::GetProtocolVersion() >= 12.0) {
2827
+                    if (!isset($message->asbody)) {
2828
+                        $message->asbody = new SyncBaseBody();
2829
+                    }
2830
+                    $message->asbody->data = MAPIStreamWrapper::Open($stream);
2831
+                    $message->asbody->estimatedDataSize = $streamsize;
2832
+                    $message->asbody->truncated = 0;
2833
+                }
2834
+                else {
2835
+                    $message->mimedata = MAPIStreamWrapper::Open($stream);
2836
+                    $message->mimesize = $streamsize;
2837
+                    $message->mimetruncated = 0;
2838
+                }
2839
+                unset($message->body, $message->bodytruncated);
2840
+
2841
+                return true;
2842
+            }
2843
+        }
2844
+        SLog::Write(LOGLEVEL_ERROR, "MAPIProvider->imtoinet(): got no stream or content from mapi_inetmapi_imtoinet()");
2845
+
2846
+        return false;
2847
+    }
2848
+
2849
+    /**
2850
+     * Sets the message body.
2851
+     *
2852
+     * @param MAPIMessage       $mapimessage
2853
+     * @param ContentParameters $contentparameters
2854
+     * @param SyncObject        $message
2855
+     */
2856
+    private function setMessageBody($mapimessage, $contentparameters, &$message) {
2857
+        // get the available body preference types
2858
+        $bpTypes = $contentparameters->GetBodyPreference();
2859
+        if ($bpTypes !== false) {
2860
+            SLog::Write(LOGLEVEL_DEBUG, sprintf("BodyPreference types: %s", implode(', ', $bpTypes)));
2861
+            // do not send mime data if the client requests it
2862
+            if (($contentparameters->GetMimeSupport() == SYNC_MIMESUPPORT_NEVER) && ($key = array_search(SYNC_BODYPREFERENCE_MIME, $bpTypes) !== false)) {
2863
+                unset($bpTypes[$key]);
2864
+                SLog::Write(LOGLEVEL_DEBUG, sprintf("Remove mime body preference type because the device required no mime support. BodyPreference types: %s", implode(', ', $bpTypes)));
2865
+            }
2866
+            // get the best fitting preference type
2867
+            $bpReturnType = Utils::GetBodyPreferenceBestMatch($bpTypes);
2868
+            SLog::Write(LOGLEVEL_DEBUG, sprintf("GetBodyPreferenceBestMatch: %d", $bpReturnType));
2869
+            $bpo = $contentparameters->BodyPreference($bpReturnType);
2870
+            SLog::Write(LOGLEVEL_DEBUG, sprintf("bpo: truncation size:'%d', allornone:'%d', preview:'%d'", $bpo->GetTruncationSize(), $bpo->GetAllOrNone(), $bpo->GetPreview()));
2871
+
2872
+            // Android Blackberry expects a full mime message for signed emails
2873
+            // @see https://jira.z-hub.io/projects/ZP/issues/ZP-1154
2874
+            // @TODO change this when refactoring
2875
+            $props = mapi_getprops($mapimessage, [PR_MESSAGE_CLASS]);
2876
+            if (isset($props[PR_MESSAGE_CLASS]) &&
2877
+                    stripos($props[PR_MESSAGE_CLASS], 'IPM.Note.SMIME.MultipartSigned') !== false &&
2878
+                    ($key = array_search(SYNC_BODYPREFERENCE_MIME, $bpTypes) !== false)) {
2879
+                SLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->setMessageBody(): enforcing SYNC_BODYPREFERENCE_MIME type for a signed message"));
2880
+                $bpReturnType = SYNC_BODYPREFERENCE_MIME;
2881
+            }
2882
+
2883
+            $this->setMessageBodyForType($mapimessage, $bpReturnType, $message);
2884
+            // only set the truncation size data if device set it in request
2885
+            if ($bpo->GetTruncationSize() != false &&
2886
+                    $bpReturnType != SYNC_BODYPREFERENCE_MIME &&
2887
+                    $message->asbody->estimatedDataSize > $bpo->GetTruncationSize()
2888
+                ) {
2889
+                // Truncated plaintext requests are used on iOS for the preview in the email list. All images and links should be removed - see https://jira.z-hub.io/browse/ZP-1025
2890
+                if ($bpReturnType == SYNC_BODYPREFERENCE_PLAIN) {
2891
+                    SLog::Write(LOGLEVEL_DEBUG, "MAPIProvider->setMessageBody(): truncated plain-text body requested, stripping all links and images");
2892
+                    // Get more data because of the filtering it's most probably going down in size. It's going to be truncated to the correct size below.
2893
+                    $plainbody = stream_get_contents($message->asbody->data, $bpo->GetTruncationSize() * 5);
2894
+                    $message->asbody->data = StringStreamWrapper::Open(preg_replace('/<http(s){0,1}:\/\/.*?>/i', '', $plainbody));
2895
+                }
2896
+
2897
+                // truncate data stream
2898
+                ftruncate($message->asbody->data, $bpo->GetTruncationSize());
2899
+                $message->asbody->truncated = 1;
2900
+            }
2901
+            // set the preview or windows phones won't show the preview of an email
2902
+            if (Request::GetProtocolVersion() >= 14.0 && $bpo->GetPreview()) {
2903
+                $message->asbody->preview = Utils::Utf8_truncate(MAPIUtils::readPropStream($mapimessage, PR_BODY), $bpo->GetPreview());
2904
+            }
2905
+        }
2906
+        else {
2907
+            // Override 'body' for truncation
2908
+            $truncsize = Utils::GetTruncSize($contentparameters->GetTruncation());
2909
+            $this->setMessageBodyForType($mapimessage, SYNC_BODYPREFERENCE_PLAIN, $message);
2910
+
2911
+            if ($message->bodysize > $truncsize) {
2912
+                $message->body = Utils::Utf8_truncate($message->body, $truncsize);
2913
+                $message->bodytruncated = 1;
2914
+            }
2915
+
2916
+            if (!isset($message->body) || strlen($message->body) == 0) {
2917
+                $message->body = " ";
2918
+            }
2919
+
2920
+            if ($contentparameters->GetMimeSupport() == SYNC_MIMESUPPORT_ALWAYS) {
2921
+                // set the html body for iphone in AS 2.5 version
2922
+                $this->imtoinet($mapimessage, $message);
2923
+            }
2924
+        }
2925
+    }
2926
+
2927
+    /**
2928
+     * Sets properties for an email message.
2929
+     *
2930
+     * @param mixed    $mapimessage
2931
+     * @param SyncMail $message
2932
+     */
2933
+    private function setFlag($mapimessage, &$message) {
2934
+        // do nothing if protocol version is lower than 12.0 as flags haven't been defined before
2935
+        if (Request::GetProtocolVersion() < 12.0) {
2936
+            return;
2937
+        }
2938
+
2939
+        $message->flag = new SyncMailFlags();
2940
+
2941
+        $this->getPropsFromMAPI($message->flag, $mapimessage, MAPIMapping::GetMailFlagsMapping());
2942
+    }
2943
+
2944
+    /**
2945
+     * Sets information from SyncBaseBody type for a MAPI message.
2946
+     *
2947
+     * @param SyncBaseBody $asbody
2948
+     * @param array        $props
2949
+     * @param array        $appointmentprops
2950
+     */
2951
+    private function setASbody($asbody, &$props, $appointmentprops) {
2952
+        // TODO: fix checking for the length
2953
+        if (isset($asbody->type, $asbody->data)   /* && strlen($asbody->data) > 0 */) {
2954
+            switch ($asbody->type) {
2955
+                case SYNC_BODYPREFERENCE_PLAIN:
2956
+                default:
2957
+                // set plain body if the type is not in valid range
2958
+                    $props[$appointmentprops["body"]] = stream_get_contents($asbody->data);
2959
+                    break;
2960
+
2961
+                case SYNC_BODYPREFERENCE_HTML:
2962
+                    $props[$appointmentprops["html"]] = stream_get_contents($asbody->data);
2963
+                    break;
2964
+
2965
+                case SYNC_BODYPREFERENCE_RTF:
2966
+                    break;
2967
+
2968
+                case SYNC_BODYPREFERENCE_MIME:
2969
+                    break;
2970
+            }
2971
+        }
2972
+        else {
2973
+            SLog::Write(LOGLEVEL_DEBUG, "MAPIProvider->setASbody either type or data are not set. Setting to empty body");
2974
+            $props[$appointmentprops["body"]] = "";
2975
+        }
2976
+    }
2977
+
2978
+    /**
2979
+     * Get MAPI addressbook object.
2980
+     *
2981
+     * @return MAPIAddressbook object to be used with mapi_ab_* or false on failure
2982
+     */
2983
+    private function getAddressbook() {
2984
+        if (isset($this->addressbook) && $this->addressbook) {
2985
+            return $this->addressbook;
2986
+        }
2987
+        $this->addressbook = mapi_openaddressbook($this->session);
2988
+        $result = mapi_last_hresult();
2989
+        if ($result && $this->addressbook === false) {
2990
+            SLog::Write(LOGLEVEL_ERROR, sprintf("MAPIProvider->getAddressbook error opening addressbook 0x%X", $result));
2991
+
2992
+            return false;
2993
+        }
2994
+
2995
+        return $this->addressbook;
2996
+    }
2997
+
2998
+    /**
2999
+     * Gets the required store properties.
3000
+     *
3001
+     * @return array
3002
+     */
3003
+    public function GetStoreProps() {
3004
+        if (!isset($this->storeProps) || empty($this->storeProps)) {
3005
+            SLog::Write(LOGLEVEL_DEBUG, "MAPIProvider->GetStoreProps(): Getting store properties.");
3006
+            $this->storeProps = mapi_getprops($this->store, [PR_IPM_SUBTREE_ENTRYID, PR_IPM_OUTBOX_ENTRYID, PR_IPM_WASTEBASKET_ENTRYID, PR_IPM_SENTMAIL_ENTRYID, PR_ENTRYID, PR_IPM_PUBLIC_FOLDERS_ENTRYID, PR_IPM_FAVORITES_ENTRYID, PR_MAILBOX_OWNER_ENTRYID]);
3007
+            // make sure all properties are set
3008
+            if (!isset($this->storeProps[PR_IPM_WASTEBASKET_ENTRYID])) {
3009
+                $this->storeProps[PR_IPM_WASTEBASKET_ENTRYID] = false;
3010
+            }
3011
+            if (!isset($this->storeProps[PR_IPM_SENTMAIL_ENTRYID])) {
3012
+                $this->storeProps[PR_IPM_SENTMAIL_ENTRYID] = false;
3013
+            }
3014
+            if (!isset($this->storeProps[PR_IPM_OUTBOX_ENTRYID])) {
3015
+                $this->storeProps[PR_IPM_OUTBOX_ENTRYID] = false;
3016
+            }
3017
+            if (!isset($this->storeProps[PR_IPM_PUBLIC_FOLDERS_ENTRYID])) {
3018
+                $this->storeProps[PR_IPM_PUBLIC_FOLDERS_ENTRYID] = false;
3019
+            }
3020
+        }
3021
+
3022
+        return $this->storeProps;
3023
+    }
3024
+
3025
+    /**
3026
+     * Gets the required inbox properties.
3027
+     *
3028
+     * @return array
3029
+     */
3030
+    public function GetInboxProps() {
3031
+        if (!isset($this->inboxProps) || empty($this->inboxProps)) {
3032
+            SLog::Write(LOGLEVEL_DEBUG, "MAPIProvider->GetInboxProps(): Getting inbox properties.");
3033
+            $this->inboxProps = [];
3034
+            $inbox = mapi_msgstore_getreceivefolder($this->store);
3035
+            if ($inbox) {
3036
+                $this->inboxProps = mapi_getprops($inbox, [PR_ENTRYID, PR_IPM_DRAFTS_ENTRYID, PR_IPM_TASK_ENTRYID, PR_IPM_APPOINTMENT_ENTRYID, PR_IPM_CONTACT_ENTRYID, PR_IPM_NOTE_ENTRYID, PR_IPM_JOURNAL_ENTRYID]);
3037
+                // make sure all properties are set
3038
+                if (!isset($this->inboxProps[PR_ENTRYID])) {
3039
+                    $this->inboxProps[PR_ENTRYID] = false;
3040
+                }
3041
+                if (!isset($this->inboxProps[PR_IPM_DRAFTS_ENTRYID])) {
3042
+                    $this->inboxProps[PR_IPM_DRAFTS_ENTRYID] = false;
3043
+                }
3044
+                if (!isset($this->inboxProps[PR_IPM_TASK_ENTRYID])) {
3045
+                    $this->inboxProps[PR_IPM_TASK_ENTRYID] = false;
3046
+                }
3047
+                if (!isset($this->inboxProps[PR_IPM_APPOINTMENT_ENTRYID])) {
3048
+                    $this->inboxProps[PR_IPM_APPOINTMENT_ENTRYID] = false;
3049
+                }
3050
+                if (!isset($this->inboxProps[PR_IPM_CONTACT_ENTRYID])) {
3051
+                    $this->inboxProps[PR_IPM_CONTACT_ENTRYID] = false;
3052
+                }
3053
+                if (!isset($this->inboxProps[PR_IPM_NOTE_ENTRYID])) {
3054
+                    $this->inboxProps[PR_IPM_NOTE_ENTRYID] = false;
3055
+                }
3056
+                if (!isset($this->inboxProps[PR_IPM_JOURNAL_ENTRYID])) {
3057
+                    $this->inboxProps[PR_IPM_JOURNAL_ENTRYID] = false;
3058
+                }
3059
+            }
3060
+        }
3061
+
3062
+        return $this->inboxProps;
3063
+    }
3064
+
3065
+    /**
3066
+     * Gets the required store root properties.
3067
+     *
3068
+     * @return array
3069
+     */
3070
+    private function getRootProps() {
3071
+        if (!isset($this->rootProps)) {
3072
+            $root = mapi_msgstore_openentry($this->store, null);
3073
+            $this->rootProps = mapi_getprops($root, [PR_IPM_OL2007_ENTRYIDS]);
3074
+        }
3075
+
3076
+        return $this->rootProps;
3077
+    }
3078
+
3079
+    /**
3080
+     * Returns an array with entryids of some special folders.
3081
+     *
3082
+     * @return array
3083
+     */
3084
+    private function getSpecialFoldersData() {
3085
+        // The persist data of an entry in PR_IPM_OL2007_ENTRYIDS consists of:
3086
+        //      PersistId - e.g. RSF_PID_SUGGESTED_CONTACTS (2 bytes)
3087
+        //      DataElementsSize - size of DataElements field (2 bytes)
3088
+        //      DataElements - array of PersistElement structures (variable size)
3089
+        //          PersistElement Structure consists of
3090
+        //              ElementID - e.g. RSF_ELID_ENTRYID (2 bytes)
3091
+        //              ElementDataSize - size of ElementData (2 bytes)
3092
+        //              ElementData - The data for the special folder identified by the PersistID (variable size)
3093
+        if (empty($this->specialFoldersData)) {
3094
+            $this->specialFoldersData = [];
3095
+            $rootProps = $this->getRootProps();
3096
+            if (isset($rootProps[PR_IPM_OL2007_ENTRYIDS])) {
3097
+                $persistData = $rootProps[PR_IPM_OL2007_ENTRYIDS];
3098
+                while (strlen($persistData) > 0) {
3099
+                    // PERSIST_SENTINEL marks the end of the persist data
3100
+                    if (strlen($persistData) == 4 && $persistData == PERSIST_SENTINEL) {
3101
+                        break;
3102
+                    }
3103
+                    $unpackedData = unpack("vdataSize/velementID/velDataSize", substr($persistData, 2, 6));
3104
+                    if (isset($unpackedData['dataSize'], $unpackedData['elementID']) && $unpackedData['elementID'] == RSF_ELID_ENTRYID && isset($unpackedData['elDataSize'])) {
3105
+                        $this->specialFoldersData[] = substr($persistData, 8, $unpackedData['elDataSize']);
3106
+                        // Add PersistId and DataElementsSize lengths to the data size as they're not part of it
3107
+                        $persistData = substr($persistData, $unpackedData['dataSize'] + 4);
3108
+                    }
3109
+                    else {
3110
+                        SLog::Write(LOGLEVEL_INFO, "MAPIProvider->getSpecialFoldersData(): persistent data is not valid");
3111
+                        break;
3112
+                    }
3113
+                }
3114
+            }
3115
+        }
3116
+
3117
+        return $this->specialFoldersData;
3118
+    }
3119
+
3120
+    /**
3121
+     * Extracts email address from PR_SEARCH_KEY property if possible.
3122
+     *
3123
+     * @param string $searchKey
3124
+     *
3125
+     * @see https://jira.z-hub.io/browse/ZP-1178
3126
+     *
3127
+     * @return string
3128
+     */
3129
+    private function getEmailAddressFromSearchKey($searchKey) {
3130
+        if (strpos($searchKey, ':') !== false && strpos($searchKey, '@') !== false) {
3131
+            SLog::Write(LOGLEVEL_INFO, "MAPIProvider->getEmailAddressFromSearchKey(): fall back to PR_SEARCH_KEY or PR_SENT_REPRESENTING_SEARCH_KEY to resolve user and get email address");
3132
+
3133
+            return trim(strtolower(explode(':', $searchKey)[1]));
3134
+        }
3135
+
3136
+        return "";
3137
+    }
3138
+
3139
+    /**
3140
+     * Returns categories for a message.
3141
+     *
3142
+     * @param binary $parentsourcekey
3143
+     * @param binary $sourcekey
3144
+     *
3145
+     * @return array or false on failure
3146
+     */
3147
+    public function GetMessageCategories($parentsourcekey, $sourcekey) {
3148
+        $entryid = mapi_msgstore_entryidfromsourcekey($this->store, $parentsourcekey, $sourcekey);
3149
+        if (!$entryid) {
3150
+            SLog::Write(LOGLEVEL_INFO, sprintf("MAPIProvider->GetMessageCategories(): Couldn't retrieve message, sourcekey: '%s', parentsourcekey: '%s'", bin2hex($sourcekey), bin2hex($parentsourcekey)));
3151
+
3152
+            return false;
3153
+        }
3154
+        $mapimessage = mapi_msgstore_openentry($this->store, $entryid);
3155
+        $emailMapping = MAPIMapping::GetEmailMapping();
3156
+        $emailMapping = ["categories" => $emailMapping["categories"]];
3157
+        $messageCategories = $this->getProps($mapimessage, $emailMapping);
3158
+        if (isset($messageCategories[$emailMapping["categories"]]) && is_array($messageCategories[$emailMapping["categories"]])) {
3159
+            return $messageCategories[$emailMapping["categories"]];
3160
+        }
3161
+
3162
+        return false;
3163
+    }
3164 3164
 }
Please login to merge, or discard this patch.
Spacing   +29 added lines, -30 removed lines patch added patch discarded remove patch
@@ -462,7 +462,7 @@  discard block
 block discarded – undo
462 462
 		switch ($recurrence->recur["type"]) {
463 463
 			case 10:
464 464
 				if ($recurrence->recur["subtype"] == 0) {
465
-					$syncRecurrence->interval = (int) ($recurrence->recur["everyn"] / 1440);
465
+					$syncRecurrence->interval = (int)($recurrence->recur["everyn"] / 1440);
466 466
 				}  // minutes
467 467
 				break;
468 468
 
@@ -472,7 +472,7 @@  discard block
 block discarded – undo
472 472
 				break; // months / weeks
473 473
 
474 474
 			case 13:
475
-				$syncRecurrence->interval = (int) ($recurrence->recur["everyn"] / 12);
475
+				$syncRecurrence->interval = (int)($recurrence->recur["everyn"] / 12);
476 476
 				break; // months
477 477
 		}
478 478
 
@@ -483,7 +483,7 @@  discard block
 block discarded – undo
483 483
 			$syncRecurrence->weekofmonth = $recurrence->recur["nday"];
484 484
 		} // N'th {DAY} of {X} (0-5)
485 485
 		if (isset($recurrence->recur["month"])) {
486
-			$syncRecurrence->monthofyear = (int) ($recurrence->recur["month"] / (60 * 24 * 29)) + 1;
486
+			$syncRecurrence->monthofyear = (int)($recurrence->recur["month"] / (60 * 24 * 29)) + 1;
487 487
 		} // works ok due to rounding. see also $monthminutes below (1-12)
488 488
 		if (isset($recurrence->recur["monthday"])) {
489 489
 			$syncRecurrence->dayofmonth = $recurrence->recur["monthday"];
@@ -637,10 +637,10 @@  discard block
 block discarded – undo
637 637
 		}
638 638
 
639 639
 		if ($fromname) {
640
-			$from = "\"" . w2u($fromname) . "\" <" . w2u($fromaddr) . ">";
640
+			$from = "\"".w2u($fromname)."\" <".w2u($fromaddr).">";
641 641
 		}
642 642
 		else { // START CHANGED dw2412 HTC shows "error" if sender name is unknown
643
-			$from = "\"" . w2u($fromaddr) . "\" <" . w2u($fromaddr) . ">";
643
+			$from = "\"".w2u($fromaddr)."\" <".w2u($fromaddr).">";
644 644
 		}
645 645
 		// END CHANGED dw2412 HTC shows "error" if sender name is unknown
646 646
 
@@ -841,7 +841,7 @@  discard block
 block discarded – undo
841 841
 					$mime = explode("/", $mimetype);
842 842
 
843 843
 					if (count($mime) == 2 && $mime[0] == "image") {
844
-						$attach->displayname = "inline." . $mime[1];
844
+						$attach->displayname = "inline.".$mime[1];
845 845
 					}
846 846
 				}
847 847
 
@@ -943,10 +943,10 @@  discard block
 block discarded – undo
943 943
 			}
944 944
 			else {
945 945
 				if (substr($name, 0, 1) != '"' && substr($name, -1) != '"') {
946
-					$fulladdr = "\"" . w2u($name) . "\" <" . w2u($address) . ">";
946
+					$fulladdr = "\"".w2u($name)."\" <".w2u($address).">";
947 947
 				}
948 948
 				else {
949
-					$fulladdr = w2u($name) . "<" . w2u($address) . ">";
949
+					$fulladdr = w2u($name)."<".w2u($address).">";
950 950
 				}
951 951
 			}
952 952
 
@@ -1540,8 +1540,7 @@  discard block
 block discarded – undo
1540 1540
 						if (isset($exception->asbody)) {
1541 1541
 							$this->setASbody($exception->asbody, $exceptionprops, $appointmentprops);
1542 1542
 							$mapiexception["body"] = $exceptionprops[$appointmentprops["body"]] =
1543
-								(isset($exceptionprops[$appointmentprops["body"]])) ? $exceptionprops[$appointmentprops["body"]] :
1544
-								((isset($exceptionprops[$appointmentprops["html"]])) ? $exceptionprops[$appointmentprops["html"]] : "");
1543
+								(isset($exceptionprops[$appointmentprops["body"]])) ? $exceptionprops[$appointmentprops["body"]] : ((isset($exceptionprops[$appointmentprops["html"]])) ? $exceptionprops[$appointmentprops["html"]] : "");
1545 1544
 						}
1546 1545
 
1547 1546
 						array_push($recur["changed_occurrences"], $mapiexception);
@@ -1582,7 +1581,7 @@  discard block
 block discarded – undo
1582 1581
 			$props[$appointmentprops["representingname"]] = ($displayname !== false) ? $displayname : Request::GetUser();
1583 1582
 			$props[$appointmentprops["sentrepresentingemail"]] = Request::GetUser();
1584 1583
 			$props[$appointmentprops["sentrepresentingaddt"]] = "ZARAFA";
1585
-			$props[$appointmentprops["sentrepresentinsrchk"]] = $props[$appointmentprops["sentrepresentingaddt"]] . ":" . $props[$appointmentprops["sentrepresentingemail"]];
1584
+			$props[$appointmentprops["sentrepresentinsrchk"]] = $props[$appointmentprops["sentrepresentingaddt"]].":".$props[$appointmentprops["sentrepresentingemail"]];
1586 1585
 
1587 1586
 			if (isset($appointment->attendees) && is_array($appointment->attendees) && !empty($appointment->attendees)) {
1588 1587
 				$props[$appointmentprops["icon"]] = 1026;
@@ -1616,7 +1615,7 @@  discard block
 block discarded – undo
1616 1615
 			$org[PR_ADDRTYPE] = isset($representingprops[$appointmentprops["sentrepresentingaddt"]]) ? $representingprops[$appointmentprops["sentrepresentingaddt"]] : $props[$appointmentprops["sentrepresentingaddt"]];
1617 1616
 			$org[PR_SMTP_ADDRESS] = $org[PR_EMAIL_ADDRESS] = isset($representingprops[$appointmentprops["sentrepresentingemail"]]) ? $representingprops[$appointmentprops["sentrepresentingemail"]] : $props[$appointmentprops["sentrepresentingemail"]];
1618 1617
 			$org[PR_SEARCH_KEY] = isset($representingprops[$appointmentprops["sentrepresentinsrchk"]]) ? $representingprops[$appointmentprops["sentrepresentinsrchk"]] : $props[$appointmentprops["sentrepresentinsrchk"]];
1619
-			$org[PR_RECIPIENT_FLAGS] = recipOrganizer | recipSendable;
1618
+			$org[PR_RECIPIENT_FLAGS] = recipOrganizer|recipSendable;
1620 1619
 			$org[PR_RECIPIENT_TYPE] = MAPI_ORIG;
1621 1620
 
1622 1621
 			array_push($recips, $org);
@@ -1643,7 +1642,7 @@  discard block
 block discarded – undo
1643 1642
 				}
1644 1643
 				else {
1645 1644
 					$recip[PR_DISPLAY_NAME] = u2w($attendee->name);
1646
-					$recip[PR_SEARCH_KEY] = "SMTP:" . $recip[PR_EMAIL_ADDRESS] . "\0";
1645
+					$recip[PR_SEARCH_KEY] = "SMTP:".$recip[PR_EMAIL_ADDRESS]."\0";
1647 1646
 					$recip[PR_ADDRTYPE] = "SMTP";
1648 1647
 					$recip[PR_RECIPIENT_TYPE] = isset($attendee->attendeetype) ? $attendee->attendeetype : MAPI_TO;
1649 1648
 					$recip[PR_ENTRYID] = mapi_createoneoff($recip[PR_DISPLAY_NAME], $recip[PR_ADDRTYPE], $recip[PR_EMAIL_ADDRESS]);
@@ -2194,8 +2193,8 @@  discard block
 block discarded – undo
2194 2193
 	 * @return array
2195 2194
 	 */
2196 2195
 	private function getTZFromMAPIBlob($data) {
2197
-		return unpack("lbias/lstdbias/ldstbias/" .
2198
-						   "vconst1/vdstendyear/vdstendmonth/vdstendday/vdstendweek/vdstendhour/vdstendminute/vdstendsecond/vdstendmillis/" .
2196
+		return unpack("lbias/lstdbias/ldstbias/".
2197
+						   "vconst1/vdstendyear/vdstendmonth/vdstendday/vdstendweek/vdstendhour/vdstendminute/vdstendsecond/vdstendmillis/".
2199 2198
 						   "vconst2/vdststartyear/vdststartmonth/vdststartday/vdststartweek/vdststarthour/vdststartminute/vdststartsecond/vdststartmillis", $data);
2200 2199
 	}
2201 2200
 
@@ -2207,8 +2206,8 @@  discard block
 block discarded – undo
2207 2206
 	 * @return array
2208 2207
 	 */
2209 2208
 	private function getTZFromSyncBlob($data) {
2210
-		$tz = unpack("lbias/a64tzname/vdstendyear/vdstendmonth/vdstendday/vdstendweek/vdstendhour/vdstendminute/vdstendsecond/vdstendmillis/" .
2211
-						"lstdbias/a64tznamedst/vdststartyear/vdststartmonth/vdststartday/vdststartweek/vdststarthour/vdststartminute/vdststartsecond/vdststartmillis/" .
2209
+		$tz = unpack("lbias/a64tzname/vdstendyear/vdstendmonth/vdstendday/vdstendweek/vdstendhour/vdstendminute/vdstendsecond/vdstendmillis/".
2210
+						"lstdbias/a64tznamedst/vdststartyear/vdststartmonth/vdststartday/vdststartweek/vdststarthour/vdststartminute/vdststartsecond/vdststartmillis/".
2212 2211
 						"ldstbias", $data);
2213 2212
 
2214 2213
 		// Make the structure compatible with class.recurrence.php
@@ -2227,7 +2226,7 @@  discard block
 block discarded – undo
2227 2226
 	 */
2228 2227
 	private function getMAPIBlobFromTZ($tz) {
2229 2228
 		return pack(
2230
-			"lll" . "vvvvvvvvv" . "vvvvvvvvv",
2229
+			"lll"."vvvvvvvvv"."vvvvvvvvv",
2231 2230
 			$tz["bias"],
2232 2231
 			$tz["stdbias"],
2233 2232
 			$tz["dstbias"],
@@ -2462,11 +2461,11 @@  discard block
 block discarded – undo
2462 2461
 	 */
2463 2462
 	private function composeDisplayName(&$contact) {
2464 2463
 		// Set display name and subject to a combined value of firstname and lastname
2465
-		$cname = (isset($contact->prefix)) ? u2w($contact->prefix) . " " : "";
2464
+		$cname = (isset($contact->prefix)) ? u2w($contact->prefix)." " : "";
2466 2465
 		$cname .= u2w($contact->firstname);
2467
-		$cname .= (isset($contact->middlename)) ? " " . u2w($contact->middlename) : "";
2468
-		$cname .= " " . u2w($contact->lastname);
2469
-		$cname .= (isset($contact->suffix)) ? " " . u2w($contact->suffix) : "";
2466
+		$cname .= (isset($contact->middlename)) ? " ".u2w($contact->middlename) : "";
2467
+		$cname .= " ".u2w($contact->lastname);
2468
+		$cname .= (isset($contact->suffix)) ? " ".u2w($contact->suffix) : "";
2470 2469
 
2471 2470
 		return trim($cname);
2472 2471
 	}
@@ -2494,7 +2493,7 @@  discard block
 block discarded – undo
2494 2493
 			$props[$properties["emailaddresstype{$cnt}"]] = "SMTP";
2495 2494
 			$props[$properties["emailaddressentryid{$cnt}"]] = mapi_createoneoff($name, "SMTP", $emailAddress);
2496 2495
 			$nremails[] = $cnt - 1;
2497
-			$abprovidertype |= 2 ^ ($cnt - 1);
2496
+			$abprovidertype |= 2^($cnt - 1);
2498 2497
 		}
2499 2498
 	}
2500 2499
 
@@ -2514,29 +2513,29 @@  discard block
 block discarded – undo
2514 2513
 	 */
2515 2514
 	private function setAddress($type, &$city, &$country, &$postalcode, &$state, &$street, &$props, &$properties) {
2516 2515
 		if (isset($city)) {
2517
-			$props[$properties[$type . "city"]] = $city = u2w($city);
2516
+			$props[$properties[$type."city"]] = $city = u2w($city);
2518 2517
 		}
2519 2518
 
2520 2519
 		if (isset($country)) {
2521
-			$props[$properties[$type . "country"]] = $country = u2w($country);
2520
+			$props[$properties[$type."country"]] = $country = u2w($country);
2522 2521
 		}
2523 2522
 
2524 2523
 		if (isset($postalcode)) {
2525
-			$props[$properties[$type . "postalcode"]] = $postalcode = u2w($postalcode);
2524
+			$props[$properties[$type."postalcode"]] = $postalcode = u2w($postalcode);
2526 2525
 		}
2527 2526
 
2528 2527
 		if (isset($state)) {
2529
-			$props[$properties[$type . "state"]] = $state = u2w($state);
2528
+			$props[$properties[$type."state"]] = $state = u2w($state);
2530 2529
 		}
2531 2530
 
2532 2531
 		if (isset($street)) {
2533
-			$props[$properties[$type . "street"]] = $street = u2w($street);
2532
+			$props[$properties[$type."street"]] = $street = u2w($street);
2534 2533
 		}
2535 2534
 
2536 2535
 		// set composed address
2537 2536
 		$address = Utils::BuildAddressString($street, $postalcode, $city, $state, $country);
2538 2537
 		if ($address) {
2539
-			$props[$properties[$type . "address"]] = $address;
2538
+			$props[$properties[$type."address"]] = $address;
2540 2539
 		}
2541 2540
 	}
2542 2541
 
@@ -2705,7 +2704,7 @@  discard block
 block discarded – undo
2705 2704
 			return false;
2706 2705
 		}
2707 2706
 
2708
-		return $parsedAddress[0]->mailbox . '@' . $parsedAddress[0]->host;
2707
+		return $parsedAddress[0]->mailbox.'@'.$parsedAddress[0]->host;
2709 2708
 	}
2710 2709
 
2711 2710
 	/**
Please login to merge, or discard this patch.
Braces   +64 added lines, -128 removed lines patch added patch discarded remove patch
@@ -46,8 +46,7 @@  discard block
 block discarded – undo
46 46
 		$props = mapi_getprops($mapimessage, [PR_MESSAGE_CLASS]);
47 47
 		if (isset($props[PR_MESSAGE_CLASS])) {
48 48
 			$messageclass = $props[PR_MESSAGE_CLASS];
49
-		}
50
-		else {
49
+		} else {
51 50
 			$messageclass = "IPM";
52 51
 		}
53 52
 
@@ -177,16 +176,14 @@  discard block
 block discarded – undo
177 176
 		if (isset($messageprops[$appointmentprops["reminderset"]]) && $messageprops[$appointmentprops["reminderset"]] == true) {
178 177
 			if ($messageprops[$appointmentprops["remindertime"]] == 0x5AE980E1) {
179 178
 				$message->reminder = 15;
180
-			}
181
-			else {
179
+			} else {
182 180
 				$message->reminder = $messageprops[$appointmentprops["remindertime"]];
183 181
 			}
184 182
 		}
185 183
 
186 184
 		if (!isset($message->uid)) {
187 185
 			$message->uid = bin2hex($messageprops[$appointmentprops["sourcekey"]]);
188
-		}
189
-		else {
186
+		} else {
190 187
 			$message->uid = Utils::GetICalUidFromOLUid($message->uid);
191 188
 		}
192 189
 
@@ -205,14 +202,12 @@  discard block
 block discarded – undo
205 202
 		if (!empty($messageprops[$appointmentprops["timezonetag"]])) {
206 203
 			$tz = $this->getTZFromMAPIBlob($messageprops[$appointmentprops["timezonetag"]]);
207 204
 			$appTz = true;
208
-		}
209
-		elseif (!empty($messageprops[$appointmentprops["timezonedesc"]])) {
205
+		} elseif (!empty($messageprops[$appointmentprops["timezonedesc"]])) {
210 206
 			// Windows uses UTC in timezone description in opposite to mstzones in TimezoneUtil which uses GMT
211 207
 			$wintz = str_replace("UTC", "GMT", $messageprops[$appointmentprops["timezonedesc"]]);
212 208
 			$tz = TimezoneUtil::GetFullTZFromTZName(TimezoneUtil::GetTZNameFromWinTZ($wintz));
213 209
 			$appTz = true;
214
-		}
215
-		else {
210
+		} else {
216 211
 			// set server default timezone (correct timezone should be configured!)
217 212
 			$tz = TimezoneUtil::GetFullTZ();
218 213
 		}
@@ -252,8 +247,7 @@  discard block
 block discarded – undo
252 247
 			// smtp address is always a proper email address
253 248
 			if (isset($row[PR_SMTP_ADDRESS])) {
254 249
 				$attendee->email = w2u($row[PR_SMTP_ADDRESS]);
255
-			}
256
-			elseif (isset($row[PR_ADDRTYPE], $row[PR_EMAIL_ADDRESS])) {
250
+			} elseif (isset($row[PR_ADDRTYPE], $row[PR_EMAIL_ADDRESS])) {
257 251
 				// if address type is SMTP, it's also a proper email address
258 252
 				if ($row[PR_ADDRTYPE] == "SMTP") {
259 253
 					$attendee->email = w2u($row[PR_EMAIL_ADDRESS]);
@@ -268,8 +262,7 @@  discard block
 block discarded – undo
268 262
 					// @see https://jira.z-hub.io/browse/ZP-1178
269 263
 					elseif (isset($row[PR_SEARCH_KEY])) {
270 264
 						$attendee->email = w2u($this->getEmailAddressFromSearchKey($row[PR_SEARCH_KEY]));
271
-					}
272
-					else {
265
+					} else {
273 266
 						SLog::Write(LOGLEVEL_WARN, sprintf("MAPIProvider->getAppointment: The attendee '%s' of type ZARAFA can not be resolved. Code: 0x%X", $row[PR_EMAIL_ADDRESS], mapi_last_hresult()));
274 267
 					}
275 268
 				}
@@ -331,8 +324,7 @@  discard block
 block discarded – undo
331 324
 
332 325
 		if (!isset($message->nativebodytype)) {
333 326
 			$message->nativebodytype = MAPIUtils::GetNativeBodyType($messageprops);
334
-		}
335
-		elseif ($message->nativebodytype == SYNC_BODYPREFERENCE_UNDEFINED) {
327
+		} elseif ($message->nativebodytype == SYNC_BODYPREFERENCE_UNDEFINED) {
336 328
 			$nbt = MAPIUtils::GetNativeBodyType($messageprops);
337 329
 			SLog::Write(LOGLEVEL_INFO, sprintf("MAPIProvider->getAppointment(): native body type is undefined. Set it to %d.", $nbt));
338 330
 			$message->nativebodytype = $nbt;
@@ -381,8 +373,7 @@  discard block
 block discarded – undo
381 373
 	private function getRecurrence($mapimessage, $recurprops, &$syncMessage, &$syncRecurrence, $tz) {
382 374
 		if ($syncRecurrence instanceof SyncTaskRecurrence) {
383 375
 			$recurrence = new TaskRecurrence($this->store, $mapimessage);
384
-		}
385
-		else {
376
+		} else {
386 377
 			$recurrence = new Recurrence($this->store, $mapimessage);
387 378
 		}
388 379
 
@@ -606,8 +597,7 @@  discard block
 block discarded – undo
606 597
 
607 598
 		if (isset($messageprops[PR_SOURCE_KEY])) {
608 599
 			$sourcekey = $messageprops[PR_SOURCE_KEY];
609
-		}
610
-		else {
600
+		} else {
611 601
 			$mbe = new SyncObjectBrokenException("The message doesn't have a sourcekey");
612 602
 			$mbe->SetSyncObject($message);
613 603
 
@@ -638,8 +628,7 @@  discard block
 block discarded – undo
638 628
 
639 629
 		if ($fromname) {
640 630
 			$from = "\"" . w2u($fromname) . "\" <" . w2u($fromaddr) . ">";
641
-		}
642
-		else { // START CHANGED dw2412 HTC shows "error" if sender name is unknown
631
+		} else { // START CHANGED dw2412 HTC shows "error" if sender name is unknown
643 632
 			$from = "\"" . w2u($fromaddr) . "\" <" . w2u($fromaddr) . ">";
644 633
 		}
645 634
 		// END CHANGED dw2412 HTC shows "error" if sender name is unknown
@@ -662,8 +651,7 @@  discard block
 block discarded – undo
662 651
 			// Set Timezone
663 652
 			if (isset($props[$meetingrequestproperties["timezonetag"]])) {
664 653
 				$tz = $this->getTZFromMAPIBlob($props[$meetingrequestproperties["timezonetag"]]);
665
-			}
666
-			else {
654
+			} else {
667 655
 				$tz = TimezoneUtil::GetFullTZ();
668 656
 			}
669 657
 
@@ -675,12 +663,10 @@  discard block
 block discarded – undo
675 663
 				if (isset($props[$meetingrequestproperties["recReplTime"]])) {
676 664
 					$basedate = $props[$meetingrequestproperties["recReplTime"]];
677 665
 					$message->meetingrequest->recurrenceid = $this->getGMTTimeByTZ($basedate, $this->getGMTTZ());
678
-				}
679
-				else {
666
+				} else {
680 667
 					if (!isset($props[$meetingrequestproperties["goidtag"]]) || !isset($props[$meetingrequestproperties["recurStartTime"]]) || !isset($props[$meetingrequestproperties["timezonetag"]])) {
681 668
 						SLog::Write(LOGLEVEL_WARN, "Missing property to set correct basedate for exception");
682
-					}
683
-					else {
669
+					} else {
684 670
 						$basedate = Utils::ExtractBaseDate($props[$meetingrequestproperties["goidtag"]], $props[$meetingrequestproperties["recurStartTime"]]);
685 671
 						$message->meetingrequest->recurrenceid = $this->getGMTTimeByTZ($basedate, $tz);
686 672
 					}
@@ -690,8 +676,7 @@  discard block
 block discarded – undo
690 676
 			// Organizer is the sender
691 677
 			if (strpos($message->messageclass, "IPM.Schedule.Meeting.Resp") === 0) {
692 678
 				$message->meetingrequest->organizer = $message->to;
693
-			}
694
-			else {
679
+			} else {
695 680
 				$message->meetingrequest->organizer = $message->from;
696 681
 			}
697 682
 
@@ -716,12 +701,10 @@  discard block
 block discarded – undo
716 701
 			$message->meetingrequest->instancetype = 0;
717 702
 			if (isset($props[$meetingrequestproperties["isrecurringtag"]]) && $props[$meetingrequestproperties["isrecurringtag"]] == 1) {
718 703
 				$message->meetingrequest->instancetype = 1;
719
-			}
720
-			elseif ((!isset($props[$meetingrequestproperties["isrecurringtag"]]) || $props[$meetingrequestproperties["isrecurringtag"]] == 0) && isset($message->meetingrequest->recurrenceid)) {
704
+			} elseif ((!isset($props[$meetingrequestproperties["isrecurringtag"]]) || $props[$meetingrequestproperties["isrecurringtag"]] == 0) && isset($message->meetingrequest->recurrenceid)) {
721 705
 				if (isset($props[$meetingrequestproperties["appSeqNr"]]) && $props[$meetingrequestproperties["appSeqNr"]] == 0) {
722 706
 					$message->meetingrequest->instancetype = 2;
723
-				}
724
-				else {
707
+				} else {
725 708
 					$message->meetingrequest->instancetype = 3;
726 709
 				}
727 710
 			}
@@ -735,8 +718,7 @@  discard block
 block discarded – undo
735 718
 				// /set the default reminder time to seconds
736 719
 				if ($props[$meetingrequestproperties["remindertime"]] == 0x5AE980E1) {
737 720
 					$message->meetingrequest->reminder = 900;
738
-				}
739
-				else {
721
+				} else {
740 722
 					$message->meetingrequest->reminder = $props[$meetingrequestproperties["remindertime"]] * 60;
741 723
 				}
742 724
 			}
@@ -820,8 +802,7 @@  discard block
 block discarded – undo
820 802
 			if (isset($row[PR_ATTACH_NUM])) {
821 803
 				if (Request::GetProtocolVersion() >= 12.0) {
822 804
 					$attach = new SyncBaseAttachment();
823
-				}
824
-				else {
805
+				} else {
825 806
 					$attach = new SyncAttachment();
826 807
 				}
827 808
 
@@ -867,8 +848,7 @@  discard block
 block discarded – undo
867 848
 						}
868 849
 						$stat = mapi_stream_stat($stream);
869 850
 						$attach->estimatedDataSize = $stat['cb'];
870
-					}
871
-					else {
851
+					} else {
872 852
 						$attach->estimatedDataSize = $attachprops[PR_ATTACH_SIZE];
873 853
 					}
874 854
 
@@ -893,8 +873,7 @@  discard block
 block discarded – undo
893 873
 					}
894 874
 
895 875
 					array_push($message->asattachments, $attach);
896
-				}
897
-				else {
876
+				} else {
898 877
 					$attach->attsize = $attachprops[PR_ATTACH_SIZE];
899 878
 					$attach->attname = sprintf("%s:%s:%s", $entryid, $row[PR_ATTACH_NUM], $parentSourcekey);
900 879
 					if (!isset($message->attachments)) {
@@ -922,11 +901,9 @@  discard block
 block discarded – undo
922 901
 
923 902
 			if (isset($row[PR_SMTP_ADDRESS])) {
924 903
 				$address = $row[PR_SMTP_ADDRESS];
925
-			}
926
-			elseif ($addrtype == "SMTP" && isset($row[PR_EMAIL_ADDRESS])) {
904
+			} elseif ($addrtype == "SMTP" && isset($row[PR_EMAIL_ADDRESS])) {
927 905
 				$address = $row[PR_EMAIL_ADDRESS];
928
-			}
929
-			elseif ($addrtype == "ZARAFA" && isset($row[PR_ENTRYID])) {
906
+			} elseif ($addrtype == "ZARAFA" && isset($row[PR_ENTRYID])) {
930 907
 				$address = $this->getSMTPAddressFromEntryID($row[PR_ENTRYID]);
931 908
 			}
932 909
 
@@ -940,20 +917,17 @@  discard block
 block discarded – undo
940 917
 
941 918
 			if ($name == "" || $name == $address) {
942 919
 				$fulladdr = w2u($address);
943
-			}
944
-			else {
920
+			} else {
945 921
 				if (substr($name, 0, 1) != '"' && substr($name, -1) != '"') {
946 922
 					$fulladdr = "\"" . w2u($name) . "\" <" . w2u($address) . ">";
947
-				}
948
-				else {
923
+				} else {
949 924
 					$fulladdr = w2u($name) . "<" . w2u($address) . ">";
950 925
 				}
951 926
 			}
952 927
 
953 928
 			if ($row[PR_RECIPIENT_TYPE] == MAPI_TO) {
954 929
 				array_push($message->to, $fulladdr);
955
-			}
956
-			elseif ($row[PR_RECIPIENT_TYPE] == MAPI_CC) {
930
+			} elseif ($row[PR_RECIPIENT_TYPE] == MAPI_CC) {
957 931
 				array_push($message->cc, $fulladdr);
958 932
 			}
959 933
 		}
@@ -981,8 +955,7 @@  discard block
 block discarded – undo
981 955
 
982 956
 		if (!isset($message->nativebodytype)) {
983 957
 			$message->nativebodytype = MAPIUtils::GetNativeBodyType($messageprops);
984
-		}
985
-		elseif ($message->nativebodytype == SYNC_BODYPREFERENCE_UNDEFINED) {
958
+		} elseif ($message->nativebodytype == SYNC_BODYPREFERENCE_UNDEFINED) {
986 959
 			$nbt = MAPIUtils::GetNativeBodyType($messageprops);
987 960
 			SLog::Write(LOGLEVEL_INFO, sprintf("MAPIProvider->getEmail(): native body type is undefined. Set it to %d.", $nbt));
988 961
 			$message->nativebodytype = $nbt;
@@ -1071,8 +1044,7 @@  discard block
 block discarded – undo
1071 1044
 		$folder->serverid = GSync::GetDeviceManager()->GetFolderIdForBackendId($folder->BackendId, true, $folderOrigin, $folderprops[PR_DISPLAY_NAME]);
1072 1045
 		if ($folderprops[PR_PARENT_ENTRYID] == $storeprops[PR_IPM_SUBTREE_ENTRYID] || $folderprops[PR_PARENT_ENTRYID] == $storeprops[PR_IPM_PUBLIC_FOLDERS_ENTRYID]) {
1073 1046
 			$folder->parentid = "0";
1074
-		}
1075
-		else {
1047
+		} else {
1076 1048
 			$folder->parentid = GSync::GetDeviceManager()->GetFolderIdForBackendId(bin2hex($folderprops[PR_PARENT_SOURCE_KEY]));
1077 1049
 		}
1078 1050
 		$folder->displayname = w2u($folderprops[PR_DISPLAY_NAME]);
@@ -1322,8 +1294,7 @@  discard block
 block discarded – undo
1322 1294
 			$delprops[] = $flagprops["completetime"];
1323 1295
 			$delprops[] = $flagprops["flagstatus"];
1324 1296
 			$delprops[] = $flagprops["flagicon"];
1325
-		}
1326
-		else {
1297
+		} else {
1327 1298
 			$this->setPropsInMAPI($mapimessage, $message->flag, $flagmapping);
1328 1299
 			$props[$flagprops["todoitemsflags"]] = 1;
1329 1300
 			if (isset($message->subject) && strlen($message->subject) > 0) {
@@ -1373,8 +1344,7 @@  discard block
 block discarded – undo
1373 1344
 		// Get timezone info
1374 1345
 		if (isset($appointment->timezone)) {
1375 1346
 			$tz = $this->getTZFromSyncBlob(base64_decode($appointment->timezone));
1376
-		}
1377
-		else {
1347
+		} else {
1378 1348
 			$tz = false;
1379 1349
 		}
1380 1350
 
@@ -1497,8 +1467,7 @@  discard block
 block discarded – undo
1497 1467
 						$noexceptions = false;
1498 1468
 						// Delete exception
1499 1469
 						$recurrence->createException([], $basedate, true);
1500
-					}
1501
-					else {
1470
+					} else {
1502 1471
 						// Change exception
1503 1472
 						$mapiexception = ["basedate" => $basedate];
1504 1473
 						// other exception properties which are not handled in recurrence
@@ -1550,8 +1519,7 @@  discard block
 block discarded – undo
1550 1519
 							$noexceptions = false;
1551 1520
 							if ($recurrence->isException($basedate)) {
1552 1521
 								$recurrence->modifyException($exceptionprops, $basedate);
1553
-							}
1554
-							else {
1522
+							} else {
1555 1523
 								$recurrence->createException($exceptionprops, $basedate);
1556 1524
 							}
1557 1525
 						}
@@ -1563,8 +1531,7 @@  discard block
 block discarded – undo
1563 1531
 			if ($noexceptions) {
1564 1532
 				$recurrence->setRecurrence($tz, $recur);
1565 1533
 			}
1566
-		}
1567
-		else {
1534
+		} else {
1568 1535
 			$props[$appointmentprops["isrecurring"]] = false;
1569 1536
 		}
1570 1537
 
@@ -1640,8 +1607,7 @@  discard block
 block discarded – undo
1640 1607
 					$recip[PR_RECIPIENT_TYPE] = isset($attendee->attendeetype) ? $attendee->attendeetype : MAPI_TO;
1641 1608
 					$recip[PR_RECIPIENT_FLAGS] = recipSendable;
1642 1609
 					$recip[PR_RECIPIENT_TRACKSTATUS] = isset($attendee->attendeestatus) ? $attendee->attendeestatus : olResponseNone;
1643
-				}
1644
-				else {
1610
+				} else {
1645 1611
 					$recip[PR_DISPLAY_NAME] = u2w($attendee->name);
1646 1612
 					$recip[PR_SEARCH_KEY] = "SMTP:" . $recip[PR_EMAIL_ADDRESS] . "\0";
1647 1613
 					$recip[PR_ADDRTYPE] = "SMTP";
@@ -1727,12 +1693,10 @@  discard block
 block discarded – undo
1727 1693
 		if (isset($props[$contactprops["businessaddress"]])) {
1728 1694
 			$props[$contactprops["mailingaddress"]] = 2;
1729 1695
 			$this->setMailingAddress($contact->businesscity, $contact->businesscountry, $contact->businesspostalcode, $contact->businessstate, $contact->businessstreet, $props[$contactprops["businessaddress"]], $props, $contactprops);
1730
-		}
1731
-		elseif (isset($props[$contactprops["homeaddress"]])) {
1696
+		} elseif (isset($props[$contactprops["homeaddress"]])) {
1732 1697
 			$props[$contactprops["mailingaddress"]] = 1;
1733 1698
 			$this->setMailingAddress($contact->homecity, $contact->homecountry, $contact->homepostalcode, $contact->homestate, $contact->homestreet, $props[$contactprops["homeaddress"]], $props, $contactprops);
1734
-		}
1735
-		elseif (isset($props[$contactprops["otheraddress"]])) {
1699
+		} elseif (isset($props[$contactprops["otheraddress"]])) {
1736 1700
 			$props[$contactprops["mailingaddress"]] = 3;
1737 1701
 			$this->setMailingAddress($contact->othercity, $contact->othercountry, $contact->otherpostalcode, $contact->otherstate, $contact->otherstreet, $props[$contactprops["otheraddress"]], $props, $contactprops);
1738 1702
 		}
@@ -1793,8 +1757,7 @@  discard block
 block discarded – undo
1793 1757
 			$middlename = (isset($contact->middlename)) ? $contact->middlename : "";
1794 1758
 			$company = (isset($contact->companyname)) ? $contact->companyname : "";
1795 1759
 			$props[$contactprops["fileas"]] = Utils::BuildFileAs($lastname, $firstname, $middlename, $company);
1796
-		}
1797
-		else {
1760
+		} else {
1798 1761
 			SLog::Write(LOGLEVEL_DEBUG, "FILEAS_ORDER not defined");
1799 1762
 		}
1800 1763
 
@@ -1833,8 +1796,7 @@  discard block
 block discarded – undo
1833 1796
 				$props[$taskprops["completion"]] = 1.0;
1834 1797
 				$props[$taskprops["status"]] = 2;
1835 1798
 				$props[$taskprops["reminderset"]] = false;
1836
-			}
1837
-			else {
1799
+			} else {
1838 1800
 				// Set completion to 0%
1839 1801
 				// Set status to 'not started'
1840 1802
 				$props[$taskprops["completion"]] = 0.0;
@@ -1993,12 +1955,10 @@  discard block
 block discarded – undo
1993 1955
 				if (mapi_prop_type($mapiprop) != PT_BINARY && mapi_prop_type($mapiprop) != PT_MV_BINARY) {
1994 1956
 					if (is_array($message->{$asprop})) {
1995 1957
 						$value = array_map("u2wi", $message->{$asprop});
1996
-					}
1997
-					else {
1958
+					} else {
1998 1959
 						$value = u2wi($message->{$asprop});
1999 1960
 					}
2000
-				}
2001
-				else {
1961
+				} else {
2002 1962
 					$value = $message->{$asprop};
2003 1963
 				}
2004 1964
 
@@ -2033,13 +1993,11 @@  discard block
 block discarded – undo
2033 1993
 				if (is_array($value) && empty($value)) {
2034 1994
 					$propsToDelete[] = $mapiprop;
2035 1995
 					SLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->setPropsInMAPI(): Property '%s' to be deleted as it is an empty array", $asprop));
2036
-				}
2037
-				else {
1996
+				} else {
2038 1997
 					// all properties will be set at once
2039 1998
 					$propsToSet[$mapiprop] = $value;
2040 1999
 				}
2041
-			}
2042
-			elseif (in_array($asprop, $unsetVars)) {
2000
+			} elseif (in_array($asprop, $unsetVars)) {
2043 2001
 				$propsToDelete[] = $mapiprop;
2044 2002
 			}
2045 2003
 		}
@@ -2099,12 +2057,10 @@  discard block
 block discarded – undo
2099 2057
 					// Force to actual '0' or '1'
2100 2058
 					if ($messageprops[$mapiprop]) {
2101 2059
 						$message->{$asprop} = 1;
2102
-					}
2103
-					else {
2060
+					} else {
2104 2061
 						$message->{$asprop} = 0;
2105 2062
 					}
2106
-				}
2107
-				else {
2063
+				} else {
2108 2064
 					// Special handling for PR_MESSAGE_FLAGS
2109 2065
 					if ($mapiprop == PR_MESSAGE_FLAGS) {
2110 2066
 						$message->{$asprop} = $messageprops[$mapiprop] & 1;
@@ -2112,15 +2068,12 @@  discard block
 block discarded – undo
2112 2068
 					elseif ($mapiprop == PR_RTF_COMPRESSED) {
2113 2069
 						// do not send rtf to the mobile
2114 2070
 						continue;
2115
-					}
2116
-					elseif (is_array($messageprops[$mapiprop])) {
2071
+					} elseif (is_array($messageprops[$mapiprop])) {
2117 2072
 						$message->{$asprop} = array_map("w2u", $messageprops[$mapiprop]);
2118
-					}
2119
-					else {
2073
+					} else {
2120 2074
 						if (mapi_prop_type($mapiprop) != PT_BINARY && mapi_prop_type($mapiprop) != PT_MV_BINARY) {
2121 2075
 							$message->{$asprop} = w2u($messageprops[$mapiprop]);
2122
-						}
2123
-						else {
2076
+						} else {
2124 2077
 							$message->{$asprop} = $messageprops[$mapiprop];
2125 2078
 						}
2126 2079
 					}
@@ -2316,17 +2269,14 @@  discard block
 block discarded – undo
2316 2269
 			// northern hemisphere (july = dst)
2317 2270
 			if ($localtime >= $start && $localtime < $end) {
2318 2271
 				$dst = true;
2319
-			}
2320
-			else {
2272
+			} else {
2321 2273
 				$dst = false;
2322 2274
 			}
2323
-		}
2324
-		else {
2275
+		} else {
2325 2276
 			// southern hemisphere (january = dst)
2326 2277
 			if ($localtime >= $end && $localtime < $start) {
2327 2278
 				$dst = false;
2328
-			}
2329
-			else {
2279
+			} else {
2330 2280
 				$dst = true;
2331 2281
 			}
2332 2282
 		}
@@ -2372,8 +2322,7 @@  discard block
 block discarded – undo
2372 2322
 			$monthnow = gmdate("n", $date); // gmdate returns 1-12
2373 2323
 			if ($monthnow > $month) {
2374 2324
 				$date = $date - (24 * 7 * 60 * 60);
2375
-			}
2376
-			else {
2325
+			} else {
2377 2326
 				break;
2378 2327
 			}
2379 2328
 		}
@@ -2602,8 +2551,7 @@  discard block
 block discarded – undo
2602 2551
 				$recur["type"] = 10;
2603 2552
 				if (isset($message->recurrence->dayofweek)) {
2604 2553
 					$recur["subtype"] = 1;
2605
-				}
2606
-				else {
2554
+				} else {
2607 2555
 					$recur["subtype"] = 0;
2608 2556
 				}
2609 2557
 
@@ -2660,12 +2608,10 @@  discard block
 block discarded – undo
2660 2608
 		if (isset($message->recurrence->until)) {
2661 2609
 			$recur["term"] = 0x21;
2662 2610
 			$recur["end"] = $message->recurrence->until;
2663
-		}
2664
-		elseif (isset($message->recurrence->occurrences)) {
2611
+		} elseif (isset($message->recurrence->occurrences)) {
2665 2612
 			$recur["term"] = 0x22;
2666 2613
 			$recur["numoccur"] = $message->recurrence->occurrences;
2667
-		}
2668
-		else {
2614
+		} else {
2669 2615
 			$recur["term"] = 0x23;
2670 2616
 		}
2671 2617
 
@@ -2745,8 +2691,7 @@  discard block
 block discarded – undo
2745 2691
 		if ($stream) {
2746 2692
 			$stat = mapi_stream_stat($stream);
2747 2693
 			$streamsize = $stat['cb'];
2748
-		}
2749
-		else {
2694
+		} else {
2750 2695
 			$streamsize = 0;
2751 2696
 		}
2752 2697
 
@@ -2757,24 +2702,20 @@  discard block
 block discarded – undo
2757 2702
 			if ($bpReturnType == SYNC_BODYPREFERENCE_RTF) {
2758 2703
 				$body = $this->mapiReadStream($stream, $streamsize);
2759 2704
 				$message->asbody->data = StringStreamWrapper::Open(base64_encode($body));
2760
-			}
2761
-			elseif (isset($message->internetcpid) && $bpReturnType == SYNC_BODYPREFERENCE_HTML) {
2705
+			} elseif (isset($message->internetcpid) && $bpReturnType == SYNC_BODYPREFERENCE_HTML) {
2762 2706
 				// if PR_HTML is UTF-8 we can stream it directly, else we have to convert to UTF-8 & wrap it
2763 2707
 				if ($message->internetcpid == INTERNET_CPID_UTF8) {
2764 2708
 					$message->asbody->data = MAPIStreamWrapper::Open($stream, $truncateHtmlSafe);
2765
-				}
2766
-				else {
2709
+				} else {
2767 2710
 					$body = $this->mapiReadStream($stream, $streamsize);
2768 2711
 					$message->asbody->data = StringStreamWrapper::Open(Utils::ConvertCodepageStringToUtf8($message->internetcpid, $body), $truncateHtmlSafe);
2769 2712
 					$message->internetcpid = INTERNET_CPID_UTF8;
2770 2713
 				}
2771
-			}
2772
-			else {
2714
+			} else {
2773 2715
 				$message->asbody->data = MAPIStreamWrapper::Open($stream);
2774 2716
 			}
2775 2717
 			$message->asbody->estimatedDataSize = $streamsize;
2776
-		}
2777
-		else {
2718
+		} else {
2778 2719
 			$body = $this->mapiReadStream($stream, $streamsize);
2779 2720
 			$message->body = str_replace("\n", "\r\n", w2u(str_replace("\r", "", $body)));
2780 2721
 			$message->bodysize = $streamsize;
@@ -2814,8 +2755,7 @@  discard block
 block discarded – undo
2814 2755
 		if (isset($mapiEmail[PR_EC_IMAP_EMAIL]) || MAPIUtils::GetError(PR_EC_IMAP_EMAIL, $mapiEmail) == MAPI_E_NOT_ENOUGH_MEMORY) {
2815 2756
 			$stream = mapi_openproperty($mapimessage, PR_EC_IMAP_EMAIL, IID_IStream, 0, 0);
2816 2757
 			SLog::Write(LOGLEVEL_DEBUG, "MAPIProvider->imtoinet(): using PR_EC_IMAP_EMAIL as full RFC822 message");
2817
-		}
2818
-		else {
2758
+		} else {
2819 2759
 			$addrbook = $this->getAddressbook();
2820 2760
 			$stream = mapi_inetmapi_imtoinet($this->session, $addrbook, $mapimessage, ['use_tnef' => -1, 'ignore_missing_attachments' => 1]);
2821 2761
 		}
@@ -2830,8 +2770,7 @@  discard block
 block discarded – undo
2830 2770
 					$message->asbody->data = MAPIStreamWrapper::Open($stream);
2831 2771
 					$message->asbody->estimatedDataSize = $streamsize;
2832 2772
 					$message->asbody->truncated = 0;
2833
-				}
2834
-				else {
2773
+				} else {
2835 2774
 					$message->mimedata = MAPIStreamWrapper::Open($stream);
2836 2775
 					$message->mimesize = $streamsize;
2837 2776
 					$message->mimetruncated = 0;
@@ -2902,8 +2841,7 @@  discard block
 block discarded – undo
2902 2841
 			if (Request::GetProtocolVersion() >= 14.0 && $bpo->GetPreview()) {
2903 2842
 				$message->asbody->preview = Utils::Utf8_truncate(MAPIUtils::readPropStream($mapimessage, PR_BODY), $bpo->GetPreview());
2904 2843
 			}
2905
-		}
2906
-		else {
2844
+		} else {
2907 2845
 			// Override 'body' for truncation
2908 2846
 			$truncsize = Utils::GetTruncSize($contentparameters->GetTruncation());
2909 2847
 			$this->setMessageBodyForType($mapimessage, SYNC_BODYPREFERENCE_PLAIN, $message);
@@ -2968,8 +2906,7 @@  discard block
 block discarded – undo
2968 2906
 				case SYNC_BODYPREFERENCE_MIME:
2969 2907
 					break;
2970 2908
 			}
2971
-		}
2972
-		else {
2909
+		} else {
2973 2910
 			SLog::Write(LOGLEVEL_DEBUG, "MAPIProvider->setASbody either type or data are not set. Setting to empty body");
2974 2911
 			$props[$appointmentprops["body"]] = "";
2975 2912
 		}
@@ -3105,8 +3042,7 @@  discard block
 block discarded – undo
3105 3042
 						$this->specialFoldersData[] = substr($persistData, 8, $unpackedData['elDataSize']);
3106 3043
 						// Add PersistId and DataElementsSize lengths to the data size as they're not part of it
3107 3044
 						$persistData = substr($persistData, $unpackedData['dataSize'] + 4);
3108
-					}
3109
-					else {
3045
+					} else {
3110 3046
 						SLog::Write(LOGLEVEL_INFO, "MAPIProvider->getSpecialFoldersData(): persistent data is not valid");
3111 3047
 						break;
3112 3048
 					}
Please login to merge, or discard this patch.
lib/grommunio/importer.php 3 patches
Indentation   +785 added lines, -785 removed lines patch added patch discarded remove patch
@@ -18,468 +18,468 @@  discard block
 block discarded – undo
18 18
  * the PDA are always e-mail folders.
19 19
  */
20 20
 class ImportChangesICS implements IImportChanges {
21
-	private $folderid;
22
-	private $folderidHex;
23
-	private $store;
24
-	private $session;
25
-	private $flags;
26
-	private $statestream;
27
-	private $importer;
28
-	private $memChanges;
29
-	private $mapiprovider;
30
-	private $conflictsLoaded;
31
-	private $conflictsContentParameters;
32
-	private $conflictsState;
33
-	private $cutoffdate;
34
-	private $contentClass;
35
-	private $prefix;
36
-
37
-	/**
38
-	 * Constructor.
39
-	 *
40
-	 * @param mapisession $session
41
-	 * @param mapistore   $store
42
-	 * @param string      $folderid (opt)
43
-	 *
44
-	 * @throws StatusException
45
-	 */
46
-	public function __construct($session, $store, $folderid = false) {
47
-		$this->session = $session;
48
-		$this->store = $store;
49
-		$this->folderid = $folderid;
50
-		$this->folderidHex = bin2hex($folderid);
51
-		$this->conflictsLoaded = false;
52
-		$this->cutoffdate = false;
53
-		$this->contentClass = false;
54
-		$this->prefix = '';
55
-
56
-		if ($folderid) {
57
-			$entryid = mapi_msgstore_entryidfromsourcekey($store, $folderid);
58
-			$folderidForBackendId = GSync::GetDeviceManager()->GetFolderIdForBackendId($this->folderidHex);
59
-			// Only append backend id if the mapping backendid<->folderid is available.
60
-			if ($folderidForBackendId != $this->folderidHex) {
61
-				$this->prefix = $folderidForBackendId . ':';
62
-			}
63
-		}
64
-		else {
65
-			$storeprops = mapi_getprops($store, [PR_IPM_SUBTREE_ENTRYID, PR_IPM_PUBLIC_FOLDERS_ENTRYID]);
66
-			if (GSync::GetBackend()->GetImpersonatedUser() == 'system') {
67
-				$entryid = $storeprops[PR_IPM_PUBLIC_FOLDERS_ENTRYID];
68
-			}
69
-			else {
70
-				$entryid = $storeprops[PR_IPM_SUBTREE_ENTRYID];
71
-			}
72
-		}
73
-
74
-		$folder = false;
75
-		if ($entryid) {
76
-			$folder = mapi_msgstore_openentry($store, $entryid);
77
-		}
78
-
79
-		if (!$folder) {
80
-			$this->importer = false;
81
-
82
-			// We throw an general error SYNC_FSSTATUS_CODEUNKNOWN (12) which is also SYNC_STATUS_FOLDERHIERARCHYCHANGED (12)
83
-			// if this happened while doing content sync, the mobile will try to resync the folderhierarchy
84
-			throw new StatusException(sprintf("ImportChangesICS('%s','%s'): Error, unable to open folder: 0x%X", $session, bin2hex($folderid), mapi_last_hresult()), SYNC_FSSTATUS_CODEUNKNOWN);
85
-		}
86
-
87
-		$this->mapiprovider = new MAPIProvider($this->session, $this->store);
88
-
89
-		if ($folderid) {
90
-			$this->importer = mapi_openproperty($folder, PR_COLLECTOR, IID_IExchangeImportContentsChanges, 0, 0);
91
-		}
92
-		else {
93
-			$this->importer = mapi_openproperty($folder, PR_COLLECTOR, IID_IExchangeImportHierarchyChanges, 0, 0);
94
-		}
95
-	}
96
-
97
-	/**
98
-	 * Initializes the importer.
99
-	 *
100
-	 * @param string $state
101
-	 * @param int    $flags
102
-	 *
103
-	 * @throws StatusException
104
-	 *
105
-	 * @return bool
106
-	 */
107
-	public function Config($state, $flags = 0) {
108
-		$this->flags = $flags;
109
-
110
-		// this should never happen
111
-		if ($this->importer === false) {
112
-			throw new StatusException("ImportChangesICS->Config(): Error, importer not available", SYNC_FSSTATUS_CODEUNKNOWN, null, LOGLEVEL_ERROR);
113
-		}
114
-
115
-		// Put the state information in a stream that can be used by ICS
116
-		$stream = mapi_stream_create();
117
-		if (strlen($state) == 0) {
118
-			$state = hex2bin("0000000000000000");
119
-		}
120
-
121
-		SLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->Config(): initializing importer with state: 0x%s", bin2hex($state)));
122
-
123
-		mapi_stream_write($stream, $state);
124
-		$this->statestream = $stream;
125
-
126
-		if ($this->folderid !== false) {
127
-			// possible conflicting messages will be cached here
128
-			$this->memChanges = new ChangesMemoryWrapper();
129
-			$stat = mapi_importcontentschanges_config($this->importer, $stream, $flags);
130
-		}
131
-		else {
132
-			$stat = mapi_importhierarchychanges_config($this->importer, $stream, $flags);
133
-		}
134
-
135
-		if (!$stat) {
136
-			throw new StatusException(sprintf("ImportChangesICS->Config(): Error, mapi_import_*_changes_config() failed: 0x%X", mapi_last_hresult()), SYNC_FSSTATUS_CODEUNKNOWN, null, LOGLEVEL_WARN);
137
-		}
138
-
139
-		return $stat;
140
-	}
141
-
142
-	/**
143
-	 * Configures additional parameters for content selection.
144
-	 *
145
-	 * @param ContentParameters $contentparameters
146
-	 *
147
-	 * @throws StatusException
148
-	 *
149
-	 * @return bool
150
-	 */
151
-	public function ConfigContentParameters($contentparameters) {
152
-		$filtertype = $contentparameters->GetFilterType();
153
-
154
-		switch ($contentparameters->GetContentClass()) {
155
-			case "Email":
156
-				$this->cutoffdate = ($filtertype) ? Utils::GetCutOffDate($filtertype) : false;
157
-				break;
158
-
159
-			case "Calendar":
160
-				$this->cutoffdate = ($filtertype) ? Utils::GetCutOffDate($filtertype) : false;
161
-				break;
162
-
163
-			default:
164
-			case "Contacts":
165
-			case "Tasks":
166
-				$this->cutoffdate = false;
167
-				break;
168
-		}
169
-		$this->contentClass = $contentparameters->GetContentClass();
170
-
171
-		return true;
172
-	}
173
-
174
-	/**
175
-	 * Reads state from the Importer.
176
-	 *
177
-	 * @throws StatusException
178
-	 *
179
-	 * @return string
180
-	 */
181
-	public function GetState() {
182
-		$error = false;
183
-		if (!isset($this->statestream) || $this->importer === false) {
184
-			$error = true;
185
-		}
186
-
187
-		if ($error === false && $this->folderid !== false && function_exists("mapi_importcontentschanges_updatestate")) {
188
-			if (mapi_importcontentschanges_updatestate($this->importer, $this->statestream) != true) {
189
-				$error = true;
190
-			}
191
-		}
192
-
193
-		if ($error == true) {
194
-			throw new StatusException(sprintf("ImportChangesICS->GetState(): Error, state not available or unable to update: 0x%X", mapi_last_hresult()), (($this->folderid) ? SYNC_STATUS_FOLDERHIERARCHYCHANGED : SYNC_FSSTATUS_CODEUNKNOWN), null, LOGLEVEL_WARN);
195
-		}
196
-
197
-		mapi_stream_seek($this->statestream, 0, STREAM_SEEK_SET);
198
-
199
-		$state = "";
200
-		while (true) {
201
-			$data = mapi_stream_read($this->statestream, 4096);
202
-			if (strlen($data)) {
203
-				$state .= $data;
204
-			}
205
-			else {
206
-				break;
207
-			}
208
-		}
209
-
210
-		return $state;
211
-	}
212
-
213
-	/**
214
-	 * Checks if a message may be modified. This involves checking:
215
-	 * - if there is a synchronization interval and if so, if the message is in it (sync window).
216
-	 *   These checks only apply to Emails and Appointments only, Contacts, Tasks and Notes do not have time restrictions.
217
-	 * - if the message is not marked as private in a shared folder.
218
-	 *
219
-	 * @param string $messageid the message id to be checked
220
-	 *
221
-	 * @return bool
222
-	 */
223
-	private function isModificationAllowed($messageid) {
224
-		$sharedUser = GSync::GetAdditionalSyncFolderStore(bin2hex($this->folderid));
225
-		// if this is either a user folder or SYSTEM and no restriction is set, we don't need to check
226
-		if (($sharedUser == false || $sharedUser == 'SYSTEM') && $this->cutoffdate === false && !GSync::GetBackend()->GetImpersonatedUser()) {
227
-			return true;
228
-		}
229
-
230
-		// open the existing object
231
-		$entryid = mapi_msgstore_entryidfromsourcekey($this->store, $this->folderid, hex2bin($messageid));
232
-		if (!$entryid) {
233
-			SLog::Write(LOGLEVEL_WARN, sprintf("ImportChangesICS->isModificationAllowed('%s'): Error, unable to resolve message id: 0x%X", $messageid, mapi_last_hresult()));
234
-
235
-			return false;
236
-		}
237
-
238
-		$mapimessage = mapi_msgstore_openentry($this->store, $entryid);
239
-		if (!$mapimessage) {
240
-			SLog::Write(LOGLEVEL_WARN, sprintf("ImportChangesICS->isModificationAllowed('%s'): Error, unable to open entry id: 0x%X", $messageid, mapi_last_hresult()));
241
-
242
-			return false;
243
-		}
244
-
245
-		// check the sync interval
246
-		if ($this->cutoffdate !== false) {
247
-			SLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->isModificationAllowed('%s'): cut off date is: %s", $messageid, $this->cutoffdate));
248
-			if (($this->contentClass == "Email" && !MAPIUtils::IsInEmailSyncInterval($this->store, $mapimessage, $this->cutoffdate)) ||
249
-				  ($this->contentClass == "Calendar" && !MAPIUtils::IsInCalendarSyncInterval($this->store, $mapimessage, $this->cutoffdate))) {
250
-				SLog::Write(LOGLEVEL_WARN, sprintf("ImportChangesICS->isModificationAllowed('%s'): Message in %s is outside the sync interval. Data not saved.", $messageid, $this->contentClass));
251
-
252
-				return false;
253
-			}
254
-		}
255
-
256
-		// check if not private
257
-		if (MAPIUtils::IsMessageSharedAndPrivate($this->folderid, $mapimessage)) {
258
-			SLog::Write(LOGLEVEL_WARN, sprintf("ImportChangesICS->isModificationAllowed('%s'): Message is shared and marked as private. Data not saved.", $messageid));
259
-
260
-			return false;
261
-		}
262
-
263
-		// yes, modification allowed
264
-		return true;
265
-	}
266
-
267
-	/*----------------------------------------------------------------------------------------------------------
21
+    private $folderid;
22
+    private $folderidHex;
23
+    private $store;
24
+    private $session;
25
+    private $flags;
26
+    private $statestream;
27
+    private $importer;
28
+    private $memChanges;
29
+    private $mapiprovider;
30
+    private $conflictsLoaded;
31
+    private $conflictsContentParameters;
32
+    private $conflictsState;
33
+    private $cutoffdate;
34
+    private $contentClass;
35
+    private $prefix;
36
+
37
+    /**
38
+     * Constructor.
39
+     *
40
+     * @param mapisession $session
41
+     * @param mapistore   $store
42
+     * @param string      $folderid (opt)
43
+     *
44
+     * @throws StatusException
45
+     */
46
+    public function __construct($session, $store, $folderid = false) {
47
+        $this->session = $session;
48
+        $this->store = $store;
49
+        $this->folderid = $folderid;
50
+        $this->folderidHex = bin2hex($folderid);
51
+        $this->conflictsLoaded = false;
52
+        $this->cutoffdate = false;
53
+        $this->contentClass = false;
54
+        $this->prefix = '';
55
+
56
+        if ($folderid) {
57
+            $entryid = mapi_msgstore_entryidfromsourcekey($store, $folderid);
58
+            $folderidForBackendId = GSync::GetDeviceManager()->GetFolderIdForBackendId($this->folderidHex);
59
+            // Only append backend id if the mapping backendid<->folderid is available.
60
+            if ($folderidForBackendId != $this->folderidHex) {
61
+                $this->prefix = $folderidForBackendId . ':';
62
+            }
63
+        }
64
+        else {
65
+            $storeprops = mapi_getprops($store, [PR_IPM_SUBTREE_ENTRYID, PR_IPM_PUBLIC_FOLDERS_ENTRYID]);
66
+            if (GSync::GetBackend()->GetImpersonatedUser() == 'system') {
67
+                $entryid = $storeprops[PR_IPM_PUBLIC_FOLDERS_ENTRYID];
68
+            }
69
+            else {
70
+                $entryid = $storeprops[PR_IPM_SUBTREE_ENTRYID];
71
+            }
72
+        }
73
+
74
+        $folder = false;
75
+        if ($entryid) {
76
+            $folder = mapi_msgstore_openentry($store, $entryid);
77
+        }
78
+
79
+        if (!$folder) {
80
+            $this->importer = false;
81
+
82
+            // We throw an general error SYNC_FSSTATUS_CODEUNKNOWN (12) which is also SYNC_STATUS_FOLDERHIERARCHYCHANGED (12)
83
+            // if this happened while doing content sync, the mobile will try to resync the folderhierarchy
84
+            throw new StatusException(sprintf("ImportChangesICS('%s','%s'): Error, unable to open folder: 0x%X", $session, bin2hex($folderid), mapi_last_hresult()), SYNC_FSSTATUS_CODEUNKNOWN);
85
+        }
86
+
87
+        $this->mapiprovider = new MAPIProvider($this->session, $this->store);
88
+
89
+        if ($folderid) {
90
+            $this->importer = mapi_openproperty($folder, PR_COLLECTOR, IID_IExchangeImportContentsChanges, 0, 0);
91
+        }
92
+        else {
93
+            $this->importer = mapi_openproperty($folder, PR_COLLECTOR, IID_IExchangeImportHierarchyChanges, 0, 0);
94
+        }
95
+    }
96
+
97
+    /**
98
+     * Initializes the importer.
99
+     *
100
+     * @param string $state
101
+     * @param int    $flags
102
+     *
103
+     * @throws StatusException
104
+     *
105
+     * @return bool
106
+     */
107
+    public function Config($state, $flags = 0) {
108
+        $this->flags = $flags;
109
+
110
+        // this should never happen
111
+        if ($this->importer === false) {
112
+            throw new StatusException("ImportChangesICS->Config(): Error, importer not available", SYNC_FSSTATUS_CODEUNKNOWN, null, LOGLEVEL_ERROR);
113
+        }
114
+
115
+        // Put the state information in a stream that can be used by ICS
116
+        $stream = mapi_stream_create();
117
+        if (strlen($state) == 0) {
118
+            $state = hex2bin("0000000000000000");
119
+        }
120
+
121
+        SLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->Config(): initializing importer with state: 0x%s", bin2hex($state)));
122
+
123
+        mapi_stream_write($stream, $state);
124
+        $this->statestream = $stream;
125
+
126
+        if ($this->folderid !== false) {
127
+            // possible conflicting messages will be cached here
128
+            $this->memChanges = new ChangesMemoryWrapper();
129
+            $stat = mapi_importcontentschanges_config($this->importer, $stream, $flags);
130
+        }
131
+        else {
132
+            $stat = mapi_importhierarchychanges_config($this->importer, $stream, $flags);
133
+        }
134
+
135
+        if (!$stat) {
136
+            throw new StatusException(sprintf("ImportChangesICS->Config(): Error, mapi_import_*_changes_config() failed: 0x%X", mapi_last_hresult()), SYNC_FSSTATUS_CODEUNKNOWN, null, LOGLEVEL_WARN);
137
+        }
138
+
139
+        return $stat;
140
+    }
141
+
142
+    /**
143
+     * Configures additional parameters for content selection.
144
+     *
145
+     * @param ContentParameters $contentparameters
146
+     *
147
+     * @throws StatusException
148
+     *
149
+     * @return bool
150
+     */
151
+    public function ConfigContentParameters($contentparameters) {
152
+        $filtertype = $contentparameters->GetFilterType();
153
+
154
+        switch ($contentparameters->GetContentClass()) {
155
+            case "Email":
156
+                $this->cutoffdate = ($filtertype) ? Utils::GetCutOffDate($filtertype) : false;
157
+                break;
158
+
159
+            case "Calendar":
160
+                $this->cutoffdate = ($filtertype) ? Utils::GetCutOffDate($filtertype) : false;
161
+                break;
162
+
163
+            default:
164
+            case "Contacts":
165
+            case "Tasks":
166
+                $this->cutoffdate = false;
167
+                break;
168
+        }
169
+        $this->contentClass = $contentparameters->GetContentClass();
170
+
171
+        return true;
172
+    }
173
+
174
+    /**
175
+     * Reads state from the Importer.
176
+     *
177
+     * @throws StatusException
178
+     *
179
+     * @return string
180
+     */
181
+    public function GetState() {
182
+        $error = false;
183
+        if (!isset($this->statestream) || $this->importer === false) {
184
+            $error = true;
185
+        }
186
+
187
+        if ($error === false && $this->folderid !== false && function_exists("mapi_importcontentschanges_updatestate")) {
188
+            if (mapi_importcontentschanges_updatestate($this->importer, $this->statestream) != true) {
189
+                $error = true;
190
+            }
191
+        }
192
+
193
+        if ($error == true) {
194
+            throw new StatusException(sprintf("ImportChangesICS->GetState(): Error, state not available or unable to update: 0x%X", mapi_last_hresult()), (($this->folderid) ? SYNC_STATUS_FOLDERHIERARCHYCHANGED : SYNC_FSSTATUS_CODEUNKNOWN), null, LOGLEVEL_WARN);
195
+        }
196
+
197
+        mapi_stream_seek($this->statestream, 0, STREAM_SEEK_SET);
198
+
199
+        $state = "";
200
+        while (true) {
201
+            $data = mapi_stream_read($this->statestream, 4096);
202
+            if (strlen($data)) {
203
+                $state .= $data;
204
+            }
205
+            else {
206
+                break;
207
+            }
208
+        }
209
+
210
+        return $state;
211
+    }
212
+
213
+    /**
214
+     * Checks if a message may be modified. This involves checking:
215
+     * - if there is a synchronization interval and if so, if the message is in it (sync window).
216
+     *   These checks only apply to Emails and Appointments only, Contacts, Tasks and Notes do not have time restrictions.
217
+     * - if the message is not marked as private in a shared folder.
218
+     *
219
+     * @param string $messageid the message id to be checked
220
+     *
221
+     * @return bool
222
+     */
223
+    private function isModificationAllowed($messageid) {
224
+        $sharedUser = GSync::GetAdditionalSyncFolderStore(bin2hex($this->folderid));
225
+        // if this is either a user folder or SYSTEM and no restriction is set, we don't need to check
226
+        if (($sharedUser == false || $sharedUser == 'SYSTEM') && $this->cutoffdate === false && !GSync::GetBackend()->GetImpersonatedUser()) {
227
+            return true;
228
+        }
229
+
230
+        // open the existing object
231
+        $entryid = mapi_msgstore_entryidfromsourcekey($this->store, $this->folderid, hex2bin($messageid));
232
+        if (!$entryid) {
233
+            SLog::Write(LOGLEVEL_WARN, sprintf("ImportChangesICS->isModificationAllowed('%s'): Error, unable to resolve message id: 0x%X", $messageid, mapi_last_hresult()));
234
+
235
+            return false;
236
+        }
237
+
238
+        $mapimessage = mapi_msgstore_openentry($this->store, $entryid);
239
+        if (!$mapimessage) {
240
+            SLog::Write(LOGLEVEL_WARN, sprintf("ImportChangesICS->isModificationAllowed('%s'): Error, unable to open entry id: 0x%X", $messageid, mapi_last_hresult()));
241
+
242
+            return false;
243
+        }
244
+
245
+        // check the sync interval
246
+        if ($this->cutoffdate !== false) {
247
+            SLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->isModificationAllowed('%s'): cut off date is: %s", $messageid, $this->cutoffdate));
248
+            if (($this->contentClass == "Email" && !MAPIUtils::IsInEmailSyncInterval($this->store, $mapimessage, $this->cutoffdate)) ||
249
+                  ($this->contentClass == "Calendar" && !MAPIUtils::IsInCalendarSyncInterval($this->store, $mapimessage, $this->cutoffdate))) {
250
+                SLog::Write(LOGLEVEL_WARN, sprintf("ImportChangesICS->isModificationAllowed('%s'): Message in %s is outside the sync interval. Data not saved.", $messageid, $this->contentClass));
251
+
252
+                return false;
253
+            }
254
+        }
255
+
256
+        // check if not private
257
+        if (MAPIUtils::IsMessageSharedAndPrivate($this->folderid, $mapimessage)) {
258
+            SLog::Write(LOGLEVEL_WARN, sprintf("ImportChangesICS->isModificationAllowed('%s'): Message is shared and marked as private. Data not saved.", $messageid));
259
+
260
+            return false;
261
+        }
262
+
263
+        // yes, modification allowed
264
+        return true;
265
+    }
266
+
267
+    /*----------------------------------------------------------------------------------------------------------
268 268
 	 * Methods for ContentsExporter
269 269
 	 */
270 270
 
271
-	/**
272
-	 * Loads objects which are expected to be exported with the state
273
-	 * Before importing/saving the actual message from the mobile, a conflict detection should be done.
274
-	 *
275
-	 * @param ContentParameters $contentparameters class of objects
276
-	 * @param string            $state
277
-	 *
278
-	 * @throws StatusException
279
-	 *
280
-	 * @return bool
281
-	 */
282
-	public function LoadConflicts($contentparameters, $state) {
283
-		if (!isset($this->session) || !isset($this->store) || !isset($this->folderid)) {
284
-			throw new StatusException("ImportChangesICS->LoadConflicts(): Error, can not load changes for conflict detection. Session, store or folder information not available", SYNC_STATUS_SERVERERROR);
285
-		}
286
-
287
-		// save data to load changes later if necessary
288
-		$this->conflictsLoaded = false;
289
-		$this->conflictsContentParameters = $contentparameters;
290
-		$this->conflictsState = $state;
291
-
292
-		SLog::Write(LOGLEVEL_DEBUG, "ImportChangesICS->LoadConflicts(): will be loaded later if necessary");
293
-
294
-		return true;
295
-	}
296
-
297
-	/**
298
-	 * Potential conflicts are only loaded when really necessary,
299
-	 * e.g. on ADD or MODIFY.
300
-	 *
301
-	 * @return bool
302
-	 */
303
-	private function lazyLoadConflicts() {
304
-		if (!isset($this->session) || !isset($this->store) || !isset($this->folderid) ||
305
-			!isset($this->conflictsContentParameters) || $this->conflictsState === false) {
306
-			SLog::Write(LOGLEVEL_WARN, "ImportChangesICS->lazyLoadConflicts(): can not load potential conflicting changes in lazymode for conflict detection. Missing information");
307
-
308
-			return false;
309
-		}
310
-
311
-		if (!$this->conflictsLoaded) {
312
-			SLog::Write(LOGLEVEL_DEBUG, "ImportChangesICS->lazyLoadConflicts(): loading..");
313
-
314
-			// configure an exporter so we can detect conflicts
315
-			$exporter = new ExportChangesICS($this->session, $this->store, $this->folderid);
316
-			$exporter->Config($this->conflictsState);
317
-			$exporter->ConfigContentParameters($this->conflictsContentParameters);
318
-			$exporter->InitializeExporter($this->memChanges);
319
-
320
-			// monitor how long it takes to export potential conflicts
321
-			// if this takes "too long" we cancel this operation!
322
-			$potConflicts = $exporter->GetChangeCount();
323
-			if ($potConflicts > 100) {
324
-				SLog::Write(LOGLEVEL_WARN, sprintf("ImportChangesICS->lazyLoadConflicts(): conflict detection abandoned as there are too many (%d) changes to be exported.", $potConflicts));
325
-				$this->conflictsLoaded = true;
326
-
327
-				return false;
328
-			}
329
-			$started = time();
330
-			$exported = 0;
331
-
332
-			try {
333
-				while (is_array($exporter->Synchronize())) {
334
-					++$exported;
335
-
336
-					// stop if this takes more than 15 seconds and there are more than 5 changes still to be exported
337
-					// within 20 seconds this should be finished or it will not be performed
338
-					if ((time() - $started) > 15 && ($potConflicts - $exported) > 5) {
339
-						SLog::Write(LOGLEVEL_WARN, sprintf("ImportChangesICS->lazyLoadConflicts(): conflict detection cancelled as operation is too slow. In %d seconds only %d from %d changes were processed.", (time() - $started), $exported, $potConflicts));
340
-						$this->conflictsLoaded = true;
341
-
342
-						return false;
343
-					}
344
-				}
345
-			}
346
-			// something really bad happened while exporting changes
347
-			catch (StatusException $stex) {
348
-				SLog::Write(LOGLEVEL_WARN, sprintf("ImportChangesICS->lazyLoadConflicts(): got StatusException code %d while exporting changes. Ignore and mark conflicts as loaded.", $stex->getCode()));
349
-			}
350
-			$this->conflictsLoaded = true;
351
-		}
352
-
353
-		return true;
354
-	}
355
-
356
-	/**
357
-	 * Imports a single message.
358
-	 *
359
-	 * @param string     $id
360
-	 * @param SyncObject $message
361
-	 *
362
-	 * @throws StatusException
363
-	 *
364
-	 * @return boolean/string - failure / id of message
365
-	 */
366
-	public function ImportMessageChange($id, $message) {
367
-		$flags = 0;
368
-		$props = [];
369
-		$props[PR_PARENT_SOURCE_KEY] = $this->folderid;
370
-
371
-		// set the PR_SOURCE_KEY if available or mark it as new message
372
-		if ($id) {
373
-			list(, $sk) = Utils::SplitMessageId($id);
374
-			$props[PR_SOURCE_KEY] = hex2bin($sk);
375
-
376
-			// check if message is in the synchronization interval and/or shared+private
377
-			if (!$this->isModificationAllowed($sk)) {
378
-				throw new StatusException(sprintf("ImportChangesICS->ImportMessageChange('%s','%s'): Message modification is not allowed. Data not saved.", $id, get_class($message)), SYNC_STATUS_SYNCCANNOTBECOMPLETED);
379
-			}
380
-
381
-			// check for conflicts
382
-			$this->lazyLoadConflicts();
383
-			if ($this->memChanges->IsChanged($id)) {
384
-				if ($this->flags & SYNC_CONFLICT_OVERWRITE_PIM) {
385
-					// in these cases the status SYNC_STATUS_CONFLICTCLIENTSERVEROBJECT should be returned, so the mobile client can inform the end user
386
-					throw new StatusException(sprintf("ImportChangesICS->ImportMessageChange('%s','%s'): Conflict detected. Data from PIM will be dropped! Server overwrites PIM. User is informed.", $id, get_class($message)), SYNC_STATUS_CONFLICTCLIENTSERVEROBJECT, null, LOGLEVEL_INFO);
387
-
388
-					return false;
389
-				}
390
-
391
-				SLog::Write(LOGLEVEL_INFO, sprintf("ImportChangesICS->ImportMessageChange('%s','%s'): Conflict detected. Data from Server will be dropped! PIM overwrites server.", $id, get_class($message)));
392
-			}
393
-			if ($this->memChanges->IsDeleted($id)) {
394
-				SLog::Write(LOGLEVEL_INFO, sprintf("ImportChangesICS->ImportMessageChange('%s','%s'): Conflict detected. Data from PIM will be dropped! Object was deleted on server.", $id, get_class($message)));
395
-
396
-				return false;
397
-			}
398
-		}
399
-		else {
400
-			$flags = SYNC_NEW_MESSAGE;
401
-		}
402
-
403
-		if (mapi_importcontentschanges_importmessagechange($this->importer, $props, $flags, $mapimessage)) {
404
-			$this->mapiprovider->SetMessage($mapimessage, $message);
405
-			mapi_savechanges($mapimessage);
406
-
407
-			if (mapi_last_hresult()) {
408
-				throw new StatusException(sprintf("ImportChangesICS->ImportMessageChange('%s','%s'): Error, mapi_savechanges() failed: 0x%X", $id, get_class($message), mapi_last_hresult()), SYNC_STATUS_SYNCCANNOTBECOMPLETED);
409
-			}
410
-
411
-			$sourcekeyprops = mapi_getprops($mapimessage, [PR_SOURCE_KEY]);
412
-
413
-			return $this->prefix . bin2hex($sourcekeyprops[PR_SOURCE_KEY]);
414
-		}
415
-
416
-		throw new StatusException(sprintf("ImportChangesICS->ImportMessageChange('%s','%s'): Error updating object: 0x%X", $id, get_class($message), mapi_last_hresult()), SYNC_STATUS_OBJECTNOTFOUND);
417
-	}
418
-
419
-	/**
420
-	 * Imports a deletion. This may conflict if the local object has been modified.
421
-	 *
422
-	 * @param string $id
423
-	 * @param bool   $asSoftDelete (opt) if true, the deletion is exported as "SoftDelete", else as "Remove" - default: false
424
-	 *
425
-	 * @return bool
426
-	 */
427
-	public function ImportMessageDeletion($id, $asSoftDelete = false) {
428
-		list(, $sk) = Utils::SplitMessageId($id);
429
-
430
-		// check if message is in the synchronization interval and/or shared+private
431
-		if (!$this->isModificationAllowed($sk)) {
432
-			throw new StatusException(sprintf("ImportChangesICS->ImportMessageDeletion('%s'): Message deletion is not allowed. Deletion not executed.", $id), SYNC_STATUS_OBJECTNOTFOUND);
433
-		}
434
-
435
-		// check for conflicts
436
-		$this->lazyLoadConflicts();
437
-		if ($this->memChanges->IsChanged($id)) {
438
-			SLog::Write(LOGLEVEL_INFO, sprintf("ImportChangesICS->ImportMessageDeletion('%s'): Conflict detected. Data from Server will be dropped! PIM deleted object.", $id));
439
-		}
440
-		elseif ($this->memChanges->IsDeleted($id)) {
441
-			SLog::Write(LOGLEVEL_INFO, sprintf("ImportChangesICS->ImportMessageDeletion('%s'): Conflict detected. Data is already deleted. Request will be ignored.", $id));
442
-
443
-			return true;
444
-		}
445
-
446
-		// do a 'soft' delete so people can un-delete if necessary
447
-		if (mapi_importcontentschanges_importmessagedeletion($this->importer, 1, [hex2bin($sk)])) {
448
-			throw new StatusException(sprintf("ImportChangesICS->ImportMessageDeletion('%s'): Error updating object: 0x%X", $sk, mapi_last_hresult()), SYNC_STATUS_OBJECTNOTFOUND);
449
-		}
450
-
451
-		return true;
452
-	}
453
-
454
-	/**
455
-	 * Imports a change in 'read' flag
456
-	 * This can never conflict.
457
-	 *
458
-	 * @param string $id
459
-	 * @param int    $flags      - read/unread
460
-	 * @param array  $categories
461
-	 *
462
-	 * @throws StatusException
463
-	 *
464
-	 * @return bool
465
-	 */
466
-	public function ImportMessageReadFlag($id, $flags, $categories = []) {
467
-		list($fsk, $sk) = Utils::SplitMessageId($id);
468
-
469
-		// if $fsk is set, we convert it into a backend id.
470
-		if ($fsk) {
471
-			$fsk = GSync::GetDeviceManager()->GetBackendIdForFolderId($fsk);
472
-		}
473
-
474
-		// read flag change for our current folder
475
-		if ($this->folderidHex == $fsk || empty($fsk)) {
476
-			// check if it is in the synchronization interval and/or shared+private
477
-			if (!$this->isModificationAllowed($sk)) {
478
-				throw new StatusException(sprintf("ImportChangesICS->ImportMessageReadFlag('%s','%d'): Flag update is not allowed. Flags not updated.", $id, $flags), SYNC_STATUS_OBJECTNOTFOUND);
479
-			}
480
-
481
-			// check for conflicts
482
-			/*
271
+    /**
272
+     * Loads objects which are expected to be exported with the state
273
+     * Before importing/saving the actual message from the mobile, a conflict detection should be done.
274
+     *
275
+     * @param ContentParameters $contentparameters class of objects
276
+     * @param string            $state
277
+     *
278
+     * @throws StatusException
279
+     *
280
+     * @return bool
281
+     */
282
+    public function LoadConflicts($contentparameters, $state) {
283
+        if (!isset($this->session) || !isset($this->store) || !isset($this->folderid)) {
284
+            throw new StatusException("ImportChangesICS->LoadConflicts(): Error, can not load changes for conflict detection. Session, store or folder information not available", SYNC_STATUS_SERVERERROR);
285
+        }
286
+
287
+        // save data to load changes later if necessary
288
+        $this->conflictsLoaded = false;
289
+        $this->conflictsContentParameters = $contentparameters;
290
+        $this->conflictsState = $state;
291
+
292
+        SLog::Write(LOGLEVEL_DEBUG, "ImportChangesICS->LoadConflicts(): will be loaded later if necessary");
293
+
294
+        return true;
295
+    }
296
+
297
+    /**
298
+     * Potential conflicts are only loaded when really necessary,
299
+     * e.g. on ADD or MODIFY.
300
+     *
301
+     * @return bool
302
+     */
303
+    private function lazyLoadConflicts() {
304
+        if (!isset($this->session) || !isset($this->store) || !isset($this->folderid) ||
305
+            !isset($this->conflictsContentParameters) || $this->conflictsState === false) {
306
+            SLog::Write(LOGLEVEL_WARN, "ImportChangesICS->lazyLoadConflicts(): can not load potential conflicting changes in lazymode for conflict detection. Missing information");
307
+
308
+            return false;
309
+        }
310
+
311
+        if (!$this->conflictsLoaded) {
312
+            SLog::Write(LOGLEVEL_DEBUG, "ImportChangesICS->lazyLoadConflicts(): loading..");
313
+
314
+            // configure an exporter so we can detect conflicts
315
+            $exporter = new ExportChangesICS($this->session, $this->store, $this->folderid);
316
+            $exporter->Config($this->conflictsState);
317
+            $exporter->ConfigContentParameters($this->conflictsContentParameters);
318
+            $exporter->InitializeExporter($this->memChanges);
319
+
320
+            // monitor how long it takes to export potential conflicts
321
+            // if this takes "too long" we cancel this operation!
322
+            $potConflicts = $exporter->GetChangeCount();
323
+            if ($potConflicts > 100) {
324
+                SLog::Write(LOGLEVEL_WARN, sprintf("ImportChangesICS->lazyLoadConflicts(): conflict detection abandoned as there are too many (%d) changes to be exported.", $potConflicts));
325
+                $this->conflictsLoaded = true;
326
+
327
+                return false;
328
+            }
329
+            $started = time();
330
+            $exported = 0;
331
+
332
+            try {
333
+                while (is_array($exporter->Synchronize())) {
334
+                    ++$exported;
335
+
336
+                    // stop if this takes more than 15 seconds and there are more than 5 changes still to be exported
337
+                    // within 20 seconds this should be finished or it will not be performed
338
+                    if ((time() - $started) > 15 && ($potConflicts - $exported) > 5) {
339
+                        SLog::Write(LOGLEVEL_WARN, sprintf("ImportChangesICS->lazyLoadConflicts(): conflict detection cancelled as operation is too slow. In %d seconds only %d from %d changes were processed.", (time() - $started), $exported, $potConflicts));
340
+                        $this->conflictsLoaded = true;
341
+
342
+                        return false;
343
+                    }
344
+                }
345
+            }
346
+            // something really bad happened while exporting changes
347
+            catch (StatusException $stex) {
348
+                SLog::Write(LOGLEVEL_WARN, sprintf("ImportChangesICS->lazyLoadConflicts(): got StatusException code %d while exporting changes. Ignore and mark conflicts as loaded.", $stex->getCode()));
349
+            }
350
+            $this->conflictsLoaded = true;
351
+        }
352
+
353
+        return true;
354
+    }
355
+
356
+    /**
357
+     * Imports a single message.
358
+     *
359
+     * @param string     $id
360
+     * @param SyncObject $message
361
+     *
362
+     * @throws StatusException
363
+     *
364
+     * @return boolean/string - failure / id of message
365
+     */
366
+    public function ImportMessageChange($id, $message) {
367
+        $flags = 0;
368
+        $props = [];
369
+        $props[PR_PARENT_SOURCE_KEY] = $this->folderid;
370
+
371
+        // set the PR_SOURCE_KEY if available or mark it as new message
372
+        if ($id) {
373
+            list(, $sk) = Utils::SplitMessageId($id);
374
+            $props[PR_SOURCE_KEY] = hex2bin($sk);
375
+
376
+            // check if message is in the synchronization interval and/or shared+private
377
+            if (!$this->isModificationAllowed($sk)) {
378
+                throw new StatusException(sprintf("ImportChangesICS->ImportMessageChange('%s','%s'): Message modification is not allowed. Data not saved.", $id, get_class($message)), SYNC_STATUS_SYNCCANNOTBECOMPLETED);
379
+            }
380
+
381
+            // check for conflicts
382
+            $this->lazyLoadConflicts();
383
+            if ($this->memChanges->IsChanged($id)) {
384
+                if ($this->flags & SYNC_CONFLICT_OVERWRITE_PIM) {
385
+                    // in these cases the status SYNC_STATUS_CONFLICTCLIENTSERVEROBJECT should be returned, so the mobile client can inform the end user
386
+                    throw new StatusException(sprintf("ImportChangesICS->ImportMessageChange('%s','%s'): Conflict detected. Data from PIM will be dropped! Server overwrites PIM. User is informed.", $id, get_class($message)), SYNC_STATUS_CONFLICTCLIENTSERVEROBJECT, null, LOGLEVEL_INFO);
387
+
388
+                    return false;
389
+                }
390
+
391
+                SLog::Write(LOGLEVEL_INFO, sprintf("ImportChangesICS->ImportMessageChange('%s','%s'): Conflict detected. Data from Server will be dropped! PIM overwrites server.", $id, get_class($message)));
392
+            }
393
+            if ($this->memChanges->IsDeleted($id)) {
394
+                SLog::Write(LOGLEVEL_INFO, sprintf("ImportChangesICS->ImportMessageChange('%s','%s'): Conflict detected. Data from PIM will be dropped! Object was deleted on server.", $id, get_class($message)));
395
+
396
+                return false;
397
+            }
398
+        }
399
+        else {
400
+            $flags = SYNC_NEW_MESSAGE;
401
+        }
402
+
403
+        if (mapi_importcontentschanges_importmessagechange($this->importer, $props, $flags, $mapimessage)) {
404
+            $this->mapiprovider->SetMessage($mapimessage, $message);
405
+            mapi_savechanges($mapimessage);
406
+
407
+            if (mapi_last_hresult()) {
408
+                throw new StatusException(sprintf("ImportChangesICS->ImportMessageChange('%s','%s'): Error, mapi_savechanges() failed: 0x%X", $id, get_class($message), mapi_last_hresult()), SYNC_STATUS_SYNCCANNOTBECOMPLETED);
409
+            }
410
+
411
+            $sourcekeyprops = mapi_getprops($mapimessage, [PR_SOURCE_KEY]);
412
+
413
+            return $this->prefix . bin2hex($sourcekeyprops[PR_SOURCE_KEY]);
414
+        }
415
+
416
+        throw new StatusException(sprintf("ImportChangesICS->ImportMessageChange('%s','%s'): Error updating object: 0x%X", $id, get_class($message), mapi_last_hresult()), SYNC_STATUS_OBJECTNOTFOUND);
417
+    }
418
+
419
+    /**
420
+     * Imports a deletion. This may conflict if the local object has been modified.
421
+     *
422
+     * @param string $id
423
+     * @param bool   $asSoftDelete (opt) if true, the deletion is exported as "SoftDelete", else as "Remove" - default: false
424
+     *
425
+     * @return bool
426
+     */
427
+    public function ImportMessageDeletion($id, $asSoftDelete = false) {
428
+        list(, $sk) = Utils::SplitMessageId($id);
429
+
430
+        // check if message is in the synchronization interval and/or shared+private
431
+        if (!$this->isModificationAllowed($sk)) {
432
+            throw new StatusException(sprintf("ImportChangesICS->ImportMessageDeletion('%s'): Message deletion is not allowed. Deletion not executed.", $id), SYNC_STATUS_OBJECTNOTFOUND);
433
+        }
434
+
435
+        // check for conflicts
436
+        $this->lazyLoadConflicts();
437
+        if ($this->memChanges->IsChanged($id)) {
438
+            SLog::Write(LOGLEVEL_INFO, sprintf("ImportChangesICS->ImportMessageDeletion('%s'): Conflict detected. Data from Server will be dropped! PIM deleted object.", $id));
439
+        }
440
+        elseif ($this->memChanges->IsDeleted($id)) {
441
+            SLog::Write(LOGLEVEL_INFO, sprintf("ImportChangesICS->ImportMessageDeletion('%s'): Conflict detected. Data is already deleted. Request will be ignored.", $id));
442
+
443
+            return true;
444
+        }
445
+
446
+        // do a 'soft' delete so people can un-delete if necessary
447
+        if (mapi_importcontentschanges_importmessagedeletion($this->importer, 1, [hex2bin($sk)])) {
448
+            throw new StatusException(sprintf("ImportChangesICS->ImportMessageDeletion('%s'): Error updating object: 0x%X", $sk, mapi_last_hresult()), SYNC_STATUS_OBJECTNOTFOUND);
449
+        }
450
+
451
+        return true;
452
+    }
453
+
454
+    /**
455
+     * Imports a change in 'read' flag
456
+     * This can never conflict.
457
+     *
458
+     * @param string $id
459
+     * @param int    $flags      - read/unread
460
+     * @param array  $categories
461
+     *
462
+     * @throws StatusException
463
+     *
464
+     * @return bool
465
+     */
466
+    public function ImportMessageReadFlag($id, $flags, $categories = []) {
467
+        list($fsk, $sk) = Utils::SplitMessageId($id);
468
+
469
+        // if $fsk is set, we convert it into a backend id.
470
+        if ($fsk) {
471
+            $fsk = GSync::GetDeviceManager()->GetBackendIdForFolderId($fsk);
472
+        }
473
+
474
+        // read flag change for our current folder
475
+        if ($this->folderidHex == $fsk || empty($fsk)) {
476
+            // check if it is in the synchronization interval and/or shared+private
477
+            if (!$this->isModificationAllowed($sk)) {
478
+                throw new StatusException(sprintf("ImportChangesICS->ImportMessageReadFlag('%s','%d'): Flag update is not allowed. Flags not updated.", $id, $flags), SYNC_STATUS_OBJECTNOTFOUND);
479
+            }
480
+
481
+            // check for conflicts
482
+            /*
483 483
 			 * Checking for conflicts is correct at this point, but is a very expensive operation.
484 484
 			 * If the message was deleted, only an error will be shown.
485 485
 			 *
@@ -490,333 +490,333 @@  discard block
 block discarded – undo
490 490
 			}
491 491
 			 */
492 492
 
493
-			$readstate = ["sourcekey" => hex2bin($sk), "flags" => $flags];
494
-
495
-			if (!mapi_importcontentschanges_importperuserreadstatechange($this->importer, [$readstate])) {
496
-				throw new StatusException(sprintf("ImportChangesICS->ImportMessageReadFlag('%s','%d'): Error setting read state: 0x%X", $id, $flags, mapi_last_hresult()), SYNC_STATUS_OBJECTNOTFOUND);
497
-			}
498
-		}
499
-		// yeah OL sucks - ZP-779
500
-		else {
501
-			if (!$fsk) {
502
-				throw new StatusException(sprintf("ImportChangesICS->ImportMessageReadFlag('%s','%d'): Error setting read state. The message is in another folder but id is unknown as no short folder id is available. Please remove your device states to fully resync your device. Operation ignored.", $id, $flags), SYNC_STATUS_OBJECTNOTFOUND);
503
-			}
504
-			$store = GSync::GetBackend()->GetMAPIStoreForFolderId(GSync::GetAdditionalSyncFolderStore($fsk), $fsk);
505
-			$entryid = mapi_msgstore_entryidfromsourcekey($store, hex2bin($fsk), hex2bin($sk));
506
-			$realMessage = mapi_msgstore_openentry($store, $entryid);
507
-			$flag = 0;
508
-			if ($flags == 0) {
509
-				$flag |= CLEAR_READ_FLAG;
510
-			}
511
-			$p = mapi_message_setreadflag($realMessage, $flag);
512
-			SLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->ImportMessageReadFlag('%s','%d'): setting readflag on message: 0x%X", $id, $flags, mapi_last_hresult()));
513
-		}
514
-
515
-		return true;
516
-	}
517
-
518
-	/**
519
-	 * Imports a move of a message. This occurs when a user moves an item to another folder.
520
-	 *
521
-	 * Normally, we would implement this via the 'offical' importmessagemove() function on the ICS importer,
522
-	 * but the grommunio importer does not support this. Therefore we currently implement it via a standard mapi
523
-	 * call. This causes a mirror 'add/delete' to be sent to the PDA at the next sync.
524
-	 * Manfred, 2010-10-21. For some mobiles import was causing duplicate messages in the destination folder
525
-	 * (Mantis #202). Therefore we will create a new message in the destination folder, copy properties
526
-	 * of the source message to the new one and then delete the source message.
527
-	 *
528
-	 * @param string $id
529
-	 * @param string $newfolder destination folder
530
-	 *
531
-	 * @throws StatusException
532
-	 *
533
-	 * @return boolean/string
534
-	 */
535
-	public function ImportMessageMove($id, $newfolder) {
536
-		list(, $sk) = Utils::SplitMessageId($id);
537
-		if (strtolower($newfolder) == strtolower(bin2hex($this->folderid))) {
538
-			throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, source and destination are equal", $id, $newfolder), SYNC_MOVEITEMSSTATUS_SAMESOURCEANDDEST);
539
-		}
540
-
541
-		// Get the entryid of the message we're moving
542
-		$entryid = mapi_msgstore_entryidfromsourcekey($this->store, $this->folderid, hex2bin($sk));
543
-		$srcmessage = false;
544
-
545
-		if ($entryid) {
546
-			// open the source message
547
-			$srcmessage = mapi_msgstore_openentry($this->store, $entryid);
548
-		}
549
-
550
-		if (!$entryid || !$srcmessage) {
551
-			$code = SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID;
552
-			$mapiLastHresult = mapi_last_hresult();
553
-			// if we move to the trash and the source message is not found, we can also just tell the mobile that we successfully moved to avoid errors (ZP-624)
554
-			if ($newfolder == GSync::GetBackend()->GetWasteBasket()) {
555
-				$code = SYNC_MOVEITEMSSTATUS_SUCCESS;
556
-			}
557
-			$errorCase = !$entryid ? "resolve source message id" : "open source message";
558
-
559
-			throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to %s: 0x%X", $sk, $newfolder, $errorCase, $mapiLastHresult), $code);
560
-		}
561
-
562
-		// check if it is in the synchronization interval and/or shared+private
563
-		if (!$this->isModificationAllowed($sk)) {
564
-			throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Source message move is not allowed. Move not performed.", $id, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID);
565
-		}
566
-
567
-		// get correct mapi store for the destination folder
568
-		$dststore = GSync::GetBackend()->GetMAPIStoreForFolderId(GSync::GetAdditionalSyncFolderStore($newfolder), $newfolder);
569
-		if ($dststore === false) {
570
-			throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to open store of destination folder", $sk, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDDESTID);
571
-		}
572
-
573
-		$dstentryid = mapi_msgstore_entryidfromsourcekey($dststore, hex2bin($newfolder));
574
-		if (!$dstentryid) {
575
-			throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to resolve destination folder", $sk, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDDESTID);
576
-		}
577
-
578
-		$dstfolder = mapi_msgstore_openentry($dststore, $dstentryid);
579
-		if (!$dstfolder) {
580
-			throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to open destination folder", $sk, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDDESTID);
581
-		}
582
-
583
-		$newmessage = mapi_folder_createmessage($dstfolder);
584
-		if (!$newmessage) {
585
-			throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to create message in destination folder: 0x%X", $sk, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_INVALIDDESTID);
586
-		}
587
-
588
-		// Copy message
589
-		mapi_copyto($srcmessage, [], [], $newmessage);
590
-		if (mapi_last_hresult()) {
591
-			throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, copy to destination message failed: 0x%X", $sk, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_CANNOTMOVE);
592
-		}
593
-
594
-		$srcfolderentryid = mapi_msgstore_entryidfromsourcekey($this->store, $this->folderid);
595
-		if (!$srcfolderentryid) {
596
-			throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to resolve source folder", $sk, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID);
597
-		}
598
-
599
-		$srcfolder = mapi_msgstore_openentry($this->store, $srcfolderentryid);
600
-		if (!$srcfolder) {
601
-			throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to open source folder: 0x%X", $sk, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID);
602
-		}
603
-
604
-		// Save changes
605
-		mapi_savechanges($newmessage);
606
-		if (mapi_last_hresult()) {
607
-			throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, mapi_savechanges() failed: 0x%X", $sk, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_CANNOTMOVE);
608
-		}
609
-
610
-		// Delete the old message
611
-		if (!mapi_folder_deletemessages($srcfolder, [$entryid])) {
612
-			throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, delete of source message failed: 0x%X. Possible duplicates.", $sk, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_SOURCEORDESTLOCKED);
613
-		}
614
-
615
-		$sourcekeyprops = mapi_getprops($newmessage, [PR_SOURCE_KEY]);
616
-		if (isset($sourcekeyprops[PR_SOURCE_KEY]) && $sourcekeyprops[PR_SOURCE_KEY]) {
617
-			$prefix = "";
618
-			// prepend the destination short folderid, if it exists
619
-			$destShortId = GSync::GetDeviceManager()->GetFolderIdForBackendId($newfolder);
620
-			if ($destShortId !== $newfolder) {
621
-				$prefix = $destShortId . ":";
622
-			}
623
-
624
-			return $prefix . bin2hex($sourcekeyprops[PR_SOURCE_KEY]);
625
-		}
626
-
627
-		return false;
628
-	}
629
-
630
-	/*----------------------------------------------------------------------------------------------------------
493
+            $readstate = ["sourcekey" => hex2bin($sk), "flags" => $flags];
494
+
495
+            if (!mapi_importcontentschanges_importperuserreadstatechange($this->importer, [$readstate])) {
496
+                throw new StatusException(sprintf("ImportChangesICS->ImportMessageReadFlag('%s','%d'): Error setting read state: 0x%X", $id, $flags, mapi_last_hresult()), SYNC_STATUS_OBJECTNOTFOUND);
497
+            }
498
+        }
499
+        // yeah OL sucks - ZP-779
500
+        else {
501
+            if (!$fsk) {
502
+                throw new StatusException(sprintf("ImportChangesICS->ImportMessageReadFlag('%s','%d'): Error setting read state. The message is in another folder but id is unknown as no short folder id is available. Please remove your device states to fully resync your device. Operation ignored.", $id, $flags), SYNC_STATUS_OBJECTNOTFOUND);
503
+            }
504
+            $store = GSync::GetBackend()->GetMAPIStoreForFolderId(GSync::GetAdditionalSyncFolderStore($fsk), $fsk);
505
+            $entryid = mapi_msgstore_entryidfromsourcekey($store, hex2bin($fsk), hex2bin($sk));
506
+            $realMessage = mapi_msgstore_openentry($store, $entryid);
507
+            $flag = 0;
508
+            if ($flags == 0) {
509
+                $flag |= CLEAR_READ_FLAG;
510
+            }
511
+            $p = mapi_message_setreadflag($realMessage, $flag);
512
+            SLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->ImportMessageReadFlag('%s','%d'): setting readflag on message: 0x%X", $id, $flags, mapi_last_hresult()));
513
+        }
514
+
515
+        return true;
516
+    }
517
+
518
+    /**
519
+     * Imports a move of a message. This occurs when a user moves an item to another folder.
520
+     *
521
+     * Normally, we would implement this via the 'offical' importmessagemove() function on the ICS importer,
522
+     * but the grommunio importer does not support this. Therefore we currently implement it via a standard mapi
523
+     * call. This causes a mirror 'add/delete' to be sent to the PDA at the next sync.
524
+     * Manfred, 2010-10-21. For some mobiles import was causing duplicate messages in the destination folder
525
+     * (Mantis #202). Therefore we will create a new message in the destination folder, copy properties
526
+     * of the source message to the new one and then delete the source message.
527
+     *
528
+     * @param string $id
529
+     * @param string $newfolder destination folder
530
+     *
531
+     * @throws StatusException
532
+     *
533
+     * @return boolean/string
534
+     */
535
+    public function ImportMessageMove($id, $newfolder) {
536
+        list(, $sk) = Utils::SplitMessageId($id);
537
+        if (strtolower($newfolder) == strtolower(bin2hex($this->folderid))) {
538
+            throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, source and destination are equal", $id, $newfolder), SYNC_MOVEITEMSSTATUS_SAMESOURCEANDDEST);
539
+        }
540
+
541
+        // Get the entryid of the message we're moving
542
+        $entryid = mapi_msgstore_entryidfromsourcekey($this->store, $this->folderid, hex2bin($sk));
543
+        $srcmessage = false;
544
+
545
+        if ($entryid) {
546
+            // open the source message
547
+            $srcmessage = mapi_msgstore_openentry($this->store, $entryid);
548
+        }
549
+
550
+        if (!$entryid || !$srcmessage) {
551
+            $code = SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID;
552
+            $mapiLastHresult = mapi_last_hresult();
553
+            // if we move to the trash and the source message is not found, we can also just tell the mobile that we successfully moved to avoid errors (ZP-624)
554
+            if ($newfolder == GSync::GetBackend()->GetWasteBasket()) {
555
+                $code = SYNC_MOVEITEMSSTATUS_SUCCESS;
556
+            }
557
+            $errorCase = !$entryid ? "resolve source message id" : "open source message";
558
+
559
+            throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to %s: 0x%X", $sk, $newfolder, $errorCase, $mapiLastHresult), $code);
560
+        }
561
+
562
+        // check if it is in the synchronization interval and/or shared+private
563
+        if (!$this->isModificationAllowed($sk)) {
564
+            throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Source message move is not allowed. Move not performed.", $id, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID);
565
+        }
566
+
567
+        // get correct mapi store for the destination folder
568
+        $dststore = GSync::GetBackend()->GetMAPIStoreForFolderId(GSync::GetAdditionalSyncFolderStore($newfolder), $newfolder);
569
+        if ($dststore === false) {
570
+            throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to open store of destination folder", $sk, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDDESTID);
571
+        }
572
+
573
+        $dstentryid = mapi_msgstore_entryidfromsourcekey($dststore, hex2bin($newfolder));
574
+        if (!$dstentryid) {
575
+            throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to resolve destination folder", $sk, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDDESTID);
576
+        }
577
+
578
+        $dstfolder = mapi_msgstore_openentry($dststore, $dstentryid);
579
+        if (!$dstfolder) {
580
+            throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to open destination folder", $sk, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDDESTID);
581
+        }
582
+
583
+        $newmessage = mapi_folder_createmessage($dstfolder);
584
+        if (!$newmessage) {
585
+            throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to create message in destination folder: 0x%X", $sk, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_INVALIDDESTID);
586
+        }
587
+
588
+        // Copy message
589
+        mapi_copyto($srcmessage, [], [], $newmessage);
590
+        if (mapi_last_hresult()) {
591
+            throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, copy to destination message failed: 0x%X", $sk, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_CANNOTMOVE);
592
+        }
593
+
594
+        $srcfolderentryid = mapi_msgstore_entryidfromsourcekey($this->store, $this->folderid);
595
+        if (!$srcfolderentryid) {
596
+            throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to resolve source folder", $sk, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID);
597
+        }
598
+
599
+        $srcfolder = mapi_msgstore_openentry($this->store, $srcfolderentryid);
600
+        if (!$srcfolder) {
601
+            throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to open source folder: 0x%X", $sk, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID);
602
+        }
603
+
604
+        // Save changes
605
+        mapi_savechanges($newmessage);
606
+        if (mapi_last_hresult()) {
607
+            throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, mapi_savechanges() failed: 0x%X", $sk, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_CANNOTMOVE);
608
+        }
609
+
610
+        // Delete the old message
611
+        if (!mapi_folder_deletemessages($srcfolder, [$entryid])) {
612
+            throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, delete of source message failed: 0x%X. Possible duplicates.", $sk, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_SOURCEORDESTLOCKED);
613
+        }
614
+
615
+        $sourcekeyprops = mapi_getprops($newmessage, [PR_SOURCE_KEY]);
616
+        if (isset($sourcekeyprops[PR_SOURCE_KEY]) && $sourcekeyprops[PR_SOURCE_KEY]) {
617
+            $prefix = "";
618
+            // prepend the destination short folderid, if it exists
619
+            $destShortId = GSync::GetDeviceManager()->GetFolderIdForBackendId($newfolder);
620
+            if ($destShortId !== $newfolder) {
621
+                $prefix = $destShortId . ":";
622
+            }
623
+
624
+            return $prefix . bin2hex($sourcekeyprops[PR_SOURCE_KEY]);
625
+        }
626
+
627
+        return false;
628
+    }
629
+
630
+    /*----------------------------------------------------------------------------------------------------------
631 631
 	 * Methods for HierarchyExporter
632 632
 	 */
633 633
 
634
-	/**
635
-	 * Imports a change on a folder.
636
-	 *
637
-	 * @param object $folder SyncFolder
638
-	 *
639
-	 * @throws StatusException
640
-	 *
641
-	 * @return boolean/SyncFolder       false on error or a SyncFolder object with serverid and BackendId set (if available)
642
-	 */
643
-	public function ImportFolderChange($folder) {
644
-		$id = isset($folder->BackendId) ? $folder->BackendId : false;
645
-		$parent = $folder->parentid;
646
-		$parent_org = $folder->parentid;
647
-		$displayname = u2wi($folder->displayname);
648
-		$type = $folder->type;
649
-
650
-		if (Utils::IsSystemFolder($type)) {
651
-			throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, system folder can not be created/modified", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname), SYNC_FSSTATUS_SYSTEMFOLDER);
652
-		}
653
-
654
-		// create a new folder if $id is not set
655
-		if (!$id) {
656
-			// the root folder is "0" - get IPM_SUBTREE
657
-			if ($parent == "0") {
658
-				$parentprops = mapi_getprops($this->store, [PR_IPM_SUBTREE_ENTRYID, PR_IPM_PUBLIC_FOLDERS_ENTRYID]);
659
-				if (GSync::GetBackend()->GetImpersonatedUser() == 'system' && isset($parentprops[PR_IPM_PUBLIC_FOLDERS_ENTRYID])) {
660
-					$parentfentryid = $parentprops[PR_IPM_PUBLIC_FOLDERS_ENTRYID];
661
-				}
662
-				elseif (isset($parentprops[PR_IPM_SUBTREE_ENTRYID])) {
663
-					$parentfentryid = $parentprops[PR_IPM_SUBTREE_ENTRYID];
664
-				}
665
-			}
666
-			else {
667
-				$parentfentryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($parent));
668
-			}
669
-
670
-			if (!$parentfentryid) {
671
-				throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, unable to open parent folder (no entry id)", Utils::PrintAsString(false), $folder->parentid, $displayname), SYNC_FSSTATUS_PARENTNOTFOUND);
672
-			}
673
-
674
-			$parentfolder = mapi_msgstore_openentry($this->store, $parentfentryid);
675
-			if (!$parentfolder) {
676
-				throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, unable to open parent folder (open entry)", Utils::PrintAsString(false), $folder->parentid, $displayname), SYNC_FSSTATUS_PARENTNOTFOUND);
677
-			}
678
-
679
-			//  mapi_folder_createfolder() fails if a folder with this name already exists -> MAPI_E_COLLISION
680
-			$newfolder = mapi_folder_createfolder($parentfolder, $displayname, "");
681
-			if (mapi_last_hresult()) {
682
-				throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, mapi_folder_createfolder() failed: 0x%X", Utils::PrintAsString(false), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_FOLDEREXISTS);
683
-			}
684
-
685
-			mapi_setprops($newfolder, [PR_CONTAINER_CLASS => MAPIUtils::GetContainerClassFromFolderType($type)]);
686
-
687
-			$props = mapi_getprops($newfolder, [PR_SOURCE_KEY]);
688
-			if (isset($props[PR_SOURCE_KEY])) {
689
-				$folder->BackendId = bin2hex($props[PR_SOURCE_KEY]);
690
-				$folderOrigin = DeviceManager::FLD_ORIGIN_USER;
691
-				if (GSync::GetBackend()->GetImpersonatedUser()) {
692
-					$folderOrigin = DeviceManager::FLD_ORIGIN_IMPERSONATED;
693
-				}
694
-				$folder->serverid = GSync::GetDeviceManager()->GetFolderIdForBackendId($folder->BackendId, true, $folderOrigin, $folder->displayname);
695
-				SLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->ImportFolderChange(): Created folder '%s' with id: '%s' backendid: '%s'", $displayname, $folder->serverid, $folder->BackendId));
696
-
697
-				return $folder;
698
-			}
699
-
700
-			throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, folder created but PR_SOURCE_KEY not available: 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_SERVERERROR);
701
-		}
702
-
703
-		// open folder for update
704
-		$entryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($id));
705
-		if (!$entryid) {
706
-			throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, unable to open folder (no entry id): 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_PARENTNOTFOUND);
707
-		}
708
-
709
-		// check if this is a MAPI default folder
710
-		if ($this->mapiprovider->IsMAPIDefaultFolder($entryid)) {
711
-			throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, MAPI default folder can not be created/modified", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname), SYNC_FSSTATUS_SYSTEMFOLDER);
712
-		}
713
-
714
-		$mfolder = mapi_msgstore_openentry($this->store, $entryid);
715
-		if (!$mfolder) {
716
-			throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, unable to open folder (open entry): 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_PARENTNOTFOUND);
717
-		}
718
-
719
-		$props = mapi_getprops($mfolder, [PR_SOURCE_KEY, PR_PARENT_SOURCE_KEY, PR_DISPLAY_NAME, PR_CONTAINER_CLASS]);
720
-		if (!isset($props[PR_SOURCE_KEY]) || !isset($props[PR_PARENT_SOURCE_KEY]) || !isset($props[PR_DISPLAY_NAME]) || !isset($props[PR_CONTAINER_CLASS])) {
721
-			throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, folder data not available: 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_SERVERERROR);
722
-		}
723
-
724
-		// get the real parent source key from mapi
725
-		if ($parent == "0") {
726
-			$parentprops = mapi_getprops($this->store, [PR_IPM_SUBTREE_ENTRYID, PR_IPM_PUBLIC_FOLDERS_ENTRYID]);
727
-			if (GSync::GetBackend()->GetImpersonatedUser() == 'system') {
728
-				$parentfentryid = $parentprops[PR_IPM_PUBLIC_FOLDERS_ENTRYID];
729
-			}
730
-			else {
731
-				$parentfentryid = $parentprops[PR_IPM_SUBTREE_ENTRYID];
732
-			}
733
-			$mapifolder = mapi_msgstore_openentry($this->store, $parentfentryid);
734
-
735
-			$rootfolderprops = mapi_getprops($mapifolder, [PR_SOURCE_KEY]);
736
-			$parent = bin2hex($rootfolderprops[PR_SOURCE_KEY]);
737
-			SLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->ImportFolderChange(): resolved AS parent '0' to sourcekey '%s'", $parent));
738
-		}
739
-
740
-		// a changed parent id means that the folder should be moved
741
-		if (bin2hex($props[PR_PARENT_SOURCE_KEY]) !== $parent) {
742
-			$sourceparentfentryid = mapi_msgstore_entryidfromsourcekey($this->store, $props[PR_PARENT_SOURCE_KEY]);
743
-			if (!$sourceparentfentryid) {
744
-				throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, unable to open parent source folder (no entry id): 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_PARENTNOTFOUND);
745
-			}
746
-
747
-			$sourceparentfolder = mapi_msgstore_openentry($this->store, $sourceparentfentryid);
748
-			if (!$sourceparentfolder) {
749
-				throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, unable to open parent source folder (open entry): 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_PARENTNOTFOUND);
750
-			}
751
-
752
-			$destparentfentryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($parent));
753
-			if (!$sourceparentfentryid) {
754
-				throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, unable to open destination folder (no entry id): 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_SERVERERROR);
755
-			}
756
-
757
-			$destfolder = mapi_msgstore_openentry($this->store, $destparentfentryid);
758
-			if (!$destfolder) {
759
-				throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, unable to open destination folder (open entry): 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_SERVERERROR);
760
-			}
761
-
762
-			// mapi_folder_copyfolder() fails if a folder with this name already exists -> MAPI_E_COLLISION
763
-			if (!mapi_folder_copyfolder($sourceparentfolder, $entryid, $destfolder, $displayname, FOLDER_MOVE)) {
764
-				throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, unable to move folder: 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_FOLDEREXISTS);
765
-			}
766
-
767
-			// the parent changed, but we got a backendID as parent and have to return an AS folderid - the parent-backendId must be mapped at this point already
768
-			if ($folder->parentid != 0) {
769
-				$folder->parentid = GSync::GetDeviceManager()->GetFolderIdForBackendId($parent);
770
-			}
771
-			SLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->ImportFolderChange(): Moved folder '%s' with id: %s/%s from: %s to: %s/%s", $displayname, $folder->serverid, $folder->BackendId, bin2hex($props[PR_PARENT_SOURCE_KEY]), $folder->parentid, $parent_org));
772
-
773
-			return $folder;
774
-		}
775
-
776
-		// update the display name
777
-		$props = [PR_DISPLAY_NAME => $displayname];
778
-		mapi_setprops($mfolder, $props);
779
-		mapi_savechanges($mfolder);
780
-		if (mapi_last_hresult()) {
781
-			throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, mapi_savechanges() failed: 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_SERVERERROR);
782
-		}
783
-
784
-		SLog::Write(LOGLEVEL_DEBUG, "Imported changes for folder: {$id}");
785
-
786
-		return true;
787
-	}
788
-
789
-	/**
790
-	 * Imports a folder deletion.
791
-	 *
792
-	 * @param SyncFolder $folder at least "serverid" needs to be set
793
-	 *
794
-	 * @throws StatusException
795
-	 *
796
-	 * @return int SYNC_FOLDERHIERARCHY_STATUS
797
-	 */
798
-	public function ImportFolderDeletion($folder) {
799
-		$id = $folder->BackendId;
800
-		$parent = isset($folder->parentid) ? $folder->parentid : false;
801
-		SLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->ImportFolderDeletion('%s','%s'): importing folder deletetion", $id, $parent));
802
-
803
-		$folderentryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($id));
804
-		if (!$folderentryid) {
805
-			throw new StatusException(sprintf("ImportChangesICS->ImportFolderDeletion('%s','%s'): Error, unable to resolve folder", $id, $parent, mapi_last_hresult()), SYNC_FSSTATUS_FOLDERDOESNOTEXIST);
806
-		}
807
-
808
-		// get the folder type from the MAPIProvider
809
-		$type = $this->mapiprovider->GetFolderType($folderentryid);
810
-
811
-		if (Utils::IsSystemFolder($type) || $this->mapiprovider->IsMAPIDefaultFolder($folderentryid)) {
812
-			throw new StatusException(sprintf("ImportChangesICS->ImportFolderDeletion('%s','%s'): Error deleting system/default folder", $id, $parent), SYNC_FSSTATUS_SYSTEMFOLDER);
813
-		}
814
-
815
-		$ret = mapi_importhierarchychanges_importfolderdeletion($this->importer, 0, [PR_SOURCE_KEY => hex2bin($id)]);
816
-		if (!$ret) {
817
-			throw new StatusException(sprintf("ImportChangesICS->ImportFolderDeletion('%s','%s'): Error deleting folder: 0x%X", $id, $parent, mapi_last_hresult()), SYNC_FSSTATUS_SERVERERROR);
818
-		}
819
-
820
-		return $ret;
821
-	}
634
+    /**
635
+     * Imports a change on a folder.
636
+     *
637
+     * @param object $folder SyncFolder
638
+     *
639
+     * @throws StatusException
640
+     *
641
+     * @return boolean/SyncFolder       false on error or a SyncFolder object with serverid and BackendId set (if available)
642
+     */
643
+    public function ImportFolderChange($folder) {
644
+        $id = isset($folder->BackendId) ? $folder->BackendId : false;
645
+        $parent = $folder->parentid;
646
+        $parent_org = $folder->parentid;
647
+        $displayname = u2wi($folder->displayname);
648
+        $type = $folder->type;
649
+
650
+        if (Utils::IsSystemFolder($type)) {
651
+            throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, system folder can not be created/modified", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname), SYNC_FSSTATUS_SYSTEMFOLDER);
652
+        }
653
+
654
+        // create a new folder if $id is not set
655
+        if (!$id) {
656
+            // the root folder is "0" - get IPM_SUBTREE
657
+            if ($parent == "0") {
658
+                $parentprops = mapi_getprops($this->store, [PR_IPM_SUBTREE_ENTRYID, PR_IPM_PUBLIC_FOLDERS_ENTRYID]);
659
+                if (GSync::GetBackend()->GetImpersonatedUser() == 'system' && isset($parentprops[PR_IPM_PUBLIC_FOLDERS_ENTRYID])) {
660
+                    $parentfentryid = $parentprops[PR_IPM_PUBLIC_FOLDERS_ENTRYID];
661
+                }
662
+                elseif (isset($parentprops[PR_IPM_SUBTREE_ENTRYID])) {
663
+                    $parentfentryid = $parentprops[PR_IPM_SUBTREE_ENTRYID];
664
+                }
665
+            }
666
+            else {
667
+                $parentfentryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($parent));
668
+            }
669
+
670
+            if (!$parentfentryid) {
671
+                throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, unable to open parent folder (no entry id)", Utils::PrintAsString(false), $folder->parentid, $displayname), SYNC_FSSTATUS_PARENTNOTFOUND);
672
+            }
673
+
674
+            $parentfolder = mapi_msgstore_openentry($this->store, $parentfentryid);
675
+            if (!$parentfolder) {
676
+                throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, unable to open parent folder (open entry)", Utils::PrintAsString(false), $folder->parentid, $displayname), SYNC_FSSTATUS_PARENTNOTFOUND);
677
+            }
678
+
679
+            //  mapi_folder_createfolder() fails if a folder with this name already exists -> MAPI_E_COLLISION
680
+            $newfolder = mapi_folder_createfolder($parentfolder, $displayname, "");
681
+            if (mapi_last_hresult()) {
682
+                throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, mapi_folder_createfolder() failed: 0x%X", Utils::PrintAsString(false), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_FOLDEREXISTS);
683
+            }
684
+
685
+            mapi_setprops($newfolder, [PR_CONTAINER_CLASS => MAPIUtils::GetContainerClassFromFolderType($type)]);
686
+
687
+            $props = mapi_getprops($newfolder, [PR_SOURCE_KEY]);
688
+            if (isset($props[PR_SOURCE_KEY])) {
689
+                $folder->BackendId = bin2hex($props[PR_SOURCE_KEY]);
690
+                $folderOrigin = DeviceManager::FLD_ORIGIN_USER;
691
+                if (GSync::GetBackend()->GetImpersonatedUser()) {
692
+                    $folderOrigin = DeviceManager::FLD_ORIGIN_IMPERSONATED;
693
+                }
694
+                $folder->serverid = GSync::GetDeviceManager()->GetFolderIdForBackendId($folder->BackendId, true, $folderOrigin, $folder->displayname);
695
+                SLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->ImportFolderChange(): Created folder '%s' with id: '%s' backendid: '%s'", $displayname, $folder->serverid, $folder->BackendId));
696
+
697
+                return $folder;
698
+            }
699
+
700
+            throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, folder created but PR_SOURCE_KEY not available: 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_SERVERERROR);
701
+        }
702
+
703
+        // open folder for update
704
+        $entryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($id));
705
+        if (!$entryid) {
706
+            throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, unable to open folder (no entry id): 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_PARENTNOTFOUND);
707
+        }
708
+
709
+        // check if this is a MAPI default folder
710
+        if ($this->mapiprovider->IsMAPIDefaultFolder($entryid)) {
711
+            throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, MAPI default folder can not be created/modified", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname), SYNC_FSSTATUS_SYSTEMFOLDER);
712
+        }
713
+
714
+        $mfolder = mapi_msgstore_openentry($this->store, $entryid);
715
+        if (!$mfolder) {
716
+            throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, unable to open folder (open entry): 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_PARENTNOTFOUND);
717
+        }
718
+
719
+        $props = mapi_getprops($mfolder, [PR_SOURCE_KEY, PR_PARENT_SOURCE_KEY, PR_DISPLAY_NAME, PR_CONTAINER_CLASS]);
720
+        if (!isset($props[PR_SOURCE_KEY]) || !isset($props[PR_PARENT_SOURCE_KEY]) || !isset($props[PR_DISPLAY_NAME]) || !isset($props[PR_CONTAINER_CLASS])) {
721
+            throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, folder data not available: 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_SERVERERROR);
722
+        }
723
+
724
+        // get the real parent source key from mapi
725
+        if ($parent == "0") {
726
+            $parentprops = mapi_getprops($this->store, [PR_IPM_SUBTREE_ENTRYID, PR_IPM_PUBLIC_FOLDERS_ENTRYID]);
727
+            if (GSync::GetBackend()->GetImpersonatedUser() == 'system') {
728
+                $parentfentryid = $parentprops[PR_IPM_PUBLIC_FOLDERS_ENTRYID];
729
+            }
730
+            else {
731
+                $parentfentryid = $parentprops[PR_IPM_SUBTREE_ENTRYID];
732
+            }
733
+            $mapifolder = mapi_msgstore_openentry($this->store, $parentfentryid);
734
+
735
+            $rootfolderprops = mapi_getprops($mapifolder, [PR_SOURCE_KEY]);
736
+            $parent = bin2hex($rootfolderprops[PR_SOURCE_KEY]);
737
+            SLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->ImportFolderChange(): resolved AS parent '0' to sourcekey '%s'", $parent));
738
+        }
739
+
740
+        // a changed parent id means that the folder should be moved
741
+        if (bin2hex($props[PR_PARENT_SOURCE_KEY]) !== $parent) {
742
+            $sourceparentfentryid = mapi_msgstore_entryidfromsourcekey($this->store, $props[PR_PARENT_SOURCE_KEY]);
743
+            if (!$sourceparentfentryid) {
744
+                throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, unable to open parent source folder (no entry id): 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_PARENTNOTFOUND);
745
+            }
746
+
747
+            $sourceparentfolder = mapi_msgstore_openentry($this->store, $sourceparentfentryid);
748
+            if (!$sourceparentfolder) {
749
+                throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, unable to open parent source folder (open entry): 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_PARENTNOTFOUND);
750
+            }
751
+
752
+            $destparentfentryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($parent));
753
+            if (!$sourceparentfentryid) {
754
+                throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, unable to open destination folder (no entry id): 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_SERVERERROR);
755
+            }
756
+
757
+            $destfolder = mapi_msgstore_openentry($this->store, $destparentfentryid);
758
+            if (!$destfolder) {
759
+                throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, unable to open destination folder (open entry): 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_SERVERERROR);
760
+            }
761
+
762
+            // mapi_folder_copyfolder() fails if a folder with this name already exists -> MAPI_E_COLLISION
763
+            if (!mapi_folder_copyfolder($sourceparentfolder, $entryid, $destfolder, $displayname, FOLDER_MOVE)) {
764
+                throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, unable to move folder: 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_FOLDEREXISTS);
765
+            }
766
+
767
+            // the parent changed, but we got a backendID as parent and have to return an AS folderid - the parent-backendId must be mapped at this point already
768
+            if ($folder->parentid != 0) {
769
+                $folder->parentid = GSync::GetDeviceManager()->GetFolderIdForBackendId($parent);
770
+            }
771
+            SLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->ImportFolderChange(): Moved folder '%s' with id: %s/%s from: %s to: %s/%s", $displayname, $folder->serverid, $folder->BackendId, bin2hex($props[PR_PARENT_SOURCE_KEY]), $folder->parentid, $parent_org));
772
+
773
+            return $folder;
774
+        }
775
+
776
+        // update the display name
777
+        $props = [PR_DISPLAY_NAME => $displayname];
778
+        mapi_setprops($mfolder, $props);
779
+        mapi_savechanges($mfolder);
780
+        if (mapi_last_hresult()) {
781
+            throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, mapi_savechanges() failed: 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_SERVERERROR);
782
+        }
783
+
784
+        SLog::Write(LOGLEVEL_DEBUG, "Imported changes for folder: {$id}");
785
+
786
+        return true;
787
+    }
788
+
789
+    /**
790
+     * Imports a folder deletion.
791
+     *
792
+     * @param SyncFolder $folder at least "serverid" needs to be set
793
+     *
794
+     * @throws StatusException
795
+     *
796
+     * @return int SYNC_FOLDERHIERARCHY_STATUS
797
+     */
798
+    public function ImportFolderDeletion($folder) {
799
+        $id = $folder->BackendId;
800
+        $parent = isset($folder->parentid) ? $folder->parentid : false;
801
+        SLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->ImportFolderDeletion('%s','%s'): importing folder deletetion", $id, $parent));
802
+
803
+        $folderentryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($id));
804
+        if (!$folderentryid) {
805
+            throw new StatusException(sprintf("ImportChangesICS->ImportFolderDeletion('%s','%s'): Error, unable to resolve folder", $id, $parent, mapi_last_hresult()), SYNC_FSSTATUS_FOLDERDOESNOTEXIST);
806
+        }
807
+
808
+        // get the folder type from the MAPIProvider
809
+        $type = $this->mapiprovider->GetFolderType($folderentryid);
810
+
811
+        if (Utils::IsSystemFolder($type) || $this->mapiprovider->IsMAPIDefaultFolder($folderentryid)) {
812
+            throw new StatusException(sprintf("ImportChangesICS->ImportFolderDeletion('%s','%s'): Error deleting system/default folder", $id, $parent), SYNC_FSSTATUS_SYSTEMFOLDER);
813
+        }
814
+
815
+        $ret = mapi_importhierarchychanges_importfolderdeletion($this->importer, 0, [PR_SOURCE_KEY => hex2bin($id)]);
816
+        if (!$ret) {
817
+            throw new StatusException(sprintf("ImportChangesICS->ImportFolderDeletion('%s','%s'): Error deleting folder: 0x%X", $id, $parent, mapi_last_hresult()), SYNC_FSSTATUS_SERVERERROR);
818
+        }
819
+
820
+        return $ret;
821
+    }
822 822
 }
Please login to merge, or discard this patch.
Spacing   +4 added lines, -4 removed lines patch added patch discarded remove patch
@@ -58,7 +58,7 @@  discard block
 block discarded – undo
58 58
 			$folderidForBackendId = GSync::GetDeviceManager()->GetFolderIdForBackendId($this->folderidHex);
59 59
 			// Only append backend id if the mapping backendid<->folderid is available.
60 60
 			if ($folderidForBackendId != $this->folderidHex) {
61
-				$this->prefix = $folderidForBackendId . ':';
61
+				$this->prefix = $folderidForBackendId.':';
62 62
 			}
63 63
 		}
64 64
 		else {
@@ -410,7 +410,7 @@  discard block
 block discarded – undo
410 410
 
411 411
 			$sourcekeyprops = mapi_getprops($mapimessage, [PR_SOURCE_KEY]);
412 412
 
413
-			return $this->prefix . bin2hex($sourcekeyprops[PR_SOURCE_KEY]);
413
+			return $this->prefix.bin2hex($sourcekeyprops[PR_SOURCE_KEY]);
414 414
 		}
415 415
 
416 416
 		throw new StatusException(sprintf("ImportChangesICS->ImportMessageChange('%s','%s'): Error updating object: 0x%X", $id, get_class($message), mapi_last_hresult()), SYNC_STATUS_OBJECTNOTFOUND);
@@ -618,10 +618,10 @@  discard block
 block discarded – undo
618 618
 			// prepend the destination short folderid, if it exists
619 619
 			$destShortId = GSync::GetDeviceManager()->GetFolderIdForBackendId($newfolder);
620 620
 			if ($destShortId !== $newfolder) {
621
-				$prefix = $destShortId . ":";
621
+				$prefix = $destShortId.":";
622 622
 			}
623 623
 
624
-			return $prefix . bin2hex($sourcekeyprops[PR_SOURCE_KEY]);
624
+			return $prefix.bin2hex($sourcekeyprops[PR_SOURCE_KEY]);
625 625
 		}
626 626
 
627 627
 		return false;
Please login to merge, or discard this patch.
Braces   +10 added lines, -20 removed lines patch added patch discarded remove patch
@@ -60,13 +60,11 @@  discard block
 block discarded – undo
60 60
 			if ($folderidForBackendId != $this->folderidHex) {
61 61
 				$this->prefix = $folderidForBackendId . ':';
62 62
 			}
63
-		}
64
-		else {
63
+		} else {
65 64
 			$storeprops = mapi_getprops($store, [PR_IPM_SUBTREE_ENTRYID, PR_IPM_PUBLIC_FOLDERS_ENTRYID]);
66 65
 			if (GSync::GetBackend()->GetImpersonatedUser() == 'system') {
67 66
 				$entryid = $storeprops[PR_IPM_PUBLIC_FOLDERS_ENTRYID];
68
-			}
69
-			else {
67
+			} else {
70 68
 				$entryid = $storeprops[PR_IPM_SUBTREE_ENTRYID];
71 69
 			}
72 70
 		}
@@ -88,8 +86,7 @@  discard block
 block discarded – undo
88 86
 
89 87
 		if ($folderid) {
90 88
 			$this->importer = mapi_openproperty($folder, PR_COLLECTOR, IID_IExchangeImportContentsChanges, 0, 0);
91
-		}
92
-		else {
89
+		} else {
93 90
 			$this->importer = mapi_openproperty($folder, PR_COLLECTOR, IID_IExchangeImportHierarchyChanges, 0, 0);
94 91
 		}
95 92
 	}
@@ -127,8 +124,7 @@  discard block
 block discarded – undo
127 124
 			// possible conflicting messages will be cached here
128 125
 			$this->memChanges = new ChangesMemoryWrapper();
129 126
 			$stat = mapi_importcontentschanges_config($this->importer, $stream, $flags);
130
-		}
131
-		else {
127
+		} else {
132 128
 			$stat = mapi_importhierarchychanges_config($this->importer, $stream, $flags);
133 129
 		}
134 130
 
@@ -201,8 +197,7 @@  discard block
 block discarded – undo
201 197
 			$data = mapi_stream_read($this->statestream, 4096);
202 198
 			if (strlen($data)) {
203 199
 				$state .= $data;
204
-			}
205
-			else {
200
+			} else {
206 201
 				break;
207 202
 			}
208 203
 		}
@@ -395,8 +390,7 @@  discard block
 block discarded – undo
395 390
 
396 391
 				return false;
397 392
 			}
398
-		}
399
-		else {
393
+		} else {
400 394
 			$flags = SYNC_NEW_MESSAGE;
401 395
 		}
402 396
 
@@ -436,8 +430,7 @@  discard block
 block discarded – undo
436 430
 		$this->lazyLoadConflicts();
437 431
 		if ($this->memChanges->IsChanged($id)) {
438 432
 			SLog::Write(LOGLEVEL_INFO, sprintf("ImportChangesICS->ImportMessageDeletion('%s'): Conflict detected. Data from Server will be dropped! PIM deleted object.", $id));
439
-		}
440
-		elseif ($this->memChanges->IsDeleted($id)) {
433
+		} elseif ($this->memChanges->IsDeleted($id)) {
441 434
 			SLog::Write(LOGLEVEL_INFO, sprintf("ImportChangesICS->ImportMessageDeletion('%s'): Conflict detected. Data is already deleted. Request will be ignored.", $id));
442 435
 
443 436
 			return true;
@@ -658,12 +651,10 @@  discard block
 block discarded – undo
658 651
 				$parentprops = mapi_getprops($this->store, [PR_IPM_SUBTREE_ENTRYID, PR_IPM_PUBLIC_FOLDERS_ENTRYID]);
659 652
 				if (GSync::GetBackend()->GetImpersonatedUser() == 'system' && isset($parentprops[PR_IPM_PUBLIC_FOLDERS_ENTRYID])) {
660 653
 					$parentfentryid = $parentprops[PR_IPM_PUBLIC_FOLDERS_ENTRYID];
661
-				}
662
-				elseif (isset($parentprops[PR_IPM_SUBTREE_ENTRYID])) {
654
+				} elseif (isset($parentprops[PR_IPM_SUBTREE_ENTRYID])) {
663 655
 					$parentfentryid = $parentprops[PR_IPM_SUBTREE_ENTRYID];
664 656
 				}
665
-			}
666
-			else {
657
+			} else {
667 658
 				$parentfentryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($parent));
668 659
 			}
669 660
 
@@ -726,8 +717,7 @@  discard block
 block discarded – undo
726 717
 			$parentprops = mapi_getprops($this->store, [PR_IPM_SUBTREE_ENTRYID, PR_IPM_PUBLIC_FOLDERS_ENTRYID]);
727 718
 			if (GSync::GetBackend()->GetImpersonatedUser() == 'system') {
728 719
 				$parentfentryid = $parentprops[PR_IPM_PUBLIC_FOLDERS_ENTRYID];
729
-			}
730
-			else {
720
+			} else {
731 721
 				$parentfentryid = $parentprops[PR_IPM_SUBTREE_ENTRYID];
732 722
 			}
733 723
 			$mapifolder = mapi_msgstore_openentry($this->store, $parentfentryid);
Please login to merge, or discard this patch.
lib/interface/ichanges.php 1 patch
Indentation   +35 added lines, -35 removed lines patch added patch discarded remove patch
@@ -11,42 +11,42 @@
 block discarded – undo
11 11
  */
12 12
 
13 13
 interface IChanges {
14
-	/**
15
-	 * Constructor.
16
-	 *
17
-	 * @param mixed $state
18
-	 * @param mixed $flags
19
-	 *
20
-	 * @throws StatusException
21
-	 */
14
+    /**
15
+     * Constructor.
16
+     *
17
+     * @param mixed $state
18
+     * @param mixed $flags
19
+     *
20
+     * @throws StatusException
21
+     */
22 22
 
23
-	/**
24
-	 * Initializes the state and flags.
25
-	 *
26
-	 * @param string $state
27
-	 * @param int    $flags
28
-	 *
29
-	 * @throws StatusException
30
-	 *
31
-	 * @return bool status flag
32
-	 */
33
-	public function Config($state, $flags = 0);
23
+    /**
24
+     * Initializes the state and flags.
25
+     *
26
+     * @param string $state
27
+     * @param int    $flags
28
+     *
29
+     * @throws StatusException
30
+     *
31
+     * @return bool status flag
32
+     */
33
+    public function Config($state, $flags = 0);
34 34
 
35
-	/**
36
-	 * Configures additional parameters used for content synchronization.
37
-	 *
38
-	 * @param ContentParameters $contentparameters
39
-	 *
40
-	 * @throws StatusException
41
-	 *
42
-	 * @return bool
43
-	 */
44
-	public function ConfigContentParameters($contentparameters);
35
+    /**
36
+     * Configures additional parameters used for content synchronization.
37
+     *
38
+     * @param ContentParameters $contentparameters
39
+     *
40
+     * @throws StatusException
41
+     *
42
+     * @return bool
43
+     */
44
+    public function ConfigContentParameters($contentparameters);
45 45
 
46
-	/**
47
-	 * Reads and returns the current state.
48
-	 *
49
-	 * @return string
50
-	 */
51
-	public function GetState();
46
+    /**
47
+     * Reads and returns the current state.
48
+     *
49
+     * @return string
50
+     */
51
+    public function GetState();
52 52
 }
Please login to merge, or discard this patch.
lib/interface/iimportchanges.php 1 patch
Indentation   +78 added lines, -78 removed lines patch added patch discarded remove patch
@@ -10,94 +10,94 @@
 block discarded – undo
10 10
  */
11 11
 
12 12
 interface IImportChanges extends IChanges {
13
-	/*----------------------------------------------------------------------------------------------------------
13
+    /*----------------------------------------------------------------------------------------------------------
14 14
 	 * Methods for to import contents
15 15
 	 */
16 16
 
17
-	/**
18
-	 * Loads objects which are expected to be exported with the state
19
-	 * Before importing/saving the actual message from the mobile, a conflict detection should be done.
20
-	 *
21
-	 * @param ContentParameters $contentparameters
22
-	 * @param string            $state
23
-	 *
24
-	 * @throws StatusException
25
-	 *
26
-	 * @return bool
27
-	 */
28
-	public function LoadConflicts($contentparameters, $state);
17
+    /**
18
+     * Loads objects which are expected to be exported with the state
19
+     * Before importing/saving the actual message from the mobile, a conflict detection should be done.
20
+     *
21
+     * @param ContentParameters $contentparameters
22
+     * @param string            $state
23
+     *
24
+     * @throws StatusException
25
+     *
26
+     * @return bool
27
+     */
28
+    public function LoadConflicts($contentparameters, $state);
29 29
 
30
-	/**
31
-	 * Imports a single message.
32
-	 *
33
-	 * @param string     $id
34
-	 * @param SyncObject $message
35
-	 *
36
-	 * @throws StatusException
37
-	 *
38
-	 * @return boolean/string               failure / id of message
39
-	 */
40
-	public function ImportMessageChange($id, $message);
30
+    /**
31
+     * Imports a single message.
32
+     *
33
+     * @param string     $id
34
+     * @param SyncObject $message
35
+     *
36
+     * @throws StatusException
37
+     *
38
+     * @return boolean/string               failure / id of message
39
+     */
40
+    public function ImportMessageChange($id, $message);
41 41
 
42
-	/**
43
-	 * Imports a deletion. This may conflict if the local object has been modified.
44
-	 *
45
-	 * @param string $id
46
-	 * @param bool   $asSoftDelete (opt) if true, the deletion is exported as "SoftDelete", else as "Remove" - default: false
47
-	 *
48
-	 * @return bool
49
-	 */
50
-	public function ImportMessageDeletion($id, $asSoftDelete = false);
42
+    /**
43
+     * Imports a deletion. This may conflict if the local object has been modified.
44
+     *
45
+     * @param string $id
46
+     * @param bool   $asSoftDelete (opt) if true, the deletion is exported as "SoftDelete", else as "Remove" - default: false
47
+     *
48
+     * @return bool
49
+     */
50
+    public function ImportMessageDeletion($id, $asSoftDelete = false);
51 51
 
52
-	/**
53
-	 * Imports a change in 'read' flag
54
-	 * This can never conflict.
55
-	 *
56
-	 * @param string $id
57
-	 * @param int    $flags
58
-	 * @param array  $categories
59
-	 *
60
-	 * @throws StatusException
61
-	 *
62
-	 * @return bool
63
-	 */
64
-	public function ImportMessageReadFlag($id, $flags, $categories = []);
52
+    /**
53
+     * Imports a change in 'read' flag
54
+     * This can never conflict.
55
+     *
56
+     * @param string $id
57
+     * @param int    $flags
58
+     * @param array  $categories
59
+     *
60
+     * @throws StatusException
61
+     *
62
+     * @return bool
63
+     */
64
+    public function ImportMessageReadFlag($id, $flags, $categories = []);
65 65
 
66
-	/**
67
-	 * Imports a move of a message. This occurs when a user moves an item to another folder.
68
-	 *
69
-	 * @param string $id
70
-	 * @param string $newfolder destination folder
71
-	 *
72
-	 * @throws StatusException
73
-	 *
74
-	 * @return bool
75
-	 */
76
-	public function ImportMessageMove($id, $newfolder);
66
+    /**
67
+     * Imports a move of a message. This occurs when a user moves an item to another folder.
68
+     *
69
+     * @param string $id
70
+     * @param string $newfolder destination folder
71
+     *
72
+     * @throws StatusException
73
+     *
74
+     * @return bool
75
+     */
76
+    public function ImportMessageMove($id, $newfolder);
77 77
 
78
-	/*----------------------------------------------------------------------------------------------------------
78
+    /*----------------------------------------------------------------------------------------------------------
79 79
 	 * Methods to import hierarchy
80 80
 	 */
81 81
 
82
-	/**
83
-	 * Imports a change on a folder.
84
-	 *
85
-	 * @param object $folder SyncFolder
86
-	 *
87
-	 * @throws StatusException
88
-	 *
89
-	 * @return boolean/SyncObject           status/object with the ath least the serverid of the folder set
90
-	 */
91
-	public function ImportFolderChange($folder);
82
+    /**
83
+     * Imports a change on a folder.
84
+     *
85
+     * @param object $folder SyncFolder
86
+     *
87
+     * @throws StatusException
88
+     *
89
+     * @return boolean/SyncObject           status/object with the ath least the serverid of the folder set
90
+     */
91
+    public function ImportFolderChange($folder);
92 92
 
93
-	/**
94
-	 * Imports a folder deletion.
95
-	 *
96
-	 * @param SyncFolder $folder at least "serverid" needs to be set
97
-	 *
98
-	 * @throws StatusException
99
-	 *
100
-	 * @return boolean/int  success/SYNC_FOLDERHIERARCHY_STATUS
101
-	 */
102
-	public function ImportFolderDeletion($folder);
93
+    /**
94
+     * Imports a folder deletion.
95
+     *
96
+     * @param SyncFolder $folder at least "serverid" needs to be set
97
+     *
98
+     * @throws StatusException
99
+     *
100
+     * @return boolean/int  success/SYNC_FOLDERHIERARCHY_STATUS
101
+     */
102
+    public function ImportFolderDeletion($folder);
103 103
 }
Please login to merge, or discard this patch.
lib/interface/istatemachine.php 1 patch
Indentation   +113 added lines, -113 removed lines patch added patch discarded remove patch
@@ -15,127 +15,127 @@
 block discarded – undo
15 15
  */
16 16
 
17 17
 interface IStateMachine {
18
-	public const DEFTYPE = "";
19
-	public const DEVICEDATA = "devicedata";
20
-	public const FOLDERDATA = "fd";
21
-	public const FAILSAVE = "fs";
22
-	public const HIERARCHY = "hc";
23
-	public const BACKENDSTORAGE = "bs";
18
+    public const DEFTYPE = "";
19
+    public const DEVICEDATA = "devicedata";
20
+    public const FOLDERDATA = "fd";
21
+    public const FAILSAVE = "fs";
22
+    public const HIERARCHY = "hc";
23
+    public const BACKENDSTORAGE = "bs";
24 24
 
25
-	public const STATEVERSION_01 = "1";
26
-	public const STATEVERSION_02 = "2";
25
+    public const STATEVERSION_01 = "1";
26
+    public const STATEVERSION_02 = "2";
27 27
 
28
-	/**
29
-	 * Constructor.
30
-	 *
31
-	 * @param mixed $devid
32
-	 * @param mixed $type
33
-	 * @param mixed $key
34
-	 * @param mixed $counter
35
-	 *
36
-	 * @throws FatalMisconfigurationException
37
-	 */
28
+    /**
29
+     * Constructor.
30
+     *
31
+     * @param mixed $devid
32
+     * @param mixed $type
33
+     * @param mixed $key
34
+     * @param mixed $counter
35
+     *
36
+     * @throws FatalMisconfigurationException
37
+     */
38 38
 
39
-	/**
40
-	 * Gets a hash value indicating the latest dataset of the named
41
-	 * state with a specified key and counter.
42
-	 * If the state is changed between two calls of this method
43
-	 * the returned hash should be different.
44
-	 *
45
-	 * @param string $devid   the device id
46
-	 * @param string $type    the state type
47
-	 * @param string $key     (opt)
48
-	 * @param string $counter (opt)
49
-	 *
50
-	 * @throws StateNotFoundException, StateInvalidException, UnavailableException
51
-	 *
52
-	 * @return string
53
-	 */
54
-	public function GetStateHash($devid, $type, $key = false, $counter = false);
39
+    /**
40
+     * Gets a hash value indicating the latest dataset of the named
41
+     * state with a specified key and counter.
42
+     * If the state is changed between two calls of this method
43
+     * the returned hash should be different.
44
+     *
45
+     * @param string $devid   the device id
46
+     * @param string $type    the state type
47
+     * @param string $key     (opt)
48
+     * @param string $counter (opt)
49
+     *
50
+     * @throws StateNotFoundException, StateInvalidException, UnavailableException
51
+     *
52
+     * @return string
53
+     */
54
+    public function GetStateHash($devid, $type, $key = false, $counter = false);
55 55
 
56
-	/**
57
-	 * Gets a state for a specified key and counter.
58
-	 * This method should call IStateMachine->CleanStates()
59
-	 * to remove older states (same key, previous counters).
60
-	 *
61
-	 * @param string $devid       the device id
62
-	 * @param string $type        the state type
63
-	 * @param string $key         (opt)
64
-	 * @param string $counter     (opt)
65
-	 * @param string $cleanstates (opt)
66
-	 *
67
-	 * @throws StateNotFoundException, StateInvalidException, UnavailableException
68
-	 *
69
-	 * @return mixed
70
-	 */
71
-	public function GetState($devid, $type, $key = false, $counter = false, $cleanstates = true);
56
+    /**
57
+     * Gets a state for a specified key and counter.
58
+     * This method should call IStateMachine->CleanStates()
59
+     * to remove older states (same key, previous counters).
60
+     *
61
+     * @param string $devid       the device id
62
+     * @param string $type        the state type
63
+     * @param string $key         (opt)
64
+     * @param string $counter     (opt)
65
+     * @param string $cleanstates (opt)
66
+     *
67
+     * @throws StateNotFoundException, StateInvalidException, UnavailableException
68
+     *
69
+     * @return mixed
70
+     */
71
+    public function GetState($devid, $type, $key = false, $counter = false, $cleanstates = true);
72 72
 
73
-	/**
74
-	 * Writes ta state to for a key and counter.
75
-	 *
76
-	 * @param mixed  $state
77
-	 * @param string $devid   the device id
78
-	 * @param string $type    the state type
79
-	 * @param string $key     (opt)
80
-	 * @param int    $counter (opt)
81
-	 *
82
-	 * @throws StateInvalidException, UnavailableException
83
-	 *
84
-	 * @return bool
85
-	 */
86
-	public function SetState($state, $devid, $type, $key = false, $counter = false);
73
+    /**
74
+     * Writes ta state to for a key and counter.
75
+     *
76
+     * @param mixed  $state
77
+     * @param string $devid   the device id
78
+     * @param string $type    the state type
79
+     * @param string $key     (opt)
80
+     * @param int    $counter (opt)
81
+     *
82
+     * @throws StateInvalidException, UnavailableException
83
+     *
84
+     * @return bool
85
+     */
86
+    public function SetState($state, $devid, $type, $key = false, $counter = false);
87 87
 
88
-	/**
89
-	 * Cleans up all older states.
90
-	 * If called with a $counter, all states previous state counter can be removed.
91
-	 * If additionally the $thisCounterOnly flag is true, only that specific counter will be removed.
92
-	 * If called without $counter, all keys (independently from the counter) can be removed.
93
-	 *
94
-	 * @param string $devid           the device id
95
-	 * @param string $type            the state type
96
-	 * @param string $key
97
-	 * @param string $counter         (opt)
98
-	 * @param string $thisCounterOnly (opt) if provided, the exact counter only will be removed
99
-	 *
100
-	 * @throws StateInvalidException
101
-	 *
102
-	 * @return
103
-	 */
104
-	public function CleanStates($devid, $type, $key, $counter = false, $thisCounterOnly = false);
88
+    /**
89
+     * Cleans up all older states.
90
+     * If called with a $counter, all states previous state counter can be removed.
91
+     * If additionally the $thisCounterOnly flag is true, only that specific counter will be removed.
92
+     * If called without $counter, all keys (independently from the counter) can be removed.
93
+     *
94
+     * @param string $devid           the device id
95
+     * @param string $type            the state type
96
+     * @param string $key
97
+     * @param string $counter         (opt)
98
+     * @param string $thisCounterOnly (opt) if provided, the exact counter only will be removed
99
+     *
100
+     * @throws StateInvalidException
101
+     *
102
+     * @return
103
+     */
104
+    public function CleanStates($devid, $type, $key, $counter = false, $thisCounterOnly = false);
105 105
 
106
-	/**
107
-	 * Links a user to a device.
108
-	 *
109
-	 * @param string $username
110
-	 * @param string $devid
111
-	 *
112
-	 * @return bool indicating if the user was added or not (existed already)
113
-	 */
114
-	public function LinkUserDevice($username, $devid);
106
+    /**
107
+     * Links a user to a device.
108
+     *
109
+     * @param string $username
110
+     * @param string $devid
111
+     *
112
+     * @return bool indicating if the user was added or not (existed already)
113
+     */
114
+    public function LinkUserDevice($username, $devid);
115 115
 
116
-	/**
117
-	 * Unlinks a device from a user.
118
-	 *
119
-	 * @param string $username
120
-	 * @param string $devid
121
-	 *
122
-	 * @return bool
123
-	 */
124
-	public function UnLinkUserDevice($username, $devid);
116
+    /**
117
+     * Unlinks a device from a user.
118
+     *
119
+     * @param string $username
120
+     * @param string $devid
121
+     *
122
+     * @return bool
123
+     */
124
+    public function UnLinkUserDevice($username, $devid);
125 125
 
126
-	/**
127
-	 * Returns the current version of the state files.
128
-	 *
129
-	 * @return int
130
-	 */
131
-	public function GetStateVersion();
126
+    /**
127
+     * Returns the current version of the state files.
128
+     *
129
+     * @return int
130
+     */
131
+    public function GetStateVersion();
132 132
 
133
-	/**
134
-	 * Sets the current version of the state files.
135
-	 *
136
-	 * @param int $version the new supported version
137
-	 *
138
-	 * @return bool
139
-	 */
140
-	public function SetStateVersion($version);
133
+    /**
134
+     * Sets the current version of the state files.
135
+     *
136
+     * @param int $version the new supported version
137
+     *
138
+     * @return bool
139
+     */
140
+    public function SetStateVersion($version);
141 141
 }
Please login to merge, or discard this patch.
lib/interface/iexportchanges.php 1 patch
Indentation   +23 added lines, -23 removed lines patch added patch discarded remove patch
@@ -10,29 +10,29 @@
 block discarded – undo
10 10
  */
11 11
 
12 12
 interface IExportChanges extends IChanges {
13
-	/**
14
-	 * Sets the importer where the exporter will sent its changes to
15
-	 * This exporter should also be ready to accept calls after this.
16
-	 *
17
-	 * @param object &$importer Implementation of IImportChanges
18
-	 *
19
-	 * @throws StatusException
20
-	 *
21
-	 * @return bool
22
-	 */
23
-	public function InitializeExporter(&$importer);
13
+    /**
14
+     * Sets the importer where the exporter will sent its changes to
15
+     * This exporter should also be ready to accept calls after this.
16
+     *
17
+     * @param object &$importer Implementation of IImportChanges
18
+     *
19
+     * @throws StatusException
20
+     *
21
+     * @return bool
22
+     */
23
+    public function InitializeExporter(&$importer);
24 24
 
25
-	/**
26
-	 * Returns the amount of changes to be exported.
27
-	 *
28
-	 * @return int
29
-	 */
30
-	public function GetChangeCount();
25
+    /**
26
+     * Returns the amount of changes to be exported.
27
+     *
28
+     * @return int
29
+     */
30
+    public function GetChangeCount();
31 31
 
32
-	/**
33
-	 * Synchronizes a change to the configured importer.
34
-	 *
35
-	 * @return array with status information
36
-	 */
37
-	public function Synchronize();
32
+    /**
33
+     * Synchronizes a change to the configured importer.
34
+     *
35
+     * @return array with status information
36
+     */
37
+    public function Synchronize();
38 38
 }
Please login to merge, or discard this patch.