Passed
Push — master ( 83d80d...4ee882 )
by
unknown
09:16 queued 12s
created

Grommunio::Logon()   F

Complexity

Conditions 15
Paths 296

Size

Total Lines 90
Code Lines 47

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 15
eloc 47
nc 296
nop 3
dl 0
loc 90
rs 3.8833
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/*
3
 * SPDX-License-Identifier: AGPL-3.0-only
4
 * SPDX-FileCopyrightText: Copyright 2007-2016 Zarafa Deutschland GmbH
5
 * SPDX-FileCopyrightText: Copyright 2020-2022 grommunio GmbH
6
 *
7
 * This is a backend for grommunio. It is an implementation of IBackend and also
8
 * implements ISearchProvider to search in the grommunio system. The backend
9
 * implements IStateMachine as well to save the devices' information in the
10
 * user's store and extends InterProcessData to access Redis.
11
 */
12
13
// include PHP-MAPI classes
14
include_once 'mapi/mapi.util.php';
15
include_once 'mapi/mapidefs.php';
16
include_once 'mapi/mapitags.php';
17
include_once 'mapi/mapiguid.php';
18
19
// setlocale to UTF-8 in order to support properties containing Unicode characters
20
setlocale(LC_CTYPE, "en_US.UTF-8");
21
22
class Grommunio extends InterProcessData implements IBackend, ISearchProvider, IStateMachine {
23
	private $mainUser;
24
	private $session;
25
	private $defaultstore;
26
	private $store;
27
	private $storeName;
28
	private $storeCache;
29
	private $notifications;
30
	private $changesSink;
31
	private $changesSinkFolders;
32
	private $changesSinkHierarchyHash;
33
	private $changesSinkStores;
34
	private $wastebasket;
35
	private $addressbook;
36
	private $folderStatCache;
37
	private $impersonateUser;
38
	private $stateFolder;
39
	private $userDeviceData;
40
41
	// KC config parameter for PR_EC_ENABLED_FEATURES / PR_EC_DISABLED_FEATURES
42
	public const MOBILE_ENABLED = 'mobile';
43
44
	public const MAXAMBIGUOUSRECIPIENTS = 9999;
45
	public const FREEBUSYENUMBLOCKS = 50;
46
	public const MAXFREEBUSYSLOTS = 32767; // max length of 32k for the MergedFreeBusy element is allowed
47
	public const HALFHOURSECONDS = 1800;
48
49
	/**
50
	 * Constructor of the grommunio Backend.
51
	 */
52
	public function __construct() {
53
		$this->session = false;
54
		$this->store = false;
55
		$this->storeName = false;
56
		$this->storeCache = [];
57
		$this->notifications = false;
58
		$this->changesSink = false;
59
		$this->changesSinkFolders = [];
60
		$this->changesSinkStores = [];
61
		$this->changesSinkHierarchyHash = false;
62
		$this->wastebasket = false;
63
		$this->session = false;
64
		$this->folderStatCache = [];
65
		$this->impersonateUser = false;
66
		$this->stateFolder = null;
67
68
		SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio using PHP-MAPI version: %s - PHP version: %s", phpversion("mapi"), phpversion()));
69
70
		# Interprocessdata
71
		$this->allocate = 0;
72
		$this->type = "grommunio-sync:userdevices";
73
		$this->userDeviceData = "grommunio-sync:statefoldercache";
74
		parent::__construct();
75
	}
76
77
	/**
78
	 * Indicates which StateMachine should be used.
79
	 *
80
	 * @return bool Grommunio uses own state machine
81
	 */
82
	public function GetStateMachine() {
83
		return $this;
84
	}
85
86
	/**
87
	 * Returns the Grommunio as it implements the ISearchProvider interface
88
	 * This could be overwritten by the global configuration.
89
	 *
90
	 * @return object Implementation of ISearchProvider
91
	 */
92
	public function GetSearchProvider() {
93
		return $this;
94
	}
95
96
	/**
97
	 * Indicates which AS version is supported by the backend.
98
	 *
99
	 * @return string AS version constant
100
	 */
101
	public function GetSupportedASVersion() {
102
		return GSync::ASV_141;
103
	}
104
105
	/**
106
	 * Authenticates the user with the configured grommunio server.
107
	 *
108
	 * @param string $username
109
	 * @param string $domain
110
	 * @param string $password
111
	 * @param mixed  $user
112
	 * @param mixed  $pass
113
	 *
114
	 * @throws AuthenticationRequiredException
115
	 *
116
	 * @return bool
117
	 */
118
	public function Logon($user, $domain, $pass) {
119
		SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->Logon(): Trying to authenticate user '%s'..", $user));
120
121
		$this->mainUser = strtolower($user);
122
		// TODO the impersonated user should be passed directly to IBackend->Logon() - ZP-1351
123
		if (Request::GetImpersonatedUser()) {
124
			$this->impersonateUser = strtolower(Request::GetImpersonatedUser());
125
		}
126
127
		// check if we are impersonating someone
128
		// $defaultUser will be used for $this->defaultStore
129
		if ($this->impersonateUser !== false) {
130
			SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->Logon(): Impersonation active - authenticating: '%s' - impersonating '%s'", $this->mainUser, $this->impersonateUser));
131
			$defaultUser = $this->impersonateUser;
132
		}
133
		else {
134
			$defaultUser = $this->mainUser;
135
		}
136
137
		$deviceId = Request::GetDeviceID();
138
139
		try {
140
			// check if notifications are available in php-mapi
141
			if (function_exists('mapi_feature') && mapi_feature('LOGONFLAGS')) {
142
				// send grommunio-sync version and user agent to ZCP - ZP-589
143
				if (Utils::CheckMapiExtVersion('7.2.0')) {
144
					$gsync_version = 'Grommunio-Sync_' . @constant('GROMMUNIOSYNC_VERSION');
145
					$user_agent = ($deviceId) ? GSync::GetDeviceManager()->GetUserAgent() : "unknown";
146
					$this->session = @mapi_logon_zarafa($this->mainUser, $pass, MAPI_SERVER, null, null, 0, $gsync_version, $user_agent);
147
				}
148
				else {
149
					$this->session = @mapi_logon_zarafa($this->mainUser, $pass, MAPI_SERVER, null, null, 0);
150
				}
151
				$this->notifications = true;
152
			}
153
			// old fashioned session
154
			else {
155
				$this->session = @mapi_logon_zarafa($this->mainUser, $pass, MAPI_SERVER);
156
				$this->notifications = false;
157
			}
158
159
			if (mapi_last_hresult()) {
160
				SLog::Write(LOGLEVEL_ERROR, sprintf("Grommunio->Logon(): login failed with error code: 0x%X", mapi_last_hresult()));
161
				if (mapi_last_hresult() == MAPI_E_NETWORK_ERROR) {
162
					throw new ServiceUnavailableException("Error connecting to KC (login)");
163
				}
164
			}
165
		}
166
		catch (MAPIException $ex) {
167
			throw new AuthenticationRequiredException($ex->getDisplayMessage());
168
		}
169
170
		if (!$this->session) {
171
			SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->Logon(): logon failed for user '%s'", $this->mainUser));
172
			$this->defaultstore = false;
173
174
			return false;
175
		}
176
177
		// Get/open default store
178
		$this->defaultstore = $this->openMessageStore($this->mainUser);
179
180
		// To impersonate, we overwrite the defaultstore. We still need to open it before we can do that.
181
		if ($this->impersonateUser) {
182
			SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->Logon(): Impersonating user '%s'", $defaultUser));
183
			$this->defaultstore = $this->openMessageStore($defaultUser);
184
		}
185
186
		if (mapi_last_hresult() == MAPI_E_FAILONEPROVIDER) {
187
			throw new ServiceUnavailableException("Error connecting to KC (open store)");
188
		}
189
190
		if ($this->defaultstore === false) {
191
			throw new AuthenticationRequiredException(sprintf("Grommunio->Logon(): User '%s' has no default store", $defaultUser));
192
		}
193
194
		$this->store = $this->defaultstore;
195
		$this->storeName = $defaultUser;
196
197
		SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->Logon(): User '%s' is authenticated%s", $this->mainUser, ($this->impersonateUser ? " impersonating '" . $this->impersonateUser . "'" : '')));
198
199
		$this->isGSyncEnabled();
200
201
		// check if this is a Zarafa 7 store with unicode support
202
		MAPIUtils::IsUnicodeStore($this->store);
203
204
		// open the state folder
205
		$this->getStateFolder($deviceId);
206
207
		return true;
208
	}
209
210
	/**
211
	 * Setup the backend to work on a specific store or checks ACLs there.
212
	 * If only the $store is submitted, all Import/Export/Fetch/Etc operations should be
213
	 * performed on this store (switch operations store).
214
	 * If the ACL check is enabled, this operation should just indicate the ACL status on
215
	 * the submitted store, without changing the store for operations.
216
	 * For the ACL status, the currently logged on user MUST have access rights on
217
	 *  - the entire store - admin access if no folderid is sent, or
218
	 *  - on a specific folderid in the store (secretary/full access rights).
219
	 *
220
	 * The ACLcheck MUST fail if a folder of the authenticated user is checked!
221
	 *
222
	 * @param string $store        target store, could contain a "domain\user" value
223
	 * @param bool   $checkACLonly if set to true, Setup() should just check ACLs
224
	 * @param string $folderid     if set, only ACLs on this folderid are relevant
225
	 *
226
	 * @return bool
227
	 */
228
	public function Setup($store, $checkACLonly = false, $folderid = false) {
229
		list($user, $domain) = Utils::SplitDomainUser($store);
230
231
		if (!isset($this->mainUser)) {
232
			return false;
233
		}
234
235
		$mainUser = $this->mainUser;
236
		// when impersonating we need to check against the impersonated user
237
		if ($this->impersonateUser) {
238
			$mainUser = $this->impersonateUser;
239
		}
240
241
		if ($user === false) {
242
			$user = $mainUser;
243
		}
244
245
		// This is a special case. A user will get his entire folder structure by the foldersync by default.
246
		// The ACL check is executed when an additional folder is going to be sent to the mobile.
247
		// Configured that way the user could receive the same folderid twice, with two different names.
248
		if ($mainUser == $user && $checkACLonly && $folderid && !$this->impersonateUser) {
249
			SLog::Write(LOGLEVEL_DEBUG, "Grommunio->Setup(): Checking ACLs for folder of the users defaultstore. Fail is forced to avoid folder duplications on mobile.");
250
251
			return false;
252
		}
253
254
		// get the users store
255
		$userstore = $this->openMessageStore($user);
256
257
		// only proceed if a store was found, else return false
258
		if ($userstore) {
259
			// only check permissions
260
			if ($checkACLonly === true) {
261
				// check for admin rights
262
				if (!$folderid) {
263
					if ($user != $this->mainUser) {
264
						if ($this->impersonateUser) {
265
							$storeProps = mapi_getprops($userstore, [PR_IPM_SUBTREE_ENTRYID]);
266
							$rights = $this->HasSecretaryACLs($userstore, '', $storeProps[PR_IPM_SUBTREE_ENTRYID]);
267
							SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->Setup(): Checking for secretary ACLs on root folder of impersonated store '%s': '%s'", $user, Utils::PrintAsString($rights)));
268
						}
269
						else {
270
							$zarafauserinfo = @nsp_getuserinfo($this->mainUser);
271
							$rights = (isset($zarafauserinfo['admin']) && $zarafauserinfo['admin']) ? true : false;
272
							SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->Setup(): Checking for admin ACLs on store '%s': '%s'", $user, Utils::PrintAsString($rights)));
273
						}
274
					}
275
					// the user has always full access to his own store
276
					else {
277
						$rights = true;
278
						SLog::Write(LOGLEVEL_DEBUG, "Grommunio->Setup(): the user has always full access to his own store");
279
					}
280
281
					return $rights;
282
				}
283
				// check permissions on this folder
284
285
				$rights = $this->HasSecretaryACLs($userstore, $folderid);
286
				SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->Setup(): Checking for secretary ACLs on '%s' of store '%s': '%s'", $folderid, $user, Utils::PrintAsString($rights)));
287
288
				return $rights;
289
			}
290
291
			// switch operations store
292
			// this should also be done if called with user = mainuser or user = false
293
			// which means to switch back to the default store
294
295
			// switch active store
296
			$this->store = $userstore;
297
			$this->storeName = $user;
298
299
			return true;
300
		}
301
302
		return false;
303
	}
304
305
	/**
306
	 * Logs off
307
	 * Free/Busy information is updated for modified calendars
308
	 * This is done after the synchronization process is completed.
309
	 *
310
	 * @return bool
311
	 */
312
	public function Logoff() {
313
		return true;
314
	}
315
316
	/**
317
	 * Returns an array of SyncFolder types with the entire folder hierarchy
318
	 * on the server (the array itself is flat, but refers to parents via the 'parent' property.
319
	 *
320
	 * provides AS 1.0 compatibility
321
	 *
322
	 * @return array SYNC_FOLDER
323
	 */
324
	public function GetHierarchy() {
325
		$folders = [];
326
		$mapiprovider = new MAPIProvider($this->session, $this->store);
327
		$storeProps = $mapiprovider->GetStoreProps();
328
329
		// for SYSTEM user open the public folders
330
		if (strtoupper($this->storeName) == "SYSTEM") {
331
			$rootfolder = mapi_msgstore_openentry($this->store, $storeProps[PR_IPM_PUBLIC_FOLDERS_ENTRYID]);
332
		}
333
		else {
334
			$rootfolder = mapi_msgstore_openentry($this->store);
335
		}
336
337
		$rootfolderprops = mapi_getprops($rootfolder, [PR_SOURCE_KEY]);
338
339
		$hierarchy = mapi_folder_gethierarchytable($rootfolder, CONVENIENT_DEPTH);
340
		$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]);
341
		SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetHierarchy(): fetched %d folders from MAPI", count($rows)));
342
343
		foreach ($rows as $row) {
344
			// do not display hidden and search folders
345
			if ((isset($row[PR_ATTR_HIDDEN]) && $row[PR_ATTR_HIDDEN]) ||
346
				(isset($row[PR_FOLDER_TYPE]) && $row[PR_FOLDER_TYPE] == FOLDER_SEARCH) ||
347
				// for SYSTEM user $row[PR_PARENT_SOURCE_KEY] == $rootfolderprops[PR_SOURCE_KEY] is true, but we need those folders
348
				(isset($row[PR_PARENT_SOURCE_KEY]) && $row[PR_PARENT_SOURCE_KEY] == $rootfolderprops[PR_SOURCE_KEY] && strtoupper($this->storeName) != "SYSTEM")) {
349
				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")));
350
351
				continue;
352
			}
353
			$folder = $mapiprovider->GetFolder($row);
354
			if ($folder) {
355
				$folders[] = $folder;
356
			}
357
			else {
358
				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")));
359
			}
360
		}
361
362
		SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetHierarchy(): processed %d folders, starting parent remap", count($folders)));
363
		// reloop the folders to make sure all parentids are mapped correctly
364
		$dm = GSync::GetDeviceManager();
365
		foreach ($folders as $folder) {
366
			if ($folder->parentid !== "0") {
367
				// SYSTEM user's parentid points to $rootfolderprops[PR_SOURCE_KEY], but they need to be on the top level
368
				$folder->parentid = (strtoupper($this->storeName) == "SYSTEM" && $folder->parentid == bin2hex($rootfolderprops[PR_SOURCE_KEY])) ? '0' : $dm->GetFolderIdForBackendId($folder->parentid);
369
			}
370
		}
371
372
		return $folders;
373
	}
374
375
	/**
376
	 * Returns the importer to process changes from the mobile
377
	 * If no $folderid is given, hierarchy importer is expected.
378
	 *
379
	 * @param string $folderid (opt)
380
	 *
381
	 * @return object(ImportChanges)
382
	 */
383
	public function GetImporter($folderid = false) {
384
		SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetImporter() folderid: '%s'", Utils::PrintAsString($folderid)));
385
		if ($folderid !== false) {
386
			// check if the user of the current store has permissions to import to this folderid
387
			if ($this->storeName != $this->mainUser && !$this->hasSecretaryACLs($this->store, $folderid)) {
388
				SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetImporter(): missing permissions on folderid: '%s'.", Utils::PrintAsString($folderid)));
389
390
				return false;
391
			}
392
393
			return new ImportChangesICS($this->session, $this->store, hex2bin($folderid));
394
		}
395
396
		return new ImportChangesICS($this->session, $this->store);
397
	}
398
399
	/**
400
	 * Returns the exporter to send changes to the mobile
401
	 * If no $folderid is given, hierarchy exporter is expected.
402
	 *
403
	 * @param string $folderid (opt)
404
	 *
405
	 * @throws StatusException
406
	 *
407
	 * @return object(ExportChanges)
408
	 */
409
	public function GetExporter($folderid = false) {
410
		if ($folderid !== false) {
411
			// check if the user of the current store has permissions to export from this folderid
412
			if ($this->storeName != $this->mainUser && !$this->hasSecretaryACLs($this->store, $folderid)) {
413
				SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetExporter(): missing permissions on folderid: '%s'.", Utils::PrintAsString($folderid)));
414
415
				return false;
416
			}
417
418
			return new ExportChangesICS($this->session, $this->store, hex2bin($folderid));
419
		}
420
421
		return new ExportChangesICS($this->session, $this->store);
422
	}
423
424
	/**
425
	 * Sends an e-mail
426
	 * This messages needs to be saved into the 'sent items' folder.
427
	 *
428
	 * @param SyncSendMail $sm SyncSendMail object
429
	 *
430
	 * @throws StatusException
431
	 *
432
	 * @return bool
433
	 */
434
	public function SendMail($sm) {
435
		// Check if imtomapi function is available and use it to send the mime message.
436
		// It is available since ZCP 7.0.6
437
		// @see http://jira.zarafa.com/browse/ZCP-9508
438
		if (!(function_exists('mapi_feature') && mapi_feature('INETMAPI_IMTOMAPI'))) {
439
			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);
440
441
			return false;
442
		}
443
		$mimeLength = strlen($sm->mime);
444
		SLog::Write(LOGLEVEL_DEBUG, sprintf(
445
			"Grommunio->SendMail(): RFC822: %d bytes  forward-id: '%s' reply-id: '%s' parent-id: '%s' SaveInSent: '%s' ReplaceMIME: '%s'",
446
			$mimeLength,
447
			Utils::PrintAsString($sm->forwardflag),
448
			Utils::PrintAsString($sm->replyflag),
449
			Utils::PrintAsString((isset($sm->source->folderid) ? $sm->source->folderid : false)),
450
			Utils::PrintAsString(($sm->saveinsent)),
451
			Utils::PrintAsString(isset($sm->replacemime))
452
		));
453
		if ($mimeLength == 0) {
454
			throw new StatusException("Grommunio->SendMail(): empty mail data", SYNC_COMMONSTATUS_MAILSUBMISSIONFAILED);
455
		}
456
457
		$sendMailProps = MAPIMapping::GetSendMailProperties();
458
		$sendMailProps = getPropIdsFromStrings($this->defaultstore, $sendMailProps);
459
460
		// Open the outbox and create the message there
461
		$storeprops = mapi_getprops($this->defaultstore, [$sendMailProps["outboxentryid"], $sendMailProps["ipmsentmailentryid"]]);
462
		if (isset($storeprops[$sendMailProps["outboxentryid"]])) {
463
			$outbox = mapi_msgstore_openentry($this->defaultstore, $storeprops[$sendMailProps["outboxentryid"]]);
464
		}
465
466
		if (!$outbox) {
467
			throw new StatusException(sprintf("Grommunio->SendMail(): No Outbox found or unable to create message: 0x%X", mapi_last_hresult()), SYNC_COMMONSTATUS_SERVERERROR);
468
		}
469
470
		$mapimessage = mapi_folder_createmessage($outbox);
471
472
		// message properties to be set
473
		$mapiprops = [];
474
		// only save the outgoing in sent items folder if the mobile requests it
475
		$mapiprops[$sendMailProps["sentmailentryid"]] = $storeprops[$sendMailProps["ipmsentmailentryid"]];
476
477
		SLog::Write(LOGLEVEL_DEBUG, "Grommunio->SendMail(): Use the mapi_inetmapi_imtomapi function");
478
		$ab = mapi_openaddressbook($this->session);
479
		mapi_inetmapi_imtomapi($this->session, $this->defaultstore, $ab, $mapimessage, $sm->mime, []);
480
481
		// Set the appSeqNr so that tracking tab can be updated for meeting request updates
482
		// @see http://jira.zarafa.com/browse/ZP-68
483
		$meetingRequestProps = MAPIMapping::GetMeetingRequestProperties();
484
		$meetingRequestProps = getPropIdsFromStrings($this->defaultstore, $meetingRequestProps);
485
		$props = mapi_getprops($mapimessage, [PR_MESSAGE_CLASS, $meetingRequestProps["goidtag"], $sendMailProps["internetcpid"], $sendMailProps["body"], $sendMailProps["html"], $sendMailProps["rtf"], $sendMailProps["rtfinsync"]]);
486
487
		// Convert sent message's body to UTF-8 if it was a HTML message.
488
		// @see http://jira.zarafa.com/browse/ZP-505 and http://jira.zarafa.com/browse/ZP-555
489
		if (isset($props[$sendMailProps["internetcpid"]]) && $props[$sendMailProps["internetcpid"]] != INTERNET_CPID_UTF8 && MAPIUtils::GetNativeBodyType($props) == SYNC_BODYPREFERENCE_HTML) {
490
			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"]]));
491
			$mapiprops[$sendMailProps["internetcpid"]] = INTERNET_CPID_UTF8;
492
493
			$bodyHtml = MAPIUtils::readPropStream($mapimessage, PR_HTML);
494
			$bodyHtml = Utils::ConvertCodepageStringToUtf8($props[$sendMailProps["internetcpid"]], $bodyHtml);
495
			$mapiprops[$sendMailProps["html"]] = $bodyHtml;
496
497
			mapi_setprops($mapimessage, $mapiprops);
498
		}
499
		if (stripos($props[PR_MESSAGE_CLASS], "IPM.Schedule.Meeting.Resp.") === 0) {
500
			// search for calendar items using goid
501
			$mr = new Meetingrequest($this->defaultstore, $mapimessage);
502
			$appointments = $mr->findCalendarItems($props[$meetingRequestProps["goidtag"]]);
503
			if (is_array($appointments) && !empty($appointments)) {
504
				$app = mapi_msgstore_openentry($this->defaultstore, $appointments[0]);
505
				$appprops = mapi_getprops($app, [$meetingRequestProps["appSeqNr"]]);
506
				if (isset($appprops[$meetingRequestProps["appSeqNr"]]) && $appprops[$meetingRequestProps["appSeqNr"]]) {
507
					$mapiprops[$meetingRequestProps["appSeqNr"]] = $appprops[$meetingRequestProps["appSeqNr"]];
508
					SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->SendMail(): Set sequence number to:%d", $appprops[$meetingRequestProps["appSeqNr"]]));
509
				}
510
			}
511
		}
512
513
		// Delete the PR_SENT_REPRESENTING_* properties because some android devices
514
		// do not send neither From nor Sender header causing empty PR_SENT_REPRESENTING_NAME and
515
		// PR_SENT_REPRESENTING_EMAIL_ADDRESS properties and "broken" PR_SENT_REPRESENTING_ENTRYID
516
		// which results in spooler not being able to send the message.
517
		// @see http://jira.zarafa.com/browse/ZP-85
518
		mapi_deleteprops(
519
			$mapimessage,
520
			[
521
				$sendMailProps["sentrepresentingname"],
522
				$sendMailProps["sentrepresentingemail"],
523
				$sendMailProps["representingentryid"],
524
				$sendMailProps["sentrepresentingaddt"],
525
				$sendMailProps["sentrepresentinsrchk"],
526
			]
527
		);
528
529
		if (isset($sm->source->itemid) && $sm->source->itemid) {
530
			// answering an email in a public/shared folder
531
			// 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)
532
			if (!$this->Setup(GSync::GetAdditionalSyncFolderStore($sm->source->folderid))) {
533
				throw new StatusException(sprintf("Grommunio->SendMail() could not Setup() the backend for folder id '%s'", $sm->source->folderid), SYNC_COMMONSTATUS_SERVERERROR);
534
			}
535
536
			$entryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($sm->source->folderid), hex2bin($sm->source->itemid));
537
			if ($entryid) {
538
				$fwmessage = mapi_msgstore_openentry($this->store, $entryid);
539
			}
540
541
			if (isset($fwmessage) && $fwmessage) {
542
				// update icon and last_verb when forwarding or replying message
543
				// reply-all (verb 103) is not supported, as we cannot really detect this case
544
				if ($sm->forwardflag) {
545
					$updateProps = [
546
						PR_ICON_INDEX => 262,
547
						PR_LAST_VERB_EXECUTED => 104,
548
					];
549
				}
550
				elseif ($sm->replyflag) {
551
					$updateProps = [
552
						PR_ICON_INDEX => 261,
553
						PR_LAST_VERB_EXECUTED => 102,
554
					];
555
				}
556
				if (isset($updateProps)) {
557
					$updateProps[PR_LAST_VERB_EXECUTION_TIME] = time();
558
					mapi_setprops($fwmessage, $updateProps);
559
					mapi_savechanges($fwmessage);
560
				}
561
562
				// only attach the original message if the mobile does not send it itself
563
				if (!isset($sm->replacemime)) {
564
					// get message's body in order to append forward or reply text
565
					if (!isset($body)) {
566
						$body = MAPIUtils::readPropStream($mapimessage, PR_BODY);
567
					}
568
					if (!isset($bodyHtml)) {
569
						$bodyHtml = MAPIUtils::readPropStream($mapimessage, PR_HTML);
570
					}
571
					$cpid = mapi_getprops($fwmessage, [$sendMailProps["internetcpid"]]);
572
					if ($sm->forwardflag) {
573
						// attach the original attachments to the outgoing message
574
						$this->copyAttachments($mapimessage, $fwmessage);
575
					}
576
577
					// regarding the conversion @see ZP-470
578
					if (strlen($body) > 0) {
579
						$fwbody = MAPIUtils::readPropStream($fwmessage, PR_BODY);
580
						// if only the old message's cpid is set, convert from old charset to utf-8
581
						if (isset($cpid[$sendMailProps["internetcpid"]]) && $cpid[$sendMailProps["internetcpid"]] != INTERNET_CPID_UTF8) {
582
							SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->SendMail(): convert plain forwarded message charset (only fw set) from '%s' to '65001'", $cpid[$sendMailProps["internetcpid"]]));
583
							$fwbody = Utils::ConvertCodepageStringToUtf8($cpid[$sendMailProps["internetcpid"]], $fwbody);
584
						}
585
						// otherwise to the general conversion
586
						else {
587
							SLog::Write(LOGLEVEL_DEBUG, "Grommunio->SendMail(): no charset conversion done for plain forwarded message");
588
							$fwbody = w2u($fwbody);
589
						}
590
591
						$mapiprops[$sendMailProps["body"]] = $body . "\r\n\r\n" . $fwbody;
592
					}
593
594
					if (strlen($bodyHtml) > 0) {
595
						$fwbodyHtml = MAPIUtils::readPropStream($fwmessage, PR_HTML);
596
						// if only new message's cpid is set, convert to UTF-8
597
						if (isset($cpid[$sendMailProps["internetcpid"]]) && $cpid[$sendMailProps["internetcpid"]] != INTERNET_CPID_UTF8) {
598
							SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->SendMail(): convert html forwarded message charset (only fw set) from '%s' to '65001'", $cpid[$sendMailProps["internetcpid"]]));
599
							$fwbodyHtml = Utils::ConvertCodepageStringToUtf8($cpid[$sendMailProps["internetcpid"]], $fwbodyHtml);
600
						}
601
						// otherwise to the general conversion
602
						else {
603
							SLog::Write(LOGLEVEL_DEBUG, "Grommunio->SendMail(): no charset conversion done for html forwarded message");
604
							$fwbodyHtml = w2u($fwbodyHtml);
605
						}
606
607
						$mapiprops[$sendMailProps["html"]] = $bodyHtml . "<br><br>" . $fwbodyHtml;
608
					}
609
				}
610
			}
611
			else {
612
				// no fwmessage could be opened and we need it because we do not replace mime
613
				if (!isset($sm->replacemime) || $sm->replacemime == false) {
614
					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);
615
				}
616
			}
617
		}
618
619
		mapi_setprops($mapimessage, $mapiprops);
620
		mapi_savechanges($mapimessage);
621
		mapi_message_submitmessage($mapimessage);
622
		$hr = mapi_last_hresult();
623
624
		if ($hr) {
625
			switch ($hr) {
626
				case MAPI_E_STORE_FULL:
627
					$code = SYNC_COMMONSTATUS_MAILBOXQUOTAEXCEEDED;
628
					break;
629
630
				default:
631
					$code = SYNC_COMMONSTATUS_MAILSUBMISSIONFAILED;
632
					break;
633
			}
634
635
			throw new StatusException(sprintf("Grommunio->SendMail(): Error saving/submitting the message to the Outbox: 0x%X", $hr), $code);
636
		}
637
638
		SLog::Write(LOGLEVEL_DEBUG, "Grommunio->SendMail(): email submitted");
639
640
		return true;
641
	}
642
643
	/**
644
	 * Returns all available data of a single message.
645
	 *
646
	 * @param string            $folderid
647
	 * @param string            $id
648
	 * @param ContentParameters $contentparameters flag
649
	 *
650
	 * @throws StatusException
651
	 *
652
	 * @return object(SyncObject)
653
	 */
654
	public function Fetch($folderid, $id, $contentparameters) {
655
		// SEARCH fetches with folderid == false and PR_ENTRYID as ID
656
		if (!$folderid) {
657
			$entryid = hex2bin($id);
658
			$sk = $id;
659
		}
660
		else {
661
			// id might be in the new longid format, so we have to split it here
662
			list($fsk, $sk) = Utils::SplitMessageId($id);
663
			// get the entry id of the message
664
			$entryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($folderid), hex2bin($sk));
665
		}
666
		if (!$entryid) {
667
			throw new StatusException(sprintf("Grommunio->Fetch('%s','%s'): Error getting entryid: 0x%X", $folderid, $sk, mapi_last_hresult()), SYNC_STATUS_OBJECTNOTFOUND);
668
		}
669
670
		// open the message
671
		$message = mapi_msgstore_openentry($this->store, $entryid);
672
		if (!$message) {
673
			throw new StatusException(sprintf("Grommunio->Fetch('%s','%s'): Error, unable to open message: 0x%X", $folderid, $sk, mapi_last_hresult()), SYNC_STATUS_OBJECTNOTFOUND);
674
		}
675
676
		// convert the mapi message into a SyncObject and return it
677
		$mapiprovider = new MAPIProvider($this->session, $this->store);
678
679
		// override truncation
680
		$contentparameters->SetTruncation(SYNC_TRUNCATION_ALL);
681
		// TODO check for body preferences
682
		return $mapiprovider->GetMessage($message, $contentparameters);
683
	}
684
685
	/**
686
	 * Returns the waste basket.
687
	 *
688
	 * @return string
689
	 */
690
	public function GetWasteBasket() {
691
		if ($this->wastebasket) {
692
			return $this->wastebasket;
693
		}
694
695
		$storeprops = mapi_getprops($this->defaultstore, [PR_IPM_WASTEBASKET_ENTRYID]);
696
		if (isset($storeprops[PR_IPM_WASTEBASKET_ENTRYID])) {
697
			$wastebasket = mapi_msgstore_openentry($this->defaultstore, $storeprops[PR_IPM_WASTEBASKET_ENTRYID]);
698
			$wastebasketprops = mapi_getprops($wastebasket, [PR_SOURCE_KEY]);
699
			if (isset($wastebasketprops[PR_SOURCE_KEY])) {
700
				$this->wastebasket = bin2hex($wastebasketprops[PR_SOURCE_KEY]);
701
				SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetWasteBasket(): Got waste basket with id '%s'", $this->wastebasket));
702
703
				return $this->wastebasket;
704
			}
705
		}
706
707
		return false;
708
	}
709
710
	/**
711
	 * Returns the content of the named attachment as stream.
712
	 *
713
	 * @param string $attname
714
	 *
715
	 * @throws StatusException
716
	 *
717
	 * @return SyncItemOperationsAttachment
718
	 */
719
	public function GetAttachmentData($attname) {
720
		SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetAttachmentData('%s')", $attname));
721
722
		if (!strpos($attname, ":")) {
723
			throw new StatusException(sprintf("Grommunio->GetAttachmentData('%s'): Error, attachment requested for non-existing item", $attname), SYNC_ITEMOPERATIONSSTATUS_INVALIDATT);
724
		}
725
726
		list($id, $attachnum, $parentEntryid) = explode(":", $attname);
727
		if (isset($parentEntryid)) {
728
			$this->Setup(GSync::GetAdditionalSyncFolderStore($parentEntryid));
729
		}
730
731
		$entryid = hex2bin($id);
732
		$message = mapi_msgstore_openentry($this->store, $entryid);
733
		if (!$message) {
734
			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);
735
		}
736
737
		MAPIUtils::ParseSmime($this->session, $this->defaultstore, $this->getAddressbook(), $message);
738
		$attach = mapi_message_openattach($message, $attachnum);
739
		if (!$attach) {
740
			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);
741
		}
742
743
		// get necessary attachment props
744
		$attprops = mapi_getprops($attach, [PR_ATTACH_MIME_TAG, PR_ATTACH_METHOD]);
745
		$attachment = new SyncItemOperationsAttachment();
746
		// check if it's an embedded message and open it in such a case
747
		if (isset($attprops[PR_ATTACH_METHOD]) && $attprops[PR_ATTACH_METHOD] == ATTACH_EMBEDDED_MSG) {
748
			$embMessage = mapi_attach_openobj($attach);
749
			$addrbook = $this->getAddressbook();
750
			$stream = mapi_inetmapi_imtoinet($this->session, $addrbook, $embMessage, ['use_tnef' => -1]);
751
			// set the default contenttype for this kind of messages
752
			$attachment->contenttype = "message/rfc822";
753
		}
754
		else {
755
			$stream = mapi_openproperty($attach, PR_ATTACH_DATA_BIN, IID_IStream, 0, 0);
756
		}
757
758
		if (!$stream) {
759
			throw new StatusException(sprintf("Grommunio->GetAttachmentData('%s'): Error, unable to open attachment data stream: 0x%X", $attname, mapi_last_hresult()), SYNC_ITEMOPERATIONSSTATUS_INVALIDATT);
760
		}
761
762
		// put the mapi stream into a wrapper to get a standard stream
763
		$attachment->data = MAPIStreamWrapper::Open($stream);
764
		if (isset($attprops[PR_ATTACH_MIME_TAG])) {
765
			$attachment->contenttype = $attprops[PR_ATTACH_MIME_TAG];
766
		}
767
		// TODO default contenttype
768
		return $attachment;
769
	}
770
771
	/**
772
	 * Deletes all contents of the specified folder.
773
	 * This is generally used to empty the trash (wastebasked), but could also be used on any
774
	 * other folder.
775
	 *
776
	 * @param string $folderid
777
	 * @param bool   $includeSubfolders (opt) also delete sub folders, default true
778
	 *
779
	 * @throws StatusException
780
	 *
781
	 * @return bool
782
	 */
783
	public function EmptyFolder($folderid, $includeSubfolders = true) {
784
		$folderentryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($folderid));
785
		if (!$folderentryid) {
786
			throw new StatusException(sprintf("Grommunio->EmptyFolder('%s','%s'): Error, unable to open folder (no entry id)", $folderid, Utils::PrintAsString($includeSubfolders)), SYNC_ITEMOPERATIONSSTATUS_SERVERERROR);
787
		}
788
		$folder = mapi_msgstore_openentry($this->store, $folderentryid);
789
790
		if (!$folder) {
791
			throw new StatusException(sprintf("Grommunio->EmptyFolder('%s','%s'): Error, unable to open parent folder (open entry)", $folderid, Utils::PrintAsString($includeSubfolders)), SYNC_ITEMOPERATIONSSTATUS_SERVERERROR);
792
		}
793
794
		$flags = 0;
795
		if ($includeSubfolders) {
796
			$flags = DEL_ASSOCIATED;
797
		}
798
799
		SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->EmptyFolder('%s','%s'): emptying folder", $folderid, Utils::PrintAsString($includeSubfolders)));
800
801
		// empty folder!
802
		mapi_folder_emptyfolder($folder, $flags);
803
		if (mapi_last_hresult()) {
804
			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);
805
		}
806
807
		return true;
808
	}
809
810
	/**
811
	 * Processes a response to a meeting request.
812
	 * CalendarID is a reference and has to be set if a new calendar item is created.
813
	 *
814
	 * @param string $requestid id of the object containing the request
815
	 * @param string $folderid  id of the parent folder of $requestid
816
	 * @param string $response
817
	 *
818
	 * @throws StatusException
819
	 *
820
	 * @return string id of the created/updated calendar obj
821
	 */
822
	public function MeetingResponse($requestid, $folderid, $response) {
823
		// Use standard meeting response code to process meeting request
824
		list($fid, $requestid) = Utils::SplitMessageId($requestid);
825
		$reqentryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($folderid), hex2bin($requestid));
826
		if (!$reqentryid) {
827
			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);
828
		}
829
830
		$mapimessage = mapi_msgstore_openentry($this->store, $reqentryid);
831
		if (!$mapimessage) {
832
			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);
833
		}
834
835
		// ios sends calendar item in MeetingResponse
836
		// @see https://jira.z-hub.io/browse/ZP-1524
837
		$folderClass = GSync::GetDeviceManager()->GetFolderClassFromCacheByID($fid);
838
		// find the corresponding meeting request
839
		if ($folderClass != 'Email') {
840
			$props = MAPIMapping::GetMeetingRequestProperties();
841
			$props = getPropIdsFromStrings($this->store, $props);
842
843
			$messageprops = mapi_getprops($mapimessage, [$props["goidtag"]]);
844
			$goid = $messageprops[$props["goidtag"]];
845
846
			$mapiprovider = new MAPIProvider($this->session, $this->store);
847
			$inboxprops = $mapiprovider->GetInboxProps();
848
			$folder = mapi_msgstore_openentry($this->store, $inboxprops[PR_ENTRYID]);
849
850
			// Find the item by restricting all items to the correct ID
851
			$restrict = [RES_AND, [
852
				[RES_PROPERTY,
853
					[
854
						RELOP => RELOP_EQ,
855
						ULPROPTAG => $props["goidtag"],
856
						VALUE => $goid,
857
					],
858
				],
859
			]];
860
861
			$inboxcontents = mapi_folder_getcontentstable($folder);
862
863
			$rows = mapi_table_queryallrows($inboxcontents, [PR_ENTRYID], $restrict);
864
			if (empty($rows)) {
865
				throw new StatusException(sprintf("Grommunio->MeetingResponse('%s','%s', '%s'): Error, meeting request not found in the inbox", $requestid, $folderid, $response), SYNC_MEETRESPSTATUS_INVALIDMEETREQ);
866
			}
867
			SLog::Write(LOGLEVEL_DEBUG, "Grommunio->MeetingResponse found meeting request in the inbox");
868
			$mapimessage = mapi_msgstore_openentry($this->store, $rows[0][PR_ENTRYID]);
869
			$reqentryid = $rows[0][PR_ENTRYID];
870
		}
871
872
		$meetingrequest = new Meetingrequest($this->store, $mapimessage, $this->session);
873
874
		if (!$meetingrequest->isMeetingRequest() && !$meetingrequest->isMeetingRequestResponse() && !$meetingrequest->isMeetingCancellation()) {
875
			throw new StatusException(sprintf("Grommunio->MeetingResponse('%s','%s', '%s'): Error, attempt to respond to non-meeting request", $requestid, $folderid, $response), SYNC_MEETRESPSTATUS_INVALIDMEETREQ);
876
		}
877
878
		if ($meetingrequest->isLocalOrganiser()) {
879
			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);
880
		}
881
882
		// Process the meeting response. We don't have to send the actual meeting response
883
		// e-mail, because the device will send it itself. This seems not to be the case
884
		// anymore for the ios devices since at least version 12.4. grommunio-sync will send the
885
		// accepted email in such a case.
886
		// @see https://jira.z-hub.io/browse/ZP-1524
887
		$sendresponse = false;
888
		$deviceType = strtolower(Request::GetDeviceType());
889
		if ($deviceType == 'iphone' || $deviceType == 'ipad' || $deviceType == 'ipod') {
890
			$matches = [];
891
			if (preg_match("/^Apple-.*?\\/(\\d{4})\\./", Request::GetUserAgent(), $matches) && isset($matches[1]) && $matches[1] >= 1607 && $matches[1] <= 1707) {
892
				SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->MeetingResponse: iOS device %s->%s", Request::GetDeviceType(), Request::GetUserAgent()));
893
				$sendresponse = true;
894
			}
895
		}
896
897
		switch ($response) {
898
			case 1:     // accept
899
			default:
900
				$entryid = $meetingrequest->doAccept(false, $sendresponse, false, false, false, false, true); // last true is the $userAction
901
				break;
902
903
			case 2:        // tentative
904
				$entryid = $meetingrequest->doAccept(true, $sendresponse, false, false, false, false, true); // last true is the $userAction
905
				break;
906
907
			case 3:        // decline
908
				$meetingrequest->doDecline(false);
909
				break;
910
		}
911
912
		// F/B will be updated on logoff
913
914
		// We have to return the ID of the new calendar item, so do that here
915
		$calendarid = "";
916
		$calFolderId = "";
917
		if (isset($entryid)) {
918
			$newitem = mapi_msgstore_openentry($this->store, $entryid);
919
			// new item might be in a delegator's store. ActiveSync does not support accepting them.
920
			if (!$newitem) {
921
				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);
922
			}
923
924
			$newprops = mapi_getprops($newitem, [PR_SOURCE_KEY, PR_PARENT_SOURCE_KEY]);
925
			$calendarid = bin2hex($newprops[PR_SOURCE_KEY]);
926
			$calFolderId = bin2hex($newprops[PR_PARENT_SOURCE_KEY]);
927
		}
928
929
		// on recurring items, the MeetingRequest class responds with a wrong entryid
930
		if ($requestid == $calendarid) {
931
			SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->MeetingResponse('%s','%s', '%s'): returned calendar id is the same as the requestid - re-searching", $requestid, $folderid, $response));
932
933
			if (empty($props)) {
934
				$props = MAPIMapping::GetMeetingRequestProperties();
935
				$props = getPropIdsFromStrings($this->store, $props);
936
937
				$messageprops = mapi_getprops($mapimessage, [$props["goidtag"]]);
938
				$goid = $messageprops[$props["goidtag"]];
939
			}
940
941
			$items = $meetingrequest->findCalendarItems($goid);
942
943
			if (is_array($items)) {
944
				$newitem = mapi_msgstore_openentry($this->store, $items[0]);
945
				$newprops = mapi_getprops($newitem, [PR_SOURCE_KEY, PR_PARENT_SOURCE_KEY]);
946
				$calendarid = bin2hex($newprops[PR_SOURCE_KEY]);
947
				$calFolderId = bin2hex($newprops[PR_PARENT_SOURCE_KEY]);
948
				SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->MeetingResponse('%s','%s', '%s'): found other calendar entryid", $requestid, $folderid, $response));
949
			}
950
951
			if ($requestid == $calendarid) {
952
				throw new StatusException(sprintf("Grommunio->MeetingResponse('%s','%s', '%s'): Error finding the accepted meeting response in the calendar", $requestid, $folderid, $response), SYNC_MEETRESPSTATUS_INVALIDMEETREQ);
953
			}
954
		}
955
956
		// delete meeting request from Inbox
957
		if ($folderClass == 'Email') {
958
			$folderentryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($folderid));
959
			$folder = mapi_msgstore_openentry($this->store, $folderentryid);
960
		}
961
		mapi_folder_deletemessages($folder, [$reqentryid], 0);
962
963
		$prefix = '';
964
		// prepend the short folderid of the target calendar: if available and short ids are used
965
		if ($calFolderId) {
966
			$shortFolderId = GSync::GetDeviceManager()->GetFolderIdForBackendId($calFolderId);
967
			if ($calFolderId != $shortFolderId) {
968
				$prefix = $shortFolderId . ':';
969
			}
970
		}
971
972
		return $prefix . $calendarid;
973
	}
974
975
	/**
976
	 * Indicates if the backend has a ChangesSink.
977
	 * A sink is an active notification mechanism which does not need polling.
978
	 * Since Zarafa 7.0.5 such a sink is available.
979
	 * The grommunio backend uses this method to initialize the sink with mapi.
980
	 *
981
	 * @return bool
982
	 */
983
	public function HasChangesSink() {
984
		if (!$this->notifications) {
985
			SLog::Write(LOGLEVEL_DEBUG, "Grommunio->HasChangesSink(): sink is not available");
986
987
			return false;
988
		}
989
990
		$this->changesSink = @mapi_sink_create();
991
992
		if (!$this->changesSink || mapi_last_hresult()) {
993
			SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->HasChangesSink(): sink could not be created with  0x%X", mapi_last_hresult()));
994
995
			return false;
996
		}
997
998
		$this->changesSinkHierarchyHash = $this->getHierarchyHash();
999
		SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->HasChangesSink(): created - HierarchyHash: %s", $this->changesSinkHierarchyHash));
1000
1001
		// advise the main store and also to check if the connection supports it
1002
		return $this->adviseStoreToSink($this->defaultstore);
1003
	}
1004
1005
	/**
1006
	 * The folder should be considered by the sink.
1007
	 * Folders which were not initialized should not result in a notification
1008
	 * of IBackend->ChangesSink().
1009
	 *
1010
	 * @param string $folderid
1011
	 *
1012
	 * @return bool false if entryid can not be found for that folder
1013
	 */
1014
	public function ChangesSinkInitialize($folderid) {
1015
		SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->ChangesSinkInitialize(): folderid '%s'", $folderid));
1016
1017
		$entryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($folderid));
1018
		if (!$entryid) {
1019
			return false;
1020
		}
1021
1022
		// add entryid to the monitored folders
1023
		$this->changesSinkFolders[$entryid] = $folderid;
1024
1025
		// advise the current store to the sink
1026
		return $this->adviseStoreToSink($this->store);
1027
	}
1028
1029
	/**
1030
	 * The actual ChangesSink.
1031
	 * For max. the $timeout value this method should block and if no changes
1032
	 * are available return an empty array.
1033
	 * If changes are available a list of folderids is expected.
1034
	 *
1035
	 * @param int $timeout max. amount of seconds to block
1036
	 *
1037
	 * @return array
1038
	 */
1039
	public function ChangesSink($timeout = 30) {
1040
		// clear the folder stats cache
1041
		unset($this->folderStatCache);
1042
1043
		$notifications = [];
1044
		$hierarchyNotifications = [];
1045
		$sinkresult = @mapi_sink_timedwait($this->changesSink, $timeout * 1000);
1046
1047
		if (!is_array($sinkresult)) {
1048
			throw new StatusException("Grommunio->ChangesSink(): Sink returned invalid notification, aborting", SyncCollections::OBSOLETE_CONNECTION);
1049
		}
1050
1051
		// reverse array so that the changes on folders are before changes on messages and
1052
		// it's possible to filter such notifications
1053
		$sinkresult = array_reverse($sinkresult, true);
1054
		foreach ($sinkresult as $sinknotif) {
1055
			if (isset($sinknotif['objtype'])) {
1056
				// add a notification on a folder
1057
				if ($sinknotif['objtype'] == MAPI_FOLDER) {
1058
					$hierarchyNotifications[$sinknotif['entryid']] = IBackend::HIERARCHYNOTIFICATION;
1059
				}
1060
				// change on a message, remove hierarchy notification
1061
				if (isset($sinknotif['parentid']) && $sinknotif['objtype'] == MAPI_MESSAGE && isset($notifications[$sinknotif['parentid']])) {
1062
					unset($hierarchyNotifications[$sinknotif['parentid']]);
1063
				}
1064
			}
1065
1066
			// check if something in the monitored folders changed
1067
			// 'objtype' is not set when mail is received, so we don't check for it
1068
			if (isset($sinknotif['parentid']) && array_key_exists($sinknotif['parentid'], $this->changesSinkFolders)) {
1069
				$notifications[] = $this->changesSinkFolders[$sinknotif['parentid']];
1070
			}
1071
			// deletes and moves
1072
			if (isset($sinknotif['oldparentid']) && array_key_exists($sinknotif['oldparentid'], $this->changesSinkFolders)) {
1073
				$notifications[] = $this->changesSinkFolders[$sinknotif['oldparentid']];
1074
			}
1075
		}
1076
1077
		// validate hierarchy notifications by comparing the hierarchy hashes (too many false positives otherwise)
1078
		if (!empty($hierarchyNotifications)) {
1079
			$hash = $this->getHierarchyHash();
1080
			if ($hash !== $this->changesSinkHierarchyHash) {
1081
				SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->ChangesSink() Hierarchy notification, pending validation. New hierarchyHash: %s", $hash));
1082
				$notifications[] = IBackend::HIERARCHYNOTIFICATION;
1083
				$this->changesSinkHierarchyHash = $hash;
1084
			}
1085
		}
1086
1087
		return $notifications;
1088
	}
1089
1090
	/**
1091
	 * Applies settings to and gets information from the device.
1092
	 *
1093
	 * @param SyncObject $settings (SyncOOF, SyncUserInformation, SyncRightsManagementTemplates possible)
1094
	 *
1095
	 * @return SyncObject $settings
1096
	 */
1097
	public function Settings($settings) {
1098
		if ($settings instanceof SyncOOF) {
1099
			$this->settingsOOF($settings);
1100
		}
1101
1102
		if ($settings instanceof SyncUserInformation) {
1103
			$this->settingsUserInformation($settings);
1104
		}
1105
1106
		if ($settings instanceof SyncRightsManagementTemplates) {
1107
			$this->settingsRightsManagementTemplates($settings);
1108
		}
1109
1110
		return $settings;
1111
	}
1112
1113
	/**
1114
	 * Resolves recipients.
1115
	 *
1116
	 * @param SyncObject $resolveRecipients
1117
	 *
1118
	 * @return SyncObject $resolveRecipients
1119
	 */
1120
	public function ResolveRecipients($resolveRecipients) {
1121
		if ($resolveRecipients instanceof SyncResolveRecipients) {
1122
			$resolveRecipients->status = SYNC_RESOLVERECIPSSTATUS_SUCCESS;
1123
			$resolveRecipients->response = [];
1124
			$resolveRecipientsOptions = new SyncResolveRecipientsOptions();
1125
			$maxAmbiguousRecipients = self::MAXAMBIGUOUSRECIPIENTS;
1126
1127
			if (isset($resolveRecipients->options)) {
1128
				$resolveRecipientsOptions = $resolveRecipients->options;
1129
				// only limit ambiguous recipients if the client requests it.
1130
1131
				if (isset($resolveRecipientsOptions->maxambiguousrecipients) &&
1132
						$resolveRecipientsOptions->maxambiguousrecipients >= 0 &&
1133
						$resolveRecipientsOptions->maxambiguousrecipients <= self::MAXAMBIGUOUSRECIPIENTS) {
1134
					$maxAmbiguousRecipients = $resolveRecipientsOptions->maxambiguousrecipients;
1135
					SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->ResolveRecipients(): The client requested %d max ambiguous recipients to resolve.", $maxAmbiguousRecipients));
1136
				}
1137
			}
1138
1139
			foreach ($resolveRecipients->to as $i => $to) {
1140
				$response = new SyncResolveRecipientsResponse();
1141
				$response->to = $to;
1142
				$response->status = SYNC_RESOLVERECIPSSTATUS_SUCCESS;
1143
1144
				// do not expand distlists here
1145
				$recipient = $this->resolveRecipient($to, $maxAmbiguousRecipients, false);
1146
				if (is_array($recipient) && !empty($recipient)) {
1147
					$response->recipientcount = 0;
1148
					foreach ($recipient as $entry) {
1149
						if ($entry instanceof SyncResolveRecipient) {
1150
							// certificates are already set. Unset them if they weren't required.
1151
							if (!isset($resolveRecipientsOptions->certificateretrieval)) {
1152
								unset($entry->certificates);
1153
							}
1154
							if (isset($resolveRecipientsOptions->availability)) {
1155
								if (!isset($resolveRecipientsOptions->starttime)) {
1156
									// TODO error, the request must include a valid StartTime element value
1157
								}
1158
								$entry->availability = $this->getAvailability($to, $entry, $resolveRecipientsOptions);
1159
							}
1160
							if (isset($resolveRecipientsOptions->picture)) {
1161
								// TODO implement picture retrieval of the recipient
1162
							}
1163
							++$response->recipientcount;
1164
							$response->recipient[] = $entry;
1165
						}
1166
						elseif (is_int($recipient)) {
1167
							$response->status = $recipient;
1168
						}
1169
					}
1170
				}
1171
1172
				$resolveRecipients->response[$i] = $response;
1173
			}
1174
1175
			return $resolveRecipients;
1176
		}
1177
1178
		SLog::Write(LOGLEVEL_WARN, "Grommunio->ResolveRecipients(): Not a valid SyncResolveRecipients object.");
1179
		// return a SyncResolveRecipients object so that sync doesn't fail
1180
		$r = new SyncResolveRecipients();
1181
		$r->status = SYNC_RESOLVERECIPSSTATUS_PROTOCOLERROR;
1182
1183
		return $r;
1184
	}
1185
1186
	/*----------------------------------------------------------------------------------------------------------
1187
	 * Implementation of the ISearchProvider interface
1188
	 */
1189
1190
	/**
1191
	 * Indicates if a search type is supported by this SearchProvider
1192
	 * Currently only the type ISearchProvider::SEARCH_GAL (Global Address List) is implemented.
1193
	 *
1194
	 * @param string $searchtype
1195
	 *
1196
	 * @return bool
1197
	 */
1198
	public function SupportsType($searchtype) {
1199
		return ($searchtype == ISearchProvider::SEARCH_GAL) || ($searchtype == ISearchProvider::SEARCH_MAILBOX);
1200
	}
1201
1202
	/**
1203
	 * Searches the GAB of Grommunio
1204
	 * Can be overwritten globally by configuring a SearchBackend.
1205
	 *
1206
	 * @param string                       $searchquery   string to be searched for
1207
	 * @param string                       $searchrange   specified searchrange
1208
	 * @param SyncResolveRecipientsPicture $searchpicture limitations for picture
1209
	 *
1210
	 * @throws StatusException
1211
	 *
1212
	 * @return array search results
1213
	 */
1214
	public function GetGALSearchResults($searchquery, $searchrange, $searchpicture) {
1215
		// only return users whose displayName or the username starts with $name
1216
		// TODO: use PR_ANR for this restriction instead of PR_DISPLAY_NAME and PR_ACCOUNT
1217
		$addrbook = $this->getAddressbook();
1218
		// FIXME: create a function to get the adressbook contentstable
1219
		if ($addrbook) {
1220
			$ab_entryid = mapi_ab_getdefaultdir($addrbook);
1221
		}
1222
		if ($ab_entryid) {
1223
			$ab_dir = mapi_ab_openentry($addrbook, $ab_entryid);
1224
		}
1225
		if ($ab_dir) {
1226
			$table = mapi_folder_getcontentstable($ab_dir);
1227
		}
1228
1229
		if (!$table) {
1230
			throw new StatusException(sprintf("Grommunio->GetGALSearchResults(): could not open addressbook: 0x%X", mapi_last_hresult()), SYNC_SEARCHSTATUS_STORE_CONNECTIONFAILED);
1231
		}
1232
1233
		$restriction = MAPIUtils::GetSearchRestriction(u2w($searchquery));
1234
		mapi_table_restrict($table, $restriction);
1235
		mapi_table_sort($table, [PR_DISPLAY_NAME => TABLE_SORT_ASCEND]);
1236
1237
		if (mapi_last_hresult()) {
1238
			throw new StatusException(sprintf("Grommunio->GetGALSearchResults(): could not apply restriction: 0x%X", mapi_last_hresult()), SYNC_SEARCHSTATUS_STORE_TOOCOMPLEX);
1239
		}
1240
1241
		// range for the search results, default symbian range end is 50, wm 99,
1242
		// so we'll use that of nokia
1243
		$rangestart = 0;
1244
		$rangeend = 50;
1245
1246
		if ($searchrange != '0') {
1247
			$pos = strpos($searchrange, '-');
1248
			$rangestart = substr($searchrange, 0, $pos);
1249
			$rangeend = substr($searchrange, ($pos + 1));
1250
		}
1251
		$items = [];
1252
1253
		$querycnt = mapi_table_getrowcount($table);
1254
		// do not return more results as requested in range
1255
		$querylimit = (($rangeend + 1) < $querycnt) ? ($rangeend + 1) : $querycnt;
1256
1257
		if ($querycnt > 0) {
1258
			$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);
1259
		}
1260
1261
		for ($i = 0; $i < $querylimit; ++$i) {
1262
			if (!isset($abentries[$i][PR_SMTP_ADDRESS])) {
1263
				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])));
1264
1265
				continue;
1266
			}
1267
1268
			$items[$i][SYNC_GAL_DISPLAYNAME] = w2u($abentries[$i][PR_DISPLAY_NAME]);
1269
1270
			if (strlen(trim($items[$i][SYNC_GAL_DISPLAYNAME])) == 0) {
1271
				$items[$i][SYNC_GAL_DISPLAYNAME] = w2u($abentries[$i][PR_ACCOUNT]);
1272
			}
1273
1274
			$items[$i][SYNC_GAL_ALIAS] = w2u($abentries[$i][PR_ACCOUNT]);
1275
			// it's not possible not get first and last name of an user
1276
			// from the gab and user functions, so we just set lastname
1277
			// to displayname and leave firstname unset
1278
			// this was changed in Zarafa 6.40, so we try to get first and
1279
			// last name and fall back to the old behaviour if these values are not set
1280
			if (isset($abentries[$i][PR_GIVEN_NAME])) {
1281
				$items[$i][SYNC_GAL_FIRSTNAME] = w2u($abentries[$i][PR_GIVEN_NAME]);
1282
			}
1283
			if (isset($abentries[$i][PR_SURNAME])) {
1284
				$items[$i][SYNC_GAL_LASTNAME] = w2u($abentries[$i][PR_SURNAME]);
1285
			}
1286
1287
			if (!isset($items[$i][SYNC_GAL_LASTNAME])) {
1288
				$items[$i][SYNC_GAL_LASTNAME] = $items[$i][SYNC_GAL_DISPLAYNAME];
1289
			}
1290
1291
			$items[$i][SYNC_GAL_EMAILADDRESS] = w2u($abentries[$i][PR_SMTP_ADDRESS]);
1292
			// check if an user has an office number or it might produce warnings in the log
1293
			if (isset($abentries[$i][PR_BUSINESS_TELEPHONE_NUMBER])) {
1294
				$items[$i][SYNC_GAL_PHONE] = w2u($abentries[$i][PR_BUSINESS_TELEPHONE_NUMBER]);
1295
			}
1296
			// check if an user has a mobile number or it might produce warnings in the log
1297
			if (isset($abentries[$i][PR_MOBILE_TELEPHONE_NUMBER])) {
1298
				$items[$i][SYNC_GAL_MOBILEPHONE] = w2u($abentries[$i][PR_MOBILE_TELEPHONE_NUMBER]);
1299
			}
1300
			// check if an user has a home number or it might produce warnings in the log
1301
			if (isset($abentries[$i][PR_HOME_TELEPHONE_NUMBER])) {
1302
				$items[$i][SYNC_GAL_HOMEPHONE] = w2u($abentries[$i][PR_HOME_TELEPHONE_NUMBER]);
1303
			}
1304
1305
			if (isset($abentries[$i][PR_COMPANY_NAME])) {
1306
				$items[$i][SYNC_GAL_COMPANY] = w2u($abentries[$i][PR_COMPANY_NAME]);
1307
			}
1308
1309
			if (isset($abentries[$i][PR_TITLE])) {
1310
				$items[$i][SYNC_GAL_TITLE] = w2u($abentries[$i][PR_TITLE]);
1311
			}
1312
1313
			if (isset($abentries[$i][PR_OFFICE_LOCATION])) {
1314
				$items[$i][SYNC_GAL_OFFICE] = w2u($abentries[$i][PR_OFFICE_LOCATION]);
1315
			}
1316
1317
			if ($searchpicture !== false && isset($abentries[$i][PR_EMS_AB_THUMBNAIL_PHOTO])) {
1318
				$items[$i][SYNC_GAL_PICTURE] = StringStreamWrapper::Open($abentries[$i][PR_EMS_AB_THUMBNAIL_PHOTO]);
1319
			}
1320
		}
1321
		$nrResults = count($items);
1322
		$items['range'] = ($nrResults > 0) ? $rangestart . '-' . ($nrResults - 1) : '0-0';
1323
		$items['searchtotal'] = $nrResults;
1324
1325
		return $items;
1326
	}
1327
1328
	/**
1329
	 * Searches for the emails on the server.
1330
	 *
1331
	 * @param ContentParameter $cpo
1332
	 *
1333
	 * @return array
1334
	 */
1335
	public function GetMailboxSearchResults($cpo) {
1336
		$searchFolder = $this->getSearchFolder();
1337
		$searchRestriction = $this->getSearchRestriction($cpo);
1338
		$searchRange = explode('-', $cpo->GetSearchRange());
1339
		$searchFolderId = $cpo->GetSearchFolderid();
1340
		$searchFolders = [];
1341
		// search only in required folders
1342
		if (!empty($searchFolderId)) {
1343
			$searchFolderEntryId = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($searchFolderId));
1344
			$searchFolders[] = $searchFolderEntryId;
1345
		}
1346
		// if no folder was required then search in the entire store
1347
		else {
1348
			$tmp = mapi_getprops($this->store, [PR_ENTRYID, PR_DISPLAY_NAME, PR_IPM_SUBTREE_ENTRYID]);
1349
			$searchFolders[] = $tmp[PR_IPM_SUBTREE_ENTRYID];
1350
		}
1351
		$items = [];
1352
		$flags = 0;
1353
		// if subfolders are required, do a recursive search
1354
		if ($cpo->GetSearchDeepTraversal()) {
1355
			$flags |= SEARCH_RECURSIVE;
1356
		}
1357
1358
		mapi_folder_setsearchcriteria($searchFolder, $searchRestriction, $searchFolders, $flags);
1359
1360
		$table = mapi_folder_getcontentstable($searchFolder);
1361
		$searchStart = time();
1362
		// do the search and wait for all the results available
1363
		while (time() - $searchStart < SEARCH_WAIT) {
1364
			$searchcriteria = mapi_folder_getsearchcriteria($searchFolder);
1365
			if (($searchcriteria["searchstate"] & SEARCH_REBUILD) == 0) {
1366
				break;
1367
			} // Search is done
1368
			sleep(1);
1369
		}
1370
1371
		// if the search range is set limit the result to it, otherwise return all found messages
1372
		$rows = (is_array($searchRange) && isset($searchRange[0], $searchRange[1])) ?
1373
			mapi_table_queryrows($table, [PR_ENTRYID], $searchRange[0], $searchRange[1] - $searchRange[0] + 1) :
1374
			mapi_table_queryrows($table, [PR_ENTRYID], 0, SEARCH_MAXRESULTS);
1375
1376
		$cnt = count($rows);
1377
		$items['searchtotal'] = $cnt;
1378
		$items["range"] = $cpo->GetSearchRange();
1379
		for ($i = 0; $i < $cnt; ++$i) {
1380
			$items[$i]['class'] = 'Email';
1381
			$items[$i]['longid'] = bin2hex($rows[$i][PR_ENTRYID]);
1382
			// $items[$i]['folderid'] = bin2hex($rows[$i][PR_PARENT_SOURCE_KEY]);
1383
		}
1384
1385
		return $items;
1386
	}
1387
1388
	/**
1389
	 * Terminates a search for a given PID.
1390
	 *
1391
	 * @param int $pid
1392
	 *
1393
	 * @return bool
1394
	 */
1395
	public function TerminateSearch($pid) {
1396
		SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->TerminateSearch(): terminating search for pid %d", $pid));
1397
		if (!isset($this->store) || $this->store === false) {
1398
			SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->TerminateSearch(): The store is not available. It is not possible to remove search folder with pid %d", $pid));
1399
1400
			return false;
1401
		}
1402
1403
		$storeProps = mapi_getprops($this->store, [PR_STORE_SUPPORT_MASK, PR_FINDER_ENTRYID]);
1404
		if (($storeProps[PR_STORE_SUPPORT_MASK] & STORE_SEARCH_OK) != STORE_SEARCH_OK) {
1405
			SLog::Write(LOGLEVEL_WARN, "Grommunio->TerminateSearch(): Store doesn't support search folders. Public store doesn't have FINDER_ROOT folder");
1406
1407
			return false;
1408
		}
1409
1410
		$finderfolder = mapi_msgstore_openentry($this->store, $storeProps[PR_FINDER_ENTRYID]);
1411
		if (mapi_last_hresult() != NOERROR) {
1412
			SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->TerminateSearch(): Unable to open search folder (0x%X)", mapi_last_hresult()));
1413
1414
			return false;
1415
		}
1416
1417
		$hierarchytable = mapi_folder_gethierarchytable($finderfolder);
1418
		mapi_table_restrict(
1419
			$hierarchytable,
1420
			[RES_CONTENT,
1421
				[
1422
					FUZZYLEVEL => FL_PREFIX,
1423
					ULPROPTAG => PR_DISPLAY_NAME,
1424
					VALUE => [PR_DISPLAY_NAME => "grommunio-sync Search Folder " . $pid],
1425
				],
1426
			],
1427
			TBL_BATCH
1428
		);
1429
1430
		$folders = mapi_table_queryallrows($hierarchytable, [PR_ENTRYID, PR_DISPLAY_NAME, PR_LAST_MODIFICATION_TIME]);
1431
		foreach ($folders as $folder) {
1432
			mapi_folder_deletefolder($finderfolder, $folder[PR_ENTRYID]);
1433
		}
1434
1435
		return true;
1436
	}
1437
1438
	/**
1439
	 * Disconnects from the current search provider.
1440
	 *
1441
	 * @return bool
1442
	 */
1443
	public function Disconnect() {
1444
		return true;
1445
	}
1446
1447
	/**
1448
	 * Returns the MAPI store resource for a folderid
1449
	 * This is not part of IBackend but necessary for the ImportChangesICS->MoveMessage() operation if
1450
	 * the destination folder is not in the default store
1451
	 * Note: The current backend store might be changed as IBackend->Setup() is executed.
1452
	 *
1453
	 * @param string $store    target store, could contain a "domain\user" value - if empty default store is returned
1454
	 * @param string $folderid
1455
	 *
1456
	 * @return bool|resource
1457
	 */
1458
	public function GetMAPIStoreForFolderId($store, $folderid) {
1459
		if ($store == false) {
1460
			SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetMAPIStoreForFolderId('%s', '%s'): no store specified, returning default store", $store, $folderid));
1461
1462
			return $this->defaultstore;
1463
		}
1464
1465
		// setup the correct store
1466
		if ($this->Setup($store, false, $folderid)) {
1467
			return $this->store;
1468
		}
1469
1470
		SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->GetMAPIStoreForFolderId('%s', '%s'): store is not available", $store, $folderid));
1471
1472
		return false;
1473
	}
1474
1475
	/**
1476
	 * Returns the email address and the display name of the user. Used by autodiscover.
1477
	 *
1478
	 * @param string $username The username
1479
	 *
1480
	 * @return array
1481
	 */
1482
	public function GetUserDetails($username) {
1483
		SLog::Write(LOGLEVEL_WBXML, sprintf("Grommunio->GetUserDetails for '%s'.", $username));
1484
		$zarafauserinfo = @nsp_getuserinfo($username);
1485
		$userDetails['emailaddress'] = (isset($zarafauserinfo['primary_email']) && $zarafauserinfo['primary_email']) ? $zarafauserinfo['primary_email'] : false;
1486
		$userDetails['fullname'] = (isset($zarafauserinfo['fullname']) && $zarafauserinfo['fullname']) ? $zarafauserinfo['fullname'] : false;
1487
1488
		return $userDetails;
1489
	}
1490
1491
	/**
1492
	 * Returns the username of the currently active user.
1493
	 *
1494
	 * @return string
1495
	 */
1496
	public function GetCurrentUsername() {
1497
		return $this->storeName;
1498
	}
1499
1500
	/**
1501
	 * Returns the impersonated user name.
1502
	 *
1503
	 * @return string or false if no user is impersonated
1504
	 */
1505
	public function GetImpersonatedUser() {
1506
		return $this->impersonateUser;
1507
	}
1508
1509
	/**
1510
	 * Returns the authenticated user name.
1511
	 *
1512
	 * @return string
1513
	 */
1514
	public function GetMainUser() {
1515
		return $this->mainUser;
1516
	}
1517
1518
	/**
1519
	 * Indicates if the Backend supports folder statistics.
1520
	 *
1521
	 * @return bool
1522
	 */
1523
	public function HasFolderStats() {
1524
		return true;
1525
	}
1526
1527
	/**
1528
	 * Returns a status indication of the folder.
1529
	 * If there are changes in the folder, the returned value must change.
1530
	 * The returned values are compared with '===' to determine if a folder needs synchronization or not.
1531
	 *
1532
	 * @param string $store    the store where the folder resides
1533
	 * @param string $folderid the folder id
1534
	 *
1535
	 * @return string
1536
	 */
1537
	public function GetFolderStat($store, $folderid) {
1538
		list($user, $domain) = Utils::SplitDomainUser($store);
1539
		if ($user === false) {
1540
			$user = $this->mainUser;
1541
			if ($this->impersonateUser) {
1542
				$user = $this->impersonateUser;
1543
			}
1544
		}
1545
1546
		if (!isset($this->folderStatCache[$user])) {
1547
			$this->folderStatCache[$user] = [];
1548
		}
1549
1550
		// if there is nothing in the cache for a store, load the data for all folders of it
1551
		if (empty($this->folderStatCache[$user])) {
1552
			// get the store
1553
			$userstore = $this->openMessageStore($user);
1554
			$rootfolder = mapi_msgstore_openentry($userstore);
1555
			$hierarchy = mapi_folder_gethierarchytable($rootfolder, CONVENIENT_DEPTH);
1556
			$rows = mapi_table_queryallrows($hierarchy, [PR_SOURCE_KEY, PR_LOCAL_COMMIT_TIME_MAX, PR_CONTENT_COUNT, PR_CONTENT_UNREAD, PR_DELETED_MSG_COUNT]);
1557
1558
			if (count($rows) == 0) {
1559
				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));
1560
			}
1561
1562
			foreach ($rows as $folder) {
1563
				$commit_time = isset($folder[PR_LOCAL_COMMIT_TIME_MAX]) ? $folder[PR_LOCAL_COMMIT_TIME_MAX] : "0000000000";
1564
				$content_count = isset($folder[PR_CONTENT_COUNT]) ? $folder[PR_CONTENT_COUNT] : -1;
1565
				$content_unread = isset($folder[PR_CONTENT_UNREAD]) ? $folder[PR_CONTENT_UNREAD] : -1;
1566
				$content_deleted = isset($folder[PR_DELETED_MSG_COUNT]) ? $folder[PR_DELETED_MSG_COUNT] : -1;
1567
1568
				$this->folderStatCache[$user][bin2hex($folder[PR_SOURCE_KEY])] = $commit_time . "/" . $content_count . "/" . $content_unread . "/" . $content_deleted;
1569
			}
1570
			SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetFolderStat() fetched status information of %d folders for store '%s'", count($this->folderStatCache[$user]), $user));
1571
		}
1572
1573
		if (isset($this->folderStatCache[$user][$folderid])) {
1574
			return $this->folderStatCache[$user][$folderid];
1575
		}
1576
1577
		// 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.
1578
		return gmdate("Y-m-d-H");
1579
	}
1580
1581
	/**
1582
	 * Returns information about the user's store:
1583
	 * number of folders, store size, full name, email address.
1584
	 *
1585
	 * @return UserStoreInfo
1586
	 */
1587
	public function GetUserStoreInfo() {
1588
		$userStoreInfo = new UserStoreInfo();
1589
1590
		$rootfolder = mapi_msgstore_openentry($this->store);
1591
		$hierarchy = mapi_folder_gethierarchytable($rootfolder, CONVENIENT_DEPTH);
1592
		// Do not take hidden and system folders into account
1593
		// TODO make this restriction generic and use for hierarchy?
1594
		$restrict = [
1595
			RES_AND,
1596
			[
1597
				[
1598
					RES_PROPERTY,
1599
					[
1600
						RELOP => RELOP_NE,
1601
						ULPROPTAG => PR_ATTR_HIDDEN,
1602
						VALUE => true, ],
1603
				],
1604
				[
1605
					RES_PROPERTY,
1606
					[
1607
						RELOP => RELOP_EQ,
1608
						ULPROPTAG => PR_FOLDER_TYPE,
1609
						VALUE => FOLDER_GENERIC, ],
1610
				],
1611
				[
1612
					RES_EXIST,
1613
					[ULPROPTAG => PR_CONTAINER_CLASS],
1614
				],
1615
			], ];
1616
		mapi_table_restrict($hierarchy, $restrict);
1617
		$foldercount = mapi_table_getrowcount($hierarchy);
1618
1619
		$storeProps = mapi_getprops($this->store, [PR_MESSAGE_SIZE_EXTENDED]);
1620
		$storesize = isset($storeProps[PR_MESSAGE_SIZE_EXTENDED]) ? $storeProps[PR_MESSAGE_SIZE_EXTENDED] : 0;
1621
1622
		$userDetails = $this->GetUserDetails($this->impersonateUser ?: $this->mainUser);
1623
		$userStoreInfo->SetData($foldercount, $storesize, $userDetails['fullname'], $userDetails['emailaddress']);
1624
		SLog::Write(LOGLEVEL_DEBUG, sprintf(
1625
			"Grommunio->GetUserStoreInfo(): user %s (%s) store size is %d bytes and contains %d folders",
1626
			Utils::PrintAsString($userDetails['fullname']),
1627
			Utils::PrintAsString($userDetails['emailaddress']),
1628
			$storesize,
1629
			$foldercount
1630
		));
1631
1632
		return $userStoreInfo;
1633
	}
1634
1635
	/*----------------------------------------------------------------------------------------------------------
1636
	 * Implementation of the IStateMachine interface
1637
	 */
1638
1639
	/**
1640
	 * Gets a hash value indicating the latest dataset of the named
1641
	 * state with a specified key and counter.
1642
	 * If the state is changed between two calls of this method
1643
	 * the returned hash should be different.
1644
	 *
1645
	 * @param string $devid   the device id
1646
	 * @param string $type    the state type
1647
	 * @param string $key     (opt)
1648
	 * @param string $counter (opt)
1649
	 *
1650
	 * @throws StateNotFoundException
1651
	 *
1652
	 * @return string
1653
	 */
1654
	public function GetStateHash($devid, $type, $key = false, $counter = false) {
1655
		try {
1656
			$stateMessage = $this->getStateMessage($devid, $type, $key, $counter);
1657
			$stateMessageProps = mapi_getprops($stateMessage, [PR_LAST_MODIFICATION_TIME]);
1658
			if (isset($stateMessageProps[PR_LAST_MODIFICATION_TIME])) {
1659
				return $stateMessageProps[PR_LAST_MODIFICATION_TIME];
1660
			}
1661
		}
1662
		catch (StateNotFoundException $e) {
1663
		}
1664
1665
		return "0";
1666
	}
1667
1668
	/**
1669
	 * Gets a state for a specified key and counter.
1670
	 * This method should call IStateMachine->CleanStates()
1671
	 * to remove older states (same key, previous counters).
1672
	 *
1673
	 * @param string $devid       the device id
1674
	 * @param string $type        the state type
1675
	 * @param string $key         (opt)
1676
	 * @param string $counter     (opt)
1677
	 * @param string $cleanstates (opt)
1678
	 *
1679
	 * @throws StateNotFoundException, StateInvalidException, UnavailableException
1680
	 *
1681
	 * @return mixed
1682
	 */
1683
	public function GetState($devid, $type, $key = false, $counter = false, $cleanstates = true) {
1684
		if ($counter && $cleanstates) {
1685
			$this->CleanStates($devid, $type, $key, $counter);
1686
			// also clean Failsave state for previous counter
1687
			if ($key == false) {
1688
				$this->CleanStates($devid, $type, IStateMachine::FAILSAVE, $counter);
1689
			}
1690
		}
1691
		$stateMessage = $this->getStateMessage($devid, $type, $key, $counter);
1692
		$state = base64_decode(MAPIUtils::readPropStream($stateMessage, PR_BODY));
1693
1694
		if ($state && $state[0] === '{') {
1695
			$jsonDec = json_decode($state);
1696
			if (isset($jsonDec->gsSyncStateClass)) {
1697
				SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetState(): top class '%s'", $jsonDec->gsSyncStateClass));
1698
				$gsObj = new $jsonDec->gsSyncStateClass();
1699
				$gsObj->jsonDeserialize($jsonDec);
1700
				$gsObj->postUnserialize();
1701
			}
1702
		}
1703
1704
		return isset($gsObj) && is_object($gsObj) ? $gsObj : $state;
1705
	}
1706
1707
	/**
1708
	 * Writes ta state to for a key and counter.
1709
	 *
1710
	 * @param mixed  $state
1711
	 * @param string $devid   the device id
1712
	 * @param string $type    the state type
1713
	 * @param string $key     (opt)
1714
	 * @param int    $counter (opt)
1715
	 *
1716
	 * @throws StateInvalidException, UnavailableException
1717
	 *
1718
	 * @return bool
1719
	 */
1720
	public function SetState($state, $devid, $type, $key = false, $counter = false) {
1721
		return $this->setStateMessage($state, $devid, $type, $key, $counter);
1722
	}
1723
1724
	/**
1725
	 * Cleans up all older states.
1726
	 * If called with a $counter, all states previous state counter can be removed.
1727
	 * If additionally the $thisCounterOnly flag is true, only that specific counter will be removed.
1728
	 * If called without $counter, all keys (independently from the counter) can be removed.
1729
	 *
1730
	 * @param string $devid           the device id
1731
	 * @param string $type            the state type
1732
	 * @param string $key
1733
	 * @param string $counter         (opt)
1734
	 * @param string $thisCounterOnly (opt) if provided, the exact counter only will be removed
1735
	 *
1736
	 * @throws StateInvalidException
1737
	 *
1738
	 * @return
1739
	 */
1740
	public function CleanStates($devid, $type, $key, $counter = false, $thisCounterOnly = false) {
1741
		if (!$this->stateFolder) {
1742
			$this->getStateFolder($devid);
1743
			if (!$this->stateFolder) {
1744
				throw new StateNotFoundException(sprintf(
1745
					"Grommunio->getStateMessage(): Could not locate the state folder for device '%s'",
1746
					$devid
1747
				));
1748
			}
1749
		}
1750
		$messageName = rtrim((($key !== false) ? $key . "-" : "") . (($type !== "") ? $type : ""), "-");
1751
		$restriction = $this->getStateMessageRestriction($messageName, $counter, $thisCounterOnly);
1752
		$stateFolderContents = mapi_folder_getcontentstable($this->stateFolder, MAPI_ASSOCIATED);
1753
		if ($stateFolderContents) {
1754
			mapi_table_restrict($stateFolderContents, $restriction);
1755
			$rowCnt = mapi_table_getrowcount($stateFolderContents);
1756
			SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->CleanStates(): Found %d states to clean (%s)", $rowCnt, $messageName));
1757
			if ($rowCnt > 0) {
1758
				$rows = mapi_table_queryallrows($stateFolderContents, [PR_ENTRYID]);
1759
				$entryids = [];
1760
				foreach ($rows as $row) {
1761
					$entryids[] = $row[PR_ENTRYID];
1762
				}
1763
				mapi_folder_deletemessages($this->stateFolder, $entryids, DELETE_HARD_DELETE);
1764
			}
1765
		}
1766
	}
1767
1768
	/**
1769
	 * Links a user to a device.
1770
	 *
1771
	 * @param string $username
1772
	 * @param string $devid
1773
	 *
1774
	 * @return bool indicating if the user was added or not (existed already)
1775
	 */
1776
	public function LinkUserDevice($username, $devid) {
1777
		$device = [$devid => time()];
1778
		$this->setDeviceUserData($this->type, $device, $username, -1, $subkey = -1, $doCas = "merge");
1779
1780
		return false;
1781
	}
1782
1783
	/**
1784
	 * Unlinks a device from a user.
1785
	 *
1786
	 * @param string $username
1787
	 * @param string $devid
1788
	 *
1789
	 * @return bool
1790
	 */
1791
	public function UnLinkUserDevice($username, $devid) {
1792
		// TODO: Implement
1793
		return false;
1794
	}
1795
1796
	/**
1797
	 * Returns the current version of the state files
1798
	 * grommunio:  This is not relevant atm. IStateMachine::STATEVERSION_02 will match GSync::GetLatestStateVersion().
1799
	 *          If it might be required to update states in the future, this could be implemented on a store level,
1800
	 *          where states are then migrated "on-the-fly"
1801
	 *          or
1802
	 *          in a global settings where all states in all stores are migrated once.
1803
	 *
1804
	 * @return int
1805
	 */
1806
	public function GetStateVersion() {
1807
		return IStateMachine::STATEVERSION_02;
1808
	}
1809
1810
	/**
1811
	 * Sets the current version of the state files.
1812
	 *
1813
	 * @param int $version the new supported version
1814
	 *
1815
	 * @return bool
1816
	 */
1817
	public function SetStateVersion($version) {
1818
		return true;
1819
	}
1820
1821
	/**
1822
	 * Returns MAPIFolder object which contains the state information.
1823
	 * Creates this folder if it is not available yet.
1824
	 *
1825
	 * @param string $devid the device id
1826
	 *
1827
	 * @return MAPIFolder
1828
	 */
1829
	private function getStateFolder($devid) {
1830
		// Options request doesn't send device id
1831
		if (strlen($devid) == 0) {
1832
			return false;
1833
		}
1834
		// Try to get the state folder id from redis
1835
		if (!$this->stateFolder) {
1836
			$folderentryid = $this->getDeviceUserData($this->userDeviceData, $devid, $this->mainUser, "statefolder");
1837
			if ($folderentryid) {
1838
				$this->stateFolder = mapi_msgstore_openentry($this->store, hex2bin($folderentryid));
1839
			}
1840
		}
1841
1842
		// fallback code
1843
		if (!$this->stateFolder && $this->store) {
1844
			SLog::Write(LOGLEVEL_INFO, sprintf("Grommunio->getStateFolder(): state folder not set. Use fallback"));
1845
			$rootfolder = mapi_msgstore_openentry($this->store);
1846
			$hierarchy = mapi_folder_gethierarchytable($rootfolder, CONVENIENT_DEPTH);
1847
			$restriction = $this->getStateFolderRestriction($devid);
1848
			// restrict the hierarchy to the grommunio-sync search folder only
1849
			mapi_table_restrict($hierarchy, $restriction);
1850
			$rowCnt = mapi_table_getrowcount($hierarchy);
1851
			SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->getStateFolder(): found %d device state folders", $rowCnt));
1852
			if ($rowCnt == 1) {
1853
				$hierarchyRows = mapi_table_queryrows($hierarchy, [PR_ENTRYID], 0, 1);
1854
				$this->stateFolder = mapi_msgstore_openentry($this->store, $hierarchyRows[0][PR_ENTRYID]);
1855
				SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->getStateFolder(): %s", bin2hex($hierarchyRows[0][PR_ENTRYID])));
1856
				// put found id in redis
1857
				if ($devid) {
1858
					$this->setDeviceUserData($this->userDeviceData, bin2hex($hierarchyRows[0][PR_ENTRYID]), $devid, $this->mainUser, "statefolder");
1859
				}
1860
			}
1861
			elseif ($rowCnt == 0) {
1862
				// legacy code: create the hidden state folder and the device subfolder
1863
				// this should happen when the user configures the device (autodiscover or first sync if no autodiscover)
1864
1865
				$hierarchy = mapi_folder_gethierarchytable($rootfolder, CONVENIENT_DEPTH);
1866
				$restriction = $this->getStateFolderRestriction(STORE_STATE_FOLDER);
1867
				mapi_table_restrict($hierarchy, $restriction);
1868
				$rowCnt = mapi_table_getrowcount($hierarchy);
1869
				SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->getStateFolder(): found %d store state folders", $rowCnt));
1870
				if ($rowCnt == 1) {
1871
					$hierarchyRows = mapi_table_queryrows($hierarchy, [PR_ENTRYID], 0, 1);
1872
					$stateFolder = mapi_msgstore_openentry($this->store, $hierarchyRows[0][PR_ENTRYID]);
1873
				}
1874
				elseif ($rowCnt == 0) {
1875
					$stateFolder = mapi_folder_createfolder($rootfolder, STORE_STATE_FOLDER, "");
1876
					mapi_setprops($stateFolder, [PR_ATTR_HIDDEN => true]);
1877
				}
1878
1879
				// TODO: handle this
1880
1881
				if (isset($stateFolder) && $stateFolder) {
1882
					$devStateFolder = mapi_folder_createfolder($stateFolder, $devid, "");
1883
					$devStateFolderProps = mapi_getprops($devStateFolder);
1884
					$this->stateFolder = mapi_msgstore_openentry($this->store, $devStateFolderProps[PR_ENTRYID]);
1885
					mapi_setprops($this->stateFolder, [PR_ATTR_HIDDEN => true]);
1886
					// we don't cache the entryid in redis, because this will happen on the next request anyway
1887
				}
1888
1889
				// TODO: unable to create state folder - throw exception
1890
			}
1891
1892
			// This case is rather unlikely that there would be several
1893
				// hidden folders having PR_DISPLAY_NAME the same as device id.
1894
1895
				// TODO: get the hierarchy table again, get entry id of STORE_STATE_FOLDER
1896
				// and compare it to the parent id of those folders.
1897
		}
1898
1899
		return $this->stateFolder;
1900
	}
1901
1902
	/**
1903
	 * Returns the associated MAPIMessage which contains the state information.
1904
	 *
1905
	 * @param string $devid   the device id
1906
	 * @param string $type    the state type
1907
	 * @param string $key     (opt)
1908
	 * @param string $counter state counter
1909
	 *
1910
	 * @throws StateNotFoundException
1911
	 *
1912
	 * @return MAPIMessage
1913
	 */
1914
	private function getStateMessage($devid, $type, $key, $counter) {
1915
		if (!$this->stateFolder) {
1916
			$this->getStateFolder(Request::GetDeviceID());
1917
			if (!$this->stateFolder) {
1918
				throw new StateNotFoundException(sprintf(
1919
					"Grommunio->getStateMessage(): Could not locate the state folder for device '%s'",
1920
					$devid
1921
				));
1922
			}
1923
		}
1924
		$messageName = rtrim((($key !== false) ? $key . "-" : "") . (($type !== "") ? $type : ""), "-");
1925
		$restriction = $this->getStateMessageRestriction($messageName, $counter, true);
1926
		$stateFolderContents = mapi_folder_getcontentstable($this->stateFolder, MAPI_ASSOCIATED);
1927
		if ($stateFolderContents) {
1928
			mapi_table_restrict($stateFolderContents, $restriction);
1929
			$rowCnt = mapi_table_getrowcount($stateFolderContents);
1930
			if ($rowCnt == 1) {
1931
				$stateFolderRows = mapi_table_queryrows($stateFolderContents, [PR_ENTRYID], 0, 1);
1932
1933
				return mapi_msgstore_openentry($this->store, $stateFolderRows[0][PR_ENTRYID]);
1934
			}
1935
		}
1936
1937
		throw new StateNotFoundException(sprintf(
1938
			"Grommunio->getStateMessage(): Could not locate the state message '%s' (counter: %s)",
1939
			$messageName,
1940
			Utils::PrintAsString($counter)
1941
		));
1942
	}
1943
1944
	/**
1945
	 * Writes ta state to for a key and counter.
1946
	 *
1947
	 * @param mixed  $state
1948
	 * @param string $devid   the device id
1949
	 * @param string $type    the state type
1950
	 * @param string $key     (opt)
1951
	 * @param int    $counter (opt)
1952
	 *
1953
	 * @throws StateInvalidException, UnavailableException
1954
	 *
1955
	 * @return bool
1956
	 */
1957
	private function setStateMessage($state, $devid, $type, $key = false, $counter = false) {
1958
		if (!$this->stateFolder) {
1959
			throw new StateNotFoundException(sprintf("Grommunio->setStateMessage(): Could not locate the state folder for device '%s'", $devid));
1960
		}
1961
1962
		try {
1963
			$stateMessage = $this->getStateMessage($devid, $type, $key, $counter);
1964
		}
1965
		catch (StateNotFoundException $e) {
1966
			// if message is not available, try to create a new one
1967
			$stateMessage = mapi_folder_createmessage($this->stateFolder, MAPI_ASSOCIATED);
1968
			SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->setStateMessage(): mapi_folder_createmessage 0x%08X", mapi_last_hresult()));
1969
1970
			$messageName = rtrim((($key !== false) ? $key . "-" : "") . (($type !== "") ? $type : ""), "-");
1971
			SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->setStateMessage(): creating new state message '%s' (counter: %s)", $messageName, Utils::PrintAsString($counter));
0 ignored issues
show
Bug introduced by
A parse error occurred: Syntax error, unexpected ';', expecting ',' or ')' on line 1971 at column 163
Loading history...
1972
			mapi_setprops($stateMessage, [PR_DISPLAY_NAME => $messageName, PR_MESSAGE_CLASS => 'IPM.Note.GrommunioState']);
1973
		}
1974
		if (isset($stateMessage)) {
1975
			$jsonEncodedState = is_object($state) || is_array($state) ? json_encode($state, JSON_INVALID_UTF8_IGNORE | JSON_UNESCAPED_UNICODE) : $state;
1976
1977
			$encodedState = base64_encode($jsonEncodedState);
1978
			$encodedStateLength = strlen($encodedState);
1979
			mapi_setprops($stateMessage, [PR_LAST_VERB_EXECUTED => is_int($counter) ? $counter : 0]);
1980
			$stream = mapi_openproperty($stateMessage, PR_BODY, IID_IStream, STGM_DIRECT, MAPI_CREATE | MAPI_MODIFY);
1981
			mapi_stream_setsize($stream, $encodedStateLength);
1982
			mapi_stream_write($stream, $encodedState);
1983
			mapi_stream_commit($stream);
1984
			mapi_savechanges($stateMessage);
1985
1986
			return $encodedStateLength;
1987
		}
1988
1989
		return false;
1990
	}
1991
1992
	/**
1993
	 * Returns the restriction for the state folder name.
1994
	 *
1995
	 * @param string $folderName the state folder name
1996
	 *
1997
	 * @return array
1998
	 */
1999
	private function getStateFolderRestriction($folderName) {
2000
		return [RES_AND, [
2001
			[RES_PROPERTY,
2002
				[RELOP => RELOP_EQ,
2003
					ULPROPTAG => PR_DISPLAY_NAME,
2004
					VALUE => $folderName,
2005
				],
2006
			],
2007
			[RES_PROPERTY,
2008
				[RELOP => RELOP_EQ,
2009
					ULPROPTAG => PR_ATTR_HIDDEN,
2010
					VALUE => true,
2011
				],
2012
			],
2013
		]];
2014
	}
2015
2016
	/**
2017
	 * Returns the restriction for the associated message in the state folder.
2018
	 *
2019
	 * @param string $messageName     the message name
2020
	 * @param string $counter         counter
2021
	 * @param string $thisCounterOnly (opt) if provided, restrict to the exact counter
2022
	 *
2023
	 * @return array
2024
	 */
2025
	private function getStateMessageRestriction($messageName, $counter, $thisCounterOnly = false) {
2026
		return [RES_AND, [
2027
			[RES_PROPERTY,
2028
				[RELOP => RELOP_EQ,
2029
					ULPROPTAG => PR_DISPLAY_NAME,
2030
					VALUE => $messageName,
2031
				],
2032
			],
2033
			[RES_PROPERTY,
2034
				[RELOP => RELOP_EQ,
2035
					ULPROPTAG => PR_MESSAGE_CLASS,
2036
					VALUE => 'IPM.Note.GrommunioState',
2037
				],
2038
			],
2039
			[RES_PROPERTY,
2040
				[RELOP => $thisCounterOnly ? RELOP_EQ : RELOP_LT,
2041
					ULPROPTAG => PR_LAST_VERB_EXECUTED,
2042
					VALUE => $counter,
2043
				],
2044
			],
2045
		]];
2046
	}
2047
2048
	/*----------------------------------------------------------------------------------------------------------
2049
	 * Private methods
2050
	 */
2051
2052
	/**
2053
	 * Returns a hash representing changes in the hierarchy of the main user.
2054
	 * It changes if a folder is added, renamed or deleted.
2055
	 *
2056
	 * @return string
2057
	 */
2058
	private function getHierarchyHash() {
2059
		$rootfolder = mapi_msgstore_openentry($this->defaultstore);
2060
		$hierarchy = mapi_folder_gethierarchytable($rootfolder, CONVENIENT_DEPTH);
2061
2062
		return md5(serialize(mapi_table_queryallrows($hierarchy, [PR_DISPLAY_NAME, PR_PARENT_ENTRYID])));
2063
	}
2064
2065
	/**
2066
	 * Advises a store to the changes sink.
2067
	 *
2068
	 * @param mapistore $store store to be advised
2069
	 *
2070
	 * @return bool
2071
	 */
2072
	private function adviseStoreToSink($store) {
2073
		// check if we already advised the store
2074
		if (!in_array($store, $this->changesSinkStores)) {
2075
			mapi_msgstore_advise($store, null, fnevNewMail |fnevObjectModified | fnevObjectCreated | fnevObjectMoved | fnevObjectDeleted, $this->changesSink);
2076
2077
			if (mapi_last_hresult()) {
2078
				SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->adviseStoreToSink(): failed to advised store '%s' with code 0x%X. Polling will be performed.", $store, mapi_last_hresult()));
2079
2080
				return false;
2081
			}
2082
2083
			SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->adviseStoreToSink(): advised store '%s'", $store));
2084
			$this->changesSinkStores[] = $store;
2085
		}
2086
2087
		return true;
2088
	}
2089
2090
	/**
2091
	 * Open the store marked with PR_DEFAULT_STORE = TRUE
2092
	 * if $return_public is set, the public store is opened.
2093
	 *
2094
	 * @param string $user User which store should be opened
2095
	 *
2096
	 * @return bool
2097
	 */
2098
	private function openMessageStore($user) {
2099
		// During PING requests the operations store has to be switched constantly
2100
		// the cache prevents the same store opened several times
2101
		if (isset($this->storeCache[$user])) {
2102
			return $this->storeCache[$user];
2103
		}
2104
2105
		$entryid = false;
2106
		$return_public = false;
2107
2108
		if (strtoupper($user) == 'SYSTEM') {
2109
			$return_public = true;
2110
		}
2111
2112
		// loop through the storestable if authenticated user of public folder
2113
		if ($user == $this->mainUser || $return_public === true) {
2114
			// Find the default store
2115
			$storestables = mapi_getmsgstorestable($this->session);
2116
			$result = mapi_last_hresult();
2117
2118
			if ($result == NOERROR) {
2119
				$rows = mapi_table_queryallrows($storestables, [PR_ENTRYID, PR_DEFAULT_STORE, PR_MDB_PROVIDER]);
2120
2121
				foreach ($rows as $row) {
2122
					if (!$return_public && isset($row[PR_DEFAULT_STORE]) && $row[PR_DEFAULT_STORE] == true) {
2123
						$entryid = $row[PR_ENTRYID];
2124
2125
						break;
2126
					}
2127
					if ($return_public && isset($row[PR_MDB_PROVIDER]) && $row[PR_MDB_PROVIDER] == ZARAFA_STORE_PUBLIC_GUID) {
2128
						$entryid = $row[PR_ENTRYID];
2129
2130
						break;
2131
					}
2132
				}
2133
			}
2134
		}
2135
		else {
2136
			$entryid = @mapi_msgstore_createentryid($this->defaultstore, $user);
2137
		}
2138
2139
		if ($entryid) {
2140
			$store = @mapi_openmsgstore($this->session, $entryid);
2141
2142
			if (!$store) {
2143
				SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->openMessageStore('%s'): Could not open store", $user));
2144
2145
				return false;
2146
			}
2147
2148
			// add this store to the cache
2149
			if (!isset($this->storeCache[$user])) {
2150
				$this->storeCache[$user] = $store;
2151
			}
2152
2153
			SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->openMessageStore('%s'): Found '%s' store: '%s'", $user, (($return_public) ? 'PUBLIC' : 'DEFAULT'), $store));
2154
2155
			return $store;
2156
		}
2157
2158
		SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->openMessageStore('%s'): No store found for this user", $user));
2159
2160
		return false;
2161
	}
2162
2163
	/**
2164
	 * Checks if the logged in user has secretary permissions on a folder.
2165
	 *
2166
	 * @param resource $store
2167
	 * @param string   $folderid
2168
	 * @param mixed    $entryid
2169
	 *
2170
	 * @return bool
2171
	 */
2172
	public function HasSecretaryACLs($store, $folderid, $entryid = false) {
2173
		if (!$entryid) {
2174
			$entryid = mapi_msgstore_entryidfromsourcekey($store, hex2bin($folderid));
2175
			if (!$entryid) {
2176
				SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->HasSecretaryACLs(): error, no entryid resolved for %s on store %s", $folderid, $store));
2177
2178
				return false;
2179
			}
2180
		}
2181
2182
		$folder = mapi_msgstore_openentry($store, $entryid);
2183
		if (!$folder) {
2184
			SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->HasSecretaryACLs(): error, could not open folder with entryid %s on store %s", bin2hex($entryid), $store));
2185
2186
			return false;
2187
		}
2188
2189
		$props = mapi_getprops($folder, [PR_RIGHTS]);
2190
		if (isset($props[PR_RIGHTS]) &&
2191
			($props[PR_RIGHTS] & ecRightsReadAny) &&
2192
			($props[PR_RIGHTS] & ecRightsCreate) &&
2193
			($props[PR_RIGHTS] & ecRightsEditOwned) &&
2194
			($props[PR_RIGHTS] & ecRightsDeleteOwned) &&
2195
			($props[PR_RIGHTS] & ecRightsEditAny) &&
2196
			($props[PR_RIGHTS] & ecRightsDeleteAny) &&
2197
			($props[PR_RIGHTS] & ecRightsFolderVisible)) {
2198
			return true;
2199
		}
2200
2201
		return false;
2202
	}
2203
2204
	/**
2205
	 * The meta function for out of office settings.
2206
	 *
2207
	 * @param SyncObject $oof
2208
	 */
2209
	private function settingsOOF(&$oof) {
2210
		// if oof state is set it must be set of oof and get otherwise
2211
		if (isset($oof->oofstate)) {
2212
			$this->settingsOofSet($oof);
2213
		}
2214
		else {
2215
			$this->settingsOofGet($oof);
2216
		}
2217
	}
2218
2219
	/**
2220
	 * Gets the out of office settings.
2221
	 *
2222
	 * @param SyncObject $oof
2223
	 */
2224
	private function settingsOofGet(&$oof) {
2225
		$oofprops = mapi_getprops($this->defaultstore, [PR_EC_OUTOFOFFICE, PR_EC_OUTOFOFFICE_MSG, PR_EC_OUTOFOFFICE_SUBJECT, PR_EC_OUTOFOFFICE_FROM, PR_EC_OUTOFOFFICE_UNTIL]);
2226
		$oof->oofstate = SYNC_SETTINGSOOF_DISABLED;
2227
		$oof->Status = SYNC_SETTINGSSTATUS_SUCCESS;
2228
		if ($oofprops != false) {
2229
			$oof->oofstate = isset($oofprops[PR_EC_OUTOFOFFICE]) ? ($oofprops[PR_EC_OUTOFOFFICE] ? SYNC_SETTINGSOOF_GLOBAL : SYNC_SETTINGSOOF_DISABLED) : SYNC_SETTINGSOOF_DISABLED;
2230
			// TODO external and external unknown
2231
			$oofmessage = new SyncOOFMessage();
2232
			$oofmessage->appliesToInternal = "";
2233
			$oofmessage->enabled = $oof->oofstate;
2234
			$oofmessage->replymessage = (isset($oofprops[PR_EC_OUTOFOFFICE_MSG])) ? w2u($oofprops[PR_EC_OUTOFOFFICE_MSG]) : "";
2235
			$oofmessage->bodytype = $oof->bodytype;
2236
			unset($oofmessage->appliesToExternal, $oofmessage->appliesToExternalUnknown);
2237
			$oof->oofmessage[] = $oofmessage;
2238
2239
			// check whether time based out of office is set
2240
			if ($oof->oofstate == SYNC_SETTINGSOOF_GLOBAL && isset($oofprops[PR_EC_OUTOFOFFICE_FROM], $oofprops[PR_EC_OUTOFOFFICE_UNTIL])) {
2241
				$now = time();
2242
				if ($now > $oofprops[PR_EC_OUTOFOFFICE_FROM] && $now > $oofprops[PR_EC_OUTOFOFFICE_UNTIL]) {
2243
					// Out of office is set but the date is in the past. Set the state to disabled.
2244
					// @see https://jira.z-hub.io/browse/ZP-1188 for details
2245
					$oof->oofstate = SYNC_SETTINGSOOF_DISABLED;
2246
					@mapi_setprops($this->defaultstore, [PR_EC_OUTOFOFFICE => false]);
2247
					@mapi_deleteprops($this->defaultstore, [PR_EC_OUTOFOFFICE_FROM, PR_EC_OUTOFOFFICE_UNTIL]);
2248
					SLog::Write(LOGLEVEL_INFO, "Grommunio->settingsOofGet(): Out of office is set but the from and until are in the past. Disabling out of office.");
2249
				}
2250
				elseif ($oofprops[PR_EC_OUTOFOFFICE_FROM] < $oofprops[PR_EC_OUTOFOFFICE_UNTIL]) {
2251
					$oof->oofstate = SYNC_SETTINGSOOF_TIMEBASED;
2252
					$oof->starttime = $oofprops[PR_EC_OUTOFOFFICE_FROM];
2253
					$oof->endtime = $oofprops[PR_EC_OUTOFOFFICE_UNTIL];
2254
				}
2255
				else {
2256
					SLog::Write(LOGLEVEL_WARN, sprintf(
2257
						"Grommunio->settingsOofGet(): Time based out of office set but end time ('%s') is before startime ('%s').",
2258
						date("Y-m-d H:i:s", $oofprops[PR_EC_OUTOFOFFICE_FROM]),
2259
						date("Y-m-d H:i:s", $oofprops[PR_EC_OUTOFOFFICE_UNTIL])
2260
					));
2261
					$oof->Status = SYNC_SETTINGSSTATUS_PROTOCOLLERROR;
2262
				}
2263
			}
2264
			elseif ($oof->oofstate == SYNC_SETTINGSOOF_GLOBAL && (isset($oofprops[PR_EC_OUTOFOFFICE_FROM]) || isset($oofprops[PR_EC_OUTOFOFFICE_UNTIL]))) {
2265
				SLog::Write(LOGLEVEL_WARN, sprintf(
2266
					"Grommunio->settingsOofGet(): Time based out of office set but either start time ('%s') or end time ('%s') is missing.",
2267
					(isset($oofprops[PR_EC_OUTOFOFFICE_FROM]) ? date("Y-m-d H:i:s", $oofprops[PR_EC_OUTOFOFFICE_FROM]) : 'empty'),
2268
					(isset($oofprops[PR_EC_OUTOFOFFICE_UNTIL]) ? date("Y-m-d H:i:s", $oofprops[PR_EC_OUTOFOFFICE_UNTIL]) : 'empty')
2269
				));
2270
				$oof->Status = SYNC_SETTINGSSTATUS_PROTOCOLLERROR;
2271
			}
2272
		}
2273
		else {
2274
			SLog::Write(LOGLEVEL_WARN, "Grommunio->Unable to get out of office information");
2275
		}
2276
2277
		// unset body type for oof in order not to stream it
2278
		unset($oof->bodytype);
2279
	}
2280
2281
	/**
2282
	 * Sets the out of office settings.
2283
	 *
2284
	 * @param SyncObject $oof
2285
	 */
2286
	private function settingsOofSet(&$oof) {
2287
		$oof->Status = SYNC_SETTINGSSTATUS_SUCCESS;
2288
		$props = [];
2289
		if ($oof->oofstate == SYNC_SETTINGSOOF_GLOBAL || $oof->oofstate == SYNC_SETTINGSOOF_TIMEBASED) {
2290
			$props[PR_EC_OUTOFOFFICE] = true;
2291
			foreach ($oof->oofmessage as $oofmessage) {
2292
				if (isset($oofmessage->appliesToInternal)) {
2293
					$props[PR_EC_OUTOFOFFICE_MSG] = isset($oofmessage->replymessage) ? u2w($oofmessage->replymessage) : "";
2294
					$props[PR_EC_OUTOFOFFICE_SUBJECT] = "Out of office";
2295
				}
2296
			}
2297
			if ($oof->oofstate == SYNC_SETTINGSOOF_TIMEBASED) {
2298
				if (isset($oof->starttime, $oof->endtime)) {
2299
					$props[PR_EC_OUTOFOFFICE_FROM] = $oof->starttime;
2300
					$props[PR_EC_OUTOFOFFICE_UNTIL] = $oof->endtime;
2301
				}
2302
				elseif (isset($oof->starttime) || isset($oof->endtime)) {
2303
					$oof->Status = SYNC_SETTINGSSTATUS_PROTOCOLLERROR;
2304
				}
2305
			}
2306
			else {
2307
				$deleteProps = [PR_EC_OUTOFOFFICE_FROM, PR_EC_OUTOFOFFICE_UNTIL];
2308
			}
2309
		}
2310
		elseif ($oof->oofstate == SYNC_SETTINGSOOF_DISABLED) {
2311
			$props[PR_EC_OUTOFOFFICE] = false;
2312
			$deleteProps = [PR_EC_OUTOFOFFICE_FROM, PR_EC_OUTOFOFFICE_UNTIL];
2313
		}
2314
2315
		if (!empty($props)) {
2316
			@mapi_setprops($this->defaultstore, $props);
2317
			$result = mapi_last_hresult();
2318
			if ($result != NOERROR) {
2319
				SLog::Write(LOGLEVEL_ERROR, sprintf("Grommunio->settingsOofSet(): Setting oof information failed (%X)", $result));
2320
2321
				return false;
2322
			}
2323
		}
2324
2325
		if (!empty($deleteProps)) {
2326
			@mapi_deleteprops($this->defaultstore, $deleteProps);
2327
		}
2328
2329
		return true;
2330
	}
2331
2332
	/**
2333
	 * Gets the user's email address from server.
2334
	 *
2335
	 * @param SyncObject $userinformation
2336
	 */
2337
	private function settingsUserInformation(&$userinformation) {
2338
		if (!isset($this->defaultstore) || !isset($this->mainUser)) {
2339
			SLog::Write(LOGLEVEL_ERROR, "Grommunio->settingsUserInformation(): The store or user are not available for getting user information");
2340
2341
			return false;
2342
		}
2343
		$user = nsp_getuserinfo($this->mainUser);
2344
		if ($user != false) {
2345
			$userinformation->Status = SYNC_SETTINGSSTATUS_USERINFO_SUCCESS;
2346
			if (Request::GetProtocolVersion() >= 14.1) {
2347
				$account = new SyncAccount();
2348
				$emailaddresses = new SyncEmailAddresses();
2349
				$emailaddresses->smtpaddress[] = $user["primary_email"];
2350
				$emailaddresses->primarysmtpaddress = $user["primary_email"];
2351
				$account->emailaddresses = $emailaddresses;
2352
				$userinformation->accounts[] = $account;
2353
			}
2354
			else {
2355
				$userinformation->emailaddresses[] = $user["primary_email"];
2356
			}
2357
2358
			return true;
2359
		}
2360
		SLog::Write(LOGLEVEL_ERROR, sprintf("Grommunio->settingsUserInformation(): Getting user information failed: nsp_getuserinfo(%X)", mapi_last_hresult()));
2361
2362
		return false;
2363
	}
2364
2365
	/**
2366
	 * Gets the rights management templates from the server.
2367
	 *
2368
	 * @param SyncObject $rmTemplates
2369
	 */
2370
	private function settingsRightsManagementTemplates(&$rmTemplates) {
2371
		/* Currently there is no information rights management feature in
2372
		 * the grommunio backend, so just return the status and empty
2373
		 * SyncRightsManagementTemplates tag.
2374
		 * Once it's available, it would be something like:
2375
2376
		$rmTemplate = new SyncRightsManagementTemplate();
2377
		$rmTemplate->id = "some-template-id-eg-guid";
2378
		$rmTemplate->name = "Template name";
2379
		$rmTemplate->description = "What does the template do. E.g. it disables forward and reply.";
2380
		$rmTemplates->rmtemplates[] = $rmTemplate;
2381
		 */
2382
		$rmTemplates->Status = SYNC_COMMONSTATUS_IRMFEATUREDISABLED;
2383
		$rmTemplates->rmtemplates = [];
2384
	}
2385
2386
	/**
2387
	 * Sets the importance and priority of a message from a RFC822 message headers.
2388
	 *
2389
	 * @param int   $xPriority
2390
	 * @param array $mapiprops
2391
	 * @param mixed $sendMailProps
2392
	 */
2393
	private function getImportanceAndPriority($xPriority, &$mapiprops, $sendMailProps) {
2394
		switch ($xPriority) {
2395
			case 1:
2396
			case 2:
2397
				$priority = PRIO_URGENT;
2398
				$importance = IMPORTANCE_HIGH;
2399
				break;
2400
2401
			case 4:
2402
			case 5:
2403
				$priority = PRIO_NONURGENT;
2404
				$importance = IMPORTANCE_LOW;
2405
				break;
2406
2407
			case 3:
2408
			default:
2409
				$priority = PRIO_NORMAL;
2410
				$importance = IMPORTANCE_NORMAL;
2411
				break;
2412
		}
2413
		$mapiprops[$sendMailProps["importance"]] = $importance;
2414
		$mapiprops[$sendMailProps["priority"]] = $priority;
2415
	}
2416
2417
	/**
2418
	 * Copies attachments from one message to another.
2419
	 *
2420
	 * @param MAPIMessage $toMessage
2421
	 * @param MAPIMessage $fromMessage
2422
	 */
2423
	private function copyAttachments(&$toMessage, $fromMessage) {
2424
		$attachtable = mapi_message_getattachmenttable($fromMessage);
2425
		$rows = mapi_table_queryallrows($attachtable, [PR_ATTACH_NUM]);
2426
2427
		foreach ($rows as $row) {
2428
			if (isset($row[PR_ATTACH_NUM])) {
2429
				$attach = mapi_message_openattach($fromMessage, $row[PR_ATTACH_NUM]);
2430
				$newattach = mapi_message_createattach($toMessage);
2431
				mapi_copyto($attach, [], [], $newattach, 0);
2432
				mapi_savechanges($newattach);
2433
			}
2434
		}
2435
	}
2436
2437
	/**
2438
	 * Function will create a search folder in FINDER_ROOT folder
2439
	 * if folder exists then it will open it.
2440
	 *
2441
	 * @see createSearchFolder($store, $openIfExists = true) function in the webaccess
2442
	 *
2443
	 * @return mapiFolderObject $folder created search folder
2444
	 */
2445
	private function getSearchFolder() {
2446
		// create new or open existing search folder
2447
		$searchFolderRoot = $this->getSearchFoldersRoot($this->store);
2448
		if ($searchFolderRoot === false) {
2449
			// error in finding search root folder
2450
			// or store doesn't support search folders
2451
			return false;
2452
		}
2453
2454
		$searchFolder = $this->createSearchFolder($searchFolderRoot);
2455
2456
		if ($searchFolder !== false && mapi_last_hresult() == NOERROR) {
2457
			return $searchFolder;
2458
		}
2459
2460
		return false;
2461
	}
2462
2463
	/**
2464
	 * Function will open FINDER_ROOT folder in root container
2465
	 * public folder's don't have FINDER_ROOT folder.
2466
	 *
2467
	 * @see getSearchFoldersRoot($store) function in the webaccess
2468
	 *
2469
	 * @return mapiFolderObject root folder for search folders
2470
	 */
2471
	private function getSearchFoldersRoot() {
2472
		// check if we can create search folders
2473
		$storeProps = mapi_getprops($this->store, [PR_STORE_SUPPORT_MASK, PR_FINDER_ENTRYID]);
2474
		if (($storeProps[PR_STORE_SUPPORT_MASK] & STORE_SEARCH_OK) != STORE_SEARCH_OK) {
2475
			SLog::Write(LOGLEVEL_WARN, "Grommunio->getSearchFoldersRoot(): Store doesn't support search folders. Public store doesn't have FINDER_ROOT folder");
2476
2477
			return false;
2478
		}
2479
2480
		// open search folders root
2481
		$searchRootFolder = mapi_msgstore_openentry($this->store, $storeProps[PR_FINDER_ENTRYID]);
2482
		if (mapi_last_hresult() != NOERROR) {
2483
			SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->getSearchFoldersRoot(): Unable to open search folder (0x%X)", mapi_last_hresult()));
2484
2485
			return false;
2486
		}
2487
2488
		return $searchRootFolder;
2489
	}
2490
2491
	/**
2492
	 * Creates a search folder if it not exists or opens an existing one
2493
	 * and returns it.
2494
	 *
2495
	 * @param mapiFolderObject $searchFolderRoot
2496
	 *
2497
	 * @return mapiFolderObject
2498
	 */
2499
	private function createSearchFolder($searchFolderRoot) {
2500
		$folderName = "grommunio-sync Search Folder " . @getmypid();
2501
		$searchFolders = mapi_folder_gethierarchytable($searchFolderRoot);
2502
		$restriction = [
2503
			RES_CONTENT,
2504
			[
2505
				FUZZYLEVEL => FL_PREFIX,
2506
				ULPROPTAG => PR_DISPLAY_NAME,
2507
				VALUE => [PR_DISPLAY_NAME => $folderName],
2508
			],
2509
		];
2510
		// restrict the hierarchy to the grommunio-sync search folder only
2511
		mapi_table_restrict($searchFolders, $restriction);
2512
		if (mapi_table_getrowcount($searchFolders)) {
2513
			$searchFolder = mapi_table_queryrows($searchFolders, [PR_ENTRYID], 0, 1);
2514
2515
			return mapi_msgstore_openentry($this->store, $searchFolder[0][PR_ENTRYID]);
2516
		}
2517
2518
		return mapi_folder_createfolder($searchFolderRoot, $folderName, null, 0, FOLDER_SEARCH);
2519
	}
2520
2521
	/**
2522
	 * Creates a search restriction.
2523
	 *
2524
	 * @param ContentParameter $cpo
2525
	 *
2526
	 * @return array
2527
	 */
2528
	private function getSearchRestriction($cpo) {
2529
		$searchText = $cpo->GetSearchFreeText();
2530
2531
		$searchGreater = strtotime($cpo->GetSearchValueGreater());
2532
		$searchLess = strtotime($cpo->GetSearchValueLess());
2533
2534
		if (version_compare(phpversion(), '5.3.4') < 0) {
2535
			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()));
2536
		}
2537
		// split the search on whitespache and look for every word
2538
		$searchText = preg_split("/\\W+/u", $searchText);
2539
		$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];
2540
		$resAnd = [];
2541
		foreach ($searchText as $term) {
2542
			$resOr = [];
2543
2544
			foreach ($searchProps as $property) {
2545
				array_push(
2546
					$resOr,
2547
					[RES_CONTENT,
2548
						[
2549
							FUZZYLEVEL => FL_SUBSTRING | FL_IGNORECASE,
2550
							ULPROPTAG => $property,
2551
							VALUE => u2w($term),
2552
						],
2553
					]
2554
				);
2555
			}
2556
			array_push($resAnd, [RES_OR, $resOr]);
2557
		}
2558
2559
		// add time range restrictions
2560
		if ($searchGreater) {
2561
			array_push($resAnd, [RES_PROPERTY, [RELOP => RELOP_GE, ULPROPTAG => PR_MESSAGE_DELIVERY_TIME, VALUE => [PR_MESSAGE_DELIVERY_TIME => $searchGreater]]]); // RES_AND;
2562
		}
2563
		if ($searchLess) {
2564
			array_push($resAnd, [RES_PROPERTY, [RELOP => RELOP_LE, ULPROPTAG => PR_MESSAGE_DELIVERY_TIME, VALUE => [PR_MESSAGE_DELIVERY_TIME => $searchLess]]]);
2565
		}
2566
2567
		return [RES_AND, $resAnd];
2568
	}
2569
2570
	/**
2571
	 * Resolve recipient based on his email address.
2572
	 *
2573
	 * @param string $to
2574
	 * @param int    $maxAmbiguousRecipients
2575
	 * @param bool   $expandDistlist
2576
	 *
2577
	 * @return bool|SyncResolveRecipient
2578
	 */
2579
	private function resolveRecipient($to, $maxAmbiguousRecipients, $expandDistlist = true) {
2580
		$recipient = $this->resolveRecipientGAL($to, $maxAmbiguousRecipients, $expandDistlist);
2581
2582
		if ($recipient !== false) {
2583
			return $recipient;
2584
		}
2585
2586
		$recipient = $this->resolveRecipientContact($to, $maxAmbiguousRecipients);
2587
2588
		if ($recipient !== false) {
2589
			return $recipient;
2590
		}
2591
2592
		return false;
2593
	}
2594
2595
	/**
2596
	 * Resolves recipient from the GAL and gets his certificates.
2597
	 *
2598
	 * @param string $to
2599
	 * @param int    $maxAmbiguousRecipients
2600
	 * @param bool   $expandDistlist
2601
	 *
2602
	 * @return array|bool
2603
	 */
2604
	private function resolveRecipientGAL($to, $maxAmbiguousRecipients, $expandDistlist = true) {
2605
		SLog::Write(LOGLEVEL_WBXML, sprintf("Grommunio->resolveRecipientGAL(): Resolving recipient '%s' in GAL", $to));
2606
		$addrbook = $this->getAddressbook();
2607
		// FIXME: create a function to get the adressbook contentstable
2608
		$ab_entryid = mapi_ab_getdefaultdir($addrbook);
2609
		if ($ab_entryid) {
2610
			$ab_dir = mapi_ab_openentry($addrbook, $ab_entryid);
2611
		}
2612
		if ($ab_dir) {
2613
			$table = mapi_folder_getcontentstable($ab_dir);
2614
		}
2615
2616
		if (!$table) {
2617
			SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->resolveRecipientGAL(): Unable to open addressbook:0x%X", mapi_last_hresult()));
2618
2619
			return false;
2620
		}
2621
2622
		$restriction = MAPIUtils::GetSearchRestriction(u2w($to));
2623
		mapi_table_restrict($table, $restriction);
2624
2625
		$querycnt = mapi_table_getrowcount($table);
2626
		if ($querycnt > 0) {
2627
			$recipientGal = [];
2628
			$rowsToQuery = $maxAmbiguousRecipients;
2629
			// some devices request 0 ambiguous recipients
2630
			if ($querycnt == 1 && $maxAmbiguousRecipients == 0) {
2631
				$rowsToQuery = 1;
2632
			}
2633
			elseif ($querycnt > 1 && $maxAmbiguousRecipients == 0) {
2634
				SLog::Write(LOGLEVEL_INFO, sprintf("Grommunio->resolveRecipientGAL(): GAL search found %d recipients but the device hasn't requested ambiguous recipients", $querycnt));
2635
2636
				return $recipientGal;
2637
			}
2638
			elseif ($querycnt > 1 && $maxAmbiguousRecipients == 1) {
2639
				$rowsToQuery = $querycnt;
2640
			}
2641
			// get the certificate every time because caching the certificate is less expensive than opening addressbook entry again
2642
			$abentries = mapi_table_queryrows($table, [PR_ENTRYID, PR_DISPLAY_NAME, PR_EMS_AB_TAGGED_X509_CERT, PR_OBJECT_TYPE, PR_SMTP_ADDRESS], 0, $rowsToQuery);
2643
			for ($i = 0, $nrEntries = count($abentries); $i < $nrEntries; ++$i) {
2644
				if (strcasecmp($abentries[$i][PR_SMTP_ADDRESS], $to) !== 0 && $maxAmbiguousRecipients == 1) {
2645
					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]));
2646
2647
					continue;
2648
				}
2649
				if ($abentries[$i][PR_OBJECT_TYPE] == MAPI_DISTLIST) {
2650
					// check whether to expand dist list
2651
					if ($expandDistlist) {
2652
						SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->resolveRecipientGAL(): '%s' is a dist list. Expand it to members.", $to));
2653
						$distList = mapi_ab_openentry($addrbook, $abentries[$i][PR_ENTRYID]);
2654
						$distListContent = mapi_folder_getcontentstable($distList);
2655
						$distListMembers = mapi_table_queryallrows($distListContent, [PR_ENTRYID, PR_DISPLAY_NAME, PR_EMS_AB_TAGGED_X509_CERT]);
2656
						for ($j = 0, $nrDistListMembers = mapi_table_getrowcount($distListContent); $j < $nrDistListMembers; ++$j) {
2657
							SLog::Write(LOGLEVEL_WBXML, sprintf("Grommunio->resolveRecipientGAL(): distlist's '%s' member: '%s'", $to, $distListMembers[$j][PR_DISPLAY_NAME]));
2658
							$recipientGal[] = $this->createResolveRecipient(SYNC_RESOLVERECIPIENTS_TYPE_GAL, $to, $distListMembers[$j], $nrDistListMembers);
2659
						}
2660
					}
2661
					else {
2662
						SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->resolveRecipientGAL(): '%s' is a dist list, but return it as is.", $to));
2663
						$recipientGal[] = $this->createResolveRecipient(SYNC_RESOLVERECIPIENTS_TYPE_GAL, $abentries[$i][PR_SMTP_ADDRESS], $abentries[$i]);
2664
					}
2665
				}
2666
				elseif ($abentries[$i][PR_OBJECT_TYPE] == MAPI_MAILUSER) {
2667
					$recipientGal[] = $this->createResolveRecipient(SYNC_RESOLVERECIPIENTS_TYPE_GAL, $abentries[$i][PR_SMTP_ADDRESS], $abentries[$i]);
2668
				}
2669
			}
2670
2671
			SLog::Write(LOGLEVEL_WBXML, "Grommunio->resolveRecipientGAL(): Found a recipient in GAL");
2672
2673
			return $recipientGal;
2674
		}
2675
2676
		SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->resolveRecipientGAL(): No recipient found for: '%s' in GAL", $to));
2677
2678
		return SYNC_RESOLVERECIPSSTATUS_RESPONSE_UNRESOLVEDRECIP;
2679
2680
		return false;
2681
	}
2682
2683
	/**
2684
	 * Resolves recipient from the contact list and gets his certificates.
2685
	 *
2686
	 * @param string $to
2687
	 * @param int    $maxAmbiguousRecipients
2688
	 *
2689
	 * @return array|bool
2690
	 */
2691
	private function resolveRecipientContact($to, $maxAmbiguousRecipients) {
2692
		SLog::Write(LOGLEVEL_WBXML, sprintf("Grommunio->resolveRecipientContact(): Resolving recipient '%s' in user's contacts", $to));
2693
		// go through all contact folders of the user and
2694
		// check if there's a contact with the given email address
2695
		$root = mapi_msgstore_openentry($this->defaultstore);
2696
		if (!$root) {
2697
			SLog::Write(LOGLEVEL_ERROR, sprintf("Grommunio->resolveRecipientContact(): Unable to open default store: 0x%X", mapi_last_hresult()));
2698
		}
2699
		$rootprops = mapi_getprops($root, [PR_IPM_CONTACT_ENTRYID]);
2700
		$contacts = $this->getContactsFromFolder($this->defaultstore, $rootprops[PR_IPM_CONTACT_ENTRYID], $to);
2701
		$recipients = [];
2702
2703
		if ($contacts !== false) {
2704
			SLog::Write(LOGLEVEL_WBXML, sprintf("Grommunio->resolveRecipientContact(): Found %d contacts in main contacts folder.", count($contacts)));
2705
			// create resolve recipient object
2706
			foreach ($contacts as $contact) {
2707
				$recipients[] = $this->createResolveRecipient(SYNC_RESOLVERECIPIENTS_TYPE_CONTACT, $to, $contact);
2708
			}
2709
		}
2710
2711
		$contactfolder = mapi_msgstore_openentry($this->defaultstore, $rootprops[PR_IPM_CONTACT_ENTRYID]);
2712
		$subfolders = MAPIUtils::GetSubfoldersForType($contactfolder, "IPF.Contact");
2713
		if ($subfolders !== false) {
2714
			foreach ($subfolders as $folder) {
2715
				$contacts = $this->getContactsFromFolder($this->defaultstore, $folder[PR_ENTRYID], $to);
2716
				if ($contacts !== false) {
2717
					SLog::Write(LOGLEVEL_WBXML, sprintf("Grommunio->resolveRecipientContact(): Found %d contacts in contacts' subfolder.", count($contacts)));
2718
					foreach ($contacts as $contact) {
2719
						$recipients[] = $this->createResolveRecipient(SYNC_RESOLVERECIPIENTS_TYPE_CONTACT, $to, $contact);
2720
					}
2721
				}
2722
			}
2723
		}
2724
2725
		// search contacts in public folders
2726
		$storestables = mapi_getmsgstorestable($this->session);
2727
		$result = mapi_last_hresult();
2728
2729
		if ($result == NOERROR) {
2730
			$rows = mapi_table_queryallrows($storestables, [PR_ENTRYID, PR_DEFAULT_STORE, PR_MDB_PROVIDER]);
2731
			foreach ($rows as $row) {
2732
				if (isset($row[PR_MDB_PROVIDER]) && $row[PR_MDB_PROVIDER] == ZARAFA_STORE_PUBLIC_GUID) {
2733
					// TODO refactor public store
2734
					$publicstore = mapi_openmsgstore($this->session, $row[PR_ENTRYID]);
2735
					$publicfolder = mapi_msgstore_openentry($publicstore);
2736
2737
					$subfolders = MAPIUtils::GetSubfoldersForType($publicfolder, "IPF.Contact");
2738
					if ($subfolders !== false) {
2739
						foreach ($subfolders as $folder) {
2740
							$contacts = $this->getContactsFromFolder($publicstore, $folder[PR_ENTRYID], $to);
2741
							if ($contacts !== false) {
2742
								SLog::Write(LOGLEVEL_WBXML, sprintf("Grommunio->resolveRecipientContact(): Found %d contacts in public contacts folder.", count($contacts)));
2743
								foreach ($contacts as $contact) {
2744
									$recipients[] = $this->createResolveRecipient(SYNC_RESOLVERECIPIENTS_TYPE_CONTACT, $to, $contact);
2745
								}
2746
							}
2747
						}
2748
					}
2749
2750
					break;
2751
				}
2752
			}
2753
		}
2754
		else {
2755
			SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->resolveRecipientContact(): Unable to open public store: 0x%X", $result));
2756
		}
2757
2758
		if (empty($recipients)) {
2759
			$contactProperties = [];
2760
			$contactProperties[PR_DISPLAY_NAME] = $to;
2761
			$contactProperties[PR_USER_X509_CERTIFICATE] = false;
2762
2763
			$recipients[] = $this->createResolveRecipient(SYNC_RESOLVERECIPIENTS_TYPE_CONTACT, $to, $contactProperties);
2764
		}
2765
2766
		return $recipients;
2767
	}
2768
2769
	/**
2770
	 * Creates SyncResolveRecipientsCertificates object for ResolveRecipients.
2771
	 *
2772
	 * @param binary $certificates
2773
	 * @param int    $recipientCount
2774
	 *
2775
	 * @return SyncResolveRecipientsCertificates
2776
	 */
2777
	private function getCertificates($certificates, $recipientCount = 0) {
2778
		$cert = new SyncResolveRecipientsCertificates();
2779
		if ($certificates === false) {
2780
			$cert->status = SYNC_RESOLVERECIPSSTATUS_CERTIFICATES_NOVALIDCERT;
2781
2782
			return $cert;
2783
		}
2784
		$cert->status = SYNC_RESOLVERECIPSSTATUS_SUCCESS;
2785
		$cert->certificatecount = count($certificates);
2786
		$cert->recipientcount = $recipientCount;
2787
		$cert->certificate = [];
2788
		foreach ($certificates as $certificate) {
2789
			$cert->certificate[] = base64_encode($certificate);
2790
		}
2791
2792
		return $cert;
2793
	}
2794
2795
	/**
2796
	 * Creates SyncResolveRecipient object for ResolveRecipientsResponse.
2797
	 *
2798
	 * @param int    $type
2799
	 * @param string $email
2800
	 * @param array  $recipientProperties
2801
	 * @param int    $recipientCount
2802
	 *
2803
	 * @return SyncResolveRecipient
2804
	 */
2805
	private function createResolveRecipient($type, $email, $recipientProperties, $recipientCount = 0) {
2806
		$recipient = new SyncResolveRecipient();
2807
		$recipient->type = $type;
2808
		$recipient->displayname = u2w($recipientProperties[PR_DISPLAY_NAME]);
2809
		$recipient->emailaddress = $email;
2810
2811
		if ($type == SYNC_RESOLVERECIPIENTS_TYPE_GAL) {
2812
			$certificateProp = PR_EMS_AB_TAGGED_X509_CERT;
2813
		}
2814
		elseif ($type == SYNC_RESOLVERECIPIENTS_TYPE_CONTACT) {
2815
			$certificateProp = PR_USER_X509_CERTIFICATE;
2816
		}
2817
		else {
2818
			$certificateProp = null;
2819
		}
2820
2821
		if (isset($recipientProperties[$certificateProp]) && is_array($recipientProperties[$certificateProp]) && !empty($recipientProperties[$certificateProp])) {
2822
			$certificates = $this->getCertificates($recipientProperties[$certificateProp], $recipientCount);
2823
		}
2824
		else {
2825
			$certificates = $this->getCertificates(false);
2826
			SLog::Write(LOGLEVEL_INFO, sprintf("Grommunio->createResolveRecipient(): No certificate found for '%s' (requested email address: '%s')", $recipientProperties[PR_DISPLAY_NAME], $email));
2827
		}
2828
		$recipient->certificates = $certificates;
2829
2830
		if (isset($recipientProperties[PR_ENTRYID])) {
2831
			$recipient->id = $recipientProperties[PR_ENTRYID];
2832
		}
2833
2834
		return $recipient;
2835
	}
2836
2837
	/**
2838
	 * Gets the availability of a user for the given time window.
2839
	 *
2840
	 * @param string                       $to
2841
	 * @param SyncResolveRecipient         $resolveRecipient
2842
	 * @param SyncResolveRecipientsOptions $resolveRecipientsOptions
2843
	 *
2844
	 * @return SyncResolveRecipientsAvailability
2845
	 */
2846
	private function getAvailability($to, $resolveRecipient, $resolveRecipientsOptions) {
2847
		$availability = new SyncResolveRecipientsAvailability();
2848
		$availability->status = SYNC_RESOLVERECIPSSTATUS_AVAILABILITY_SUCCESS;
2849
2850
		if (!isset($resolveRecipient->id)) {
2851
			// TODO this shouldn't happen but try to get the recipient in such a case
2852
		}
2853
2854
		$start = strtotime($resolveRecipientsOptions->availability->starttime);
2855
		$end = strtotime($resolveRecipientsOptions->availability->endtime);
2856
		// Each digit in the MergedFreeBusy indicates the free/busy status for the user for every 30 minute interval.
2857
		$timeslots = intval(ceil(($end - $start) / self::HALFHOURSECONDS));
2858
2859
		if ($timeslots > self::MAXFREEBUSYSLOTS) {
2860
			throw new StatusException("Grommunio->getAvailability(): the requested free busy range is too large.", SYNC_RESOLVERECIPSSTATUS_PROTOCOLERROR);
2861
		}
2862
2863
		$mergedFreeBusy = str_pad(fbNoData, $timeslots, fbNoData);
2864
2865
		$retval = mapi_getuseravailability($this->session, $resolveRecipient->id, $start, $end);
2866
		SLog::Write(LOGLEVEL_INFO, sprintf("Grommunio->getAvailability(): free busy '%s'", print_r($retval, 1)));
2867
2868
		if (!empty($retval)) {
2869
			$freebusy = json_decode($retval, true);
2870
			// freebusy is available, assume that the user is free
2871
			$mergedFreeBusy = str_pad(fbFree, $timeslots, fbFree);
2872
			foreach ($freebusy['events'] as $event) {
2873
				// calculate which timeslot of mergedFreeBusy should be replaced.
2874
				$startSlot = intval(floor(($event['StartTime'] - $start) / self::HALFHOURSECONDS));
2875
				$endSlot = intval(floor(($event['EndTime'] - $start) / self::HALFHOURSECONDS));
2876
				// if event started at a multiple of half an hour from requested freebusy time and
2877
				// its duration is also a multiple of half an hour
2878
				// then it's necessary to reduce endSlot by one
2879
				if ((($event['StartTime'] - $start) % self::HALFHOURSECONDS == 0) && (($event['EndTime'] - $event['StartTime']) % self::HALFHOURSECONDS == 0)) {
2880
					--$endSlot;
2881
				}
2882
				$fbType = Utils::GetFbStatusFromType($event['BusyType']);
2883
				for ($i = $startSlot; $i <= $endSlot && $i < $timeslots; ++$i) {
2884
					// only set the new slot's free busy status if it's higher than the current one
2885
					if ($fbType > $mergedFreeBusy[$i]) {
2886
						$mergedFreeBusy[$i] = $fbType;
2887
					}
2888
				}
2889
			}
2890
		}
2891
		$availability->mergedfreebusy = $mergedFreeBusy;
2892
2893
		return $availability;
2894
	}
2895
2896
	/**
2897
	 * Returns contacts matching given email address from a folder.
2898
	 *
2899
	 * @param MAPIStore $store
2900
	 * @param binary    $folderEntryid
2901
	 * @param string    $email
2902
	 *
2903
	 * @return array|bool
2904
	 */
2905
	private function getContactsFromFolder($store, $folderEntryid, $email) {
2906
		$folder = mapi_msgstore_openentry($store, $folderEntryid);
2907
		$folderContent = mapi_folder_getcontentstable($folder);
2908
		mapi_table_restrict($folderContent, MAPIUtils::GetEmailAddressRestriction($store, $email));
2909
		// TODO max limit
2910
		if (mapi_table_getrowcount($folderContent) > 0) {
2911
			return mapi_table_queryallrows($folderContent, [PR_DISPLAY_NAME, PR_USER_X509_CERTIFICATE, PR_ENTRYID]);
2912
		}
2913
2914
		return false;
2915
	}
2916
2917
	/**
2918
	 * Get MAPI addressbook object.
2919
	 *
2920
	 * @return MAPIAddressbook object to be used with mapi_ab_* or false on failure
2921
	 */
2922
	private function getAddressbook() {
2923
		if (isset($this->addressbook) && $this->addressbook) {
2924
			return $this->addressbook;
2925
		}
2926
		$this->addressbook = mapi_openaddressbook($this->session);
2927
		$result = mapi_last_hresult();
2928
		if ($result && $this->addressbook === false) {
2929
			SLog::Write(LOGLEVEL_ERROR, sprintf("Grommunio->getAddressbook error opening addressbook 0x%X", $result));
2930
2931
			return false;
2932
		}
2933
2934
		return $this->addressbook;
2935
	}
2936
2937
	/**
2938
	 * Checks if the user is not disabled for grommunio-sync.
2939
	 *
2940
	 * @throws FatalException if user is disabled for grommunio-sync
2941
	 *
2942
	 * @return bool
2943
	 */
2944
	private function isGSyncEnabled() {
2945
		$addressbook = $this->getAddressbook();
2946
		// this check needs to be performed on the store of the main (authenticated) user
2947
		$store = $this->storeCache[$this->mainUser];
2948
		$userEntryid = mapi_getprops($store, [PR_MAILBOX_OWNER_ENTRYID]);
2949
		$mailuser = mapi_ab_openentry($addressbook, $userEntryid[PR_MAILBOX_OWNER_ENTRYID]);
2950
		$enabledFeatures = mapi_getprops($mailuser, [PR_EC_DISABLED_FEATURES]);
2951
		if (isset($enabledFeatures[PR_EC_DISABLED_FEATURES]) && is_array($enabledFeatures[PR_EC_DISABLED_FEATURES])) {
2952
			$mobileDisabled = in_array(self::MOBILE_ENABLED, $enabledFeatures[PR_EC_DISABLED_FEATURES]);
2953
			$deviceId = Request::GetDeviceID();
2954
			// Checks for deviceId present in zarafaDisabledFeatures LDAP array attribute. Check is performed case insensitive.
2955
			$deviceIdDisabled = (($deviceId !== null) && in_array($deviceId, array_map('strtolower', $enabledFeatures[PR_EC_DISABLED_FEATURES]))) ? true : false;
2956
			if ($mobileDisabled) {
2957
				throw new FatalException("User is disabled for grommunio-sync.");
2958
			}
2959
			if ($deviceIdDisabled) {
2960
				throw new FatalException(sprintf("User has deviceId %s disabled for usage with grommunio-sync.", $deviceId));
2961
			}
2962
		}
2963
2964
		return true;
2965
	}
2966
}
2967