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