Passed
Push — master ( 34e8da...f497d2 )
by
unknown
06:10 queued 02:50
created
lib/grommunio/grommunio.php 1 patch
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.
lib/grommunio/mapistreamwrapper.php 1 patch
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.
lib/grommunio/exporter.php 1 patch
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.
lib/grommunio/mapiprovider.php 1 patch
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.
lib/grommunio/importer.php 1 patch
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.
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.