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