1 | <?php |
||||||
2 | |||||||
3 | /* |
||||||
4 | * SPDX-License-Identifier: AGPL-3.0-only |
||||||
5 | * SPDX-FileCopyrightText: Copyright 2016 - 2018 Kopano b.v. |
||||||
6 | * SPDX-FileCopyrightText: Copyright 2020 - 2025 grommunio GmbH |
||||||
7 | * |
||||||
8 | * grommunio DAV backend class which handles grommunio related activities. |
||||||
9 | */ |
||||||
10 | |||||||
11 | namespace grommunio\DAV; |
||||||
12 | |||||||
13 | use Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet; |
||||||
14 | |||||||
15 | class GrommunioDavBackend { |
||||||
16 | private $logger; |
||||||
17 | protected $session; |
||||||
18 | protected $stores; |
||||||
19 | protected $user; |
||||||
20 | protected $customprops; |
||||||
21 | protected $syncstate; |
||||||
22 | |||||||
23 | /** |
||||||
24 | * Constructor. |
||||||
25 | */ |
||||||
26 | public function __construct(GLogger $glogger) { |
||||||
27 | $this->logger = $glogger; |
||||||
28 | $this->syncstate = new GrommunioSyncState($glogger, SYNC_DB); |
||||||
29 | } |
||||||
30 | |||||||
31 | /** |
||||||
32 | * Connect to grommunio and create session. |
||||||
33 | * |
||||||
34 | * @param string $user |
||||||
35 | * @param string $pass |
||||||
36 | * |
||||||
37 | * @return bool |
||||||
38 | */ |
||||||
39 | public function Logon($user, $pass) { |
||||||
40 | $this->logger->trace('%s / password', $user); |
||||||
41 | |||||||
42 | $gDavVersion = 'grommunio-dav' . @constant('GDAV_VERSION'); |
||||||
43 | $userAgent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : 'unknown'; |
||||||
44 | $this->session = mapi_logon_zarafa($user, $pass, MAPI_SERVER, null, null, 1, $gDavVersion, $userAgent); |
||||||
45 | if (!$this->session) { |
||||||
46 | $this->logger->info("Auth: ERROR - logon failed for user %s from IP %s", $user, $_SERVER['REMOTE_ADDR']); |
||||||
47 | |||||||
48 | return false; |
||||||
49 | } |
||||||
50 | |||||||
51 | $this->user = $user; |
||||||
52 | $this->logger->debug("Auth: OK - user %s - session %s", $this->user, $this->session); |
||||||
53 | |||||||
54 | return $this->isGdavEnabled(); |
||||||
55 | } |
||||||
56 | |||||||
57 | /** |
||||||
58 | * Returns the authenticated user. |
||||||
59 | * |
||||||
60 | * @return string |
||||||
61 | */ |
||||||
62 | public function GetUser() { |
||||||
63 | $this->logger->trace($this->user); |
||||||
64 | |||||||
65 | return $this->user; |
||||||
66 | } |
||||||
67 | |||||||
68 | /** |
||||||
69 | * Create a folder with MAPI class. |
||||||
70 | * |
||||||
71 | * @param mixed $principalUri |
||||||
72 | * @param string $url |
||||||
73 | * @param string $class |
||||||
74 | * @param string $displayname |
||||||
75 | * |
||||||
76 | * @return string |
||||||
77 | */ |
||||||
78 | public function CreateFolder($principalUri, $url, $class, $displayname) { |
||||||
79 | $props = mapi_getprops($this->GetStore($principalUri), [PR_IPM_SUBTREE_ENTRYID]); |
||||||
80 | $folder = mapi_msgstore_openentry($this->GetStore($principalUri), $props[PR_IPM_SUBTREE_ENTRYID]); |
||||||
81 | $newfolder = mapi_folder_createfolder($folder, $url, $displayname); |
||||||
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||||||
82 | mapi_setprops($newfolder, [PR_CONTAINER_CLASS => $class]); |
||||||
83 | |||||||
84 | return $url; |
||||||
85 | } |
||||||
86 | |||||||
87 | /** |
||||||
88 | * Delete a folder with MAPI class. |
||||||
89 | * |
||||||
90 | * @param mixed $id |
||||||
91 | * |
||||||
92 | * @return bool |
||||||
93 | */ |
||||||
94 | public function DeleteFolder($id) { |
||||||
95 | $folder = $this->GetMapiFolder($id); |
||||||
96 | if (!$folder) { |
||||||
97 | return false; |
||||||
98 | } |
||||||
99 | |||||||
100 | $props = mapi_getprops($folder, [PR_ENTRYID, PR_PARENT_ENTRYID]); |
||||||
101 | $parentfolder = mapi_msgstore_openentry($this->GetStoreById($id), $props[PR_PARENT_ENTRYID]); |
||||||
102 | mapi_folder_deletefolder($parentfolder, $props[PR_ENTRYID]); |
||||||
0 ignored issues
–
show
The function
mapi_folder_deletefolder was not found. Maybe you did not declare it correctly or list all dependencies?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
103 | |||||||
104 | return true; |
||||||
105 | } |
||||||
106 | |||||||
107 | /** |
||||||
108 | * Returns a list of folders for a MAPI class. |
||||||
109 | * |
||||||
110 | * @param string $principalUri |
||||||
111 | * @param mixed $classes |
||||||
112 | * |
||||||
113 | * @return array |
||||||
114 | */ |
||||||
115 | public function GetFolders($principalUri, $classes) { |
||||||
116 | $this->logger->trace("principal '%s', classes '%s'", $principalUri, $classes); |
||||||
117 | $folders = []; |
||||||
118 | |||||||
119 | // TODO limit the output to subfolders of the principalUri? |
||||||
120 | |||||||
121 | $store = $this->GetStore($principalUri); |
||||||
122 | $storeprops = mapi_getprops($store, [PR_IPM_WASTEBASKET_ENTRYID]); |
||||||
123 | $rootfolder = mapi_msgstore_openentry($store); |
||||||
124 | $hierarchy = mapi_folder_gethierarchytable($rootfolder, CONVENIENT_DEPTH | MAPI_DEFERRED_ERRORS); |
||||||
125 | // TODO also filter hidden folders |
||||||
126 | $restrictions = []; |
||||||
127 | foreach ($classes as $class) { |
||||||
128 | $restrictions[] = [RES_PROPERTY, [RELOP => RELOP_EQ, ULPROPTAG => PR_CONTAINER_CLASS, VALUE => $class]]; |
||||||
129 | } |
||||||
130 | mapi_table_restrict($hierarchy, [RES_OR, $restrictions]); |
||||||
131 | |||||||
132 | // TODO how to handle hierarchies? |
||||||
133 | $rows = mapi_table_queryallrows($hierarchy, [PR_DISPLAY_NAME, PR_ENTRYID, PR_SOURCE_KEY, PR_PARENT_SOURCE_KEY, PR_FOLDER_TYPE, PR_LOCAL_COMMIT_TIME_MAX, PR_CONTAINER_CLASS, PR_COMMENT, PR_PARENT_ENTRYID]); |
||||||
134 | |||||||
135 | $rootprops = mapi_getprops($rootfolder, [PR_IPM_CONTACT_ENTRYID, PR_IPM_APPOINTMENT_ENTRYID]); |
||||||
136 | foreach ($rows as $row) { |
||||||
137 | if ($row[PR_FOLDER_TYPE] == FOLDER_SEARCH) { |
||||||
138 | continue; |
||||||
139 | } |
||||||
140 | |||||||
141 | if (isset($row[PR_PARENT_ENTRYID], $storeprops[PR_IPM_WASTEBASKET_ENTRYID]) && $row[PR_PARENT_ENTRYID] == $storeprops[PR_IPM_WASTEBASKET_ENTRYID]) { |
||||||
142 | continue; |
||||||
143 | } |
||||||
144 | |||||||
145 | $folder = [ |
||||||
146 | 'id' => $principalUri . ":" . bin2hex($row[PR_SOURCE_KEY]), |
||||||
147 | 'uri' => $row[PR_DISPLAY_NAME], |
||||||
148 | 'principaluri' => $principalUri, |
||||||
149 | '{http://sabredav.org/ns}sync-token' => '0000000000', |
||||||
150 | '{DAV:}displayname' => $row[PR_DISPLAY_NAME], |
||||||
151 | '{urn:ietf:params:xml:ns:caldav}calendar-description' => $row[PR_COMMENT], |
||||||
152 | '{http://calendarserver.org/ns/}getctag' => isset($row[PR_LOCAL_COMMIT_TIME_MAX]) ? strval($row[PR_LOCAL_COMMIT_TIME_MAX]) : '0000000000', |
||||||
153 | ]; |
||||||
154 | |||||||
155 | // set the supported component (task or calendar) |
||||||
156 | if ($row[PR_CONTAINER_CLASS] == "IPF.Task") { |
||||||
157 | $folder['{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set'] = new SupportedCalendarComponentSet(['VTODO']); |
||||||
158 | } |
||||||
159 | if ($row[PR_CONTAINER_CLASS] == "IPF.Appointment") { |
||||||
160 | $folder['{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set'] = new SupportedCalendarComponentSet(['VEVENT']); |
||||||
161 | } |
||||||
162 | |||||||
163 | // ensure default contacts folder is put first, some clients |
||||||
164 | // i.e. Apple Addressbook only supports one contact folder, |
||||||
165 | // therefore it is desired that folder is the default one. |
||||||
166 | if (in_array("IPF.Contact", $classes) && isset($rootprops[PR_IPM_CONTACT_ENTRYID]) && $row[PR_ENTRYID] == $rootprops[PR_IPM_CONTACT_ENTRYID]) { |
||||||
167 | array_unshift($folders, $folder); |
||||||
168 | } |
||||||
169 | // ensure default calendar folder is put first, |
||||||
170 | // before the tasks folder. |
||||||
171 | elseif (in_array('IPF.Appointment', $classes) && isset($rootprops[PR_IPM_APPOINTMENT_ENTRYID]) && $row[PR_ENTRYID] == $rootprops[PR_IPM_APPOINTMENT_ENTRYID]) { |
||||||
172 | array_unshift($folders, $folder); |
||||||
173 | } |
||||||
174 | else { |
||||||
175 | array_push($folders, $folder); |
||||||
176 | } |
||||||
177 | } |
||||||
178 | $this->logger->trace('found %d folders: %s', count($folders), $folders); |
||||||
179 | |||||||
180 | return $folders; |
||||||
181 | } |
||||||
182 | |||||||
183 | /** |
||||||
184 | * Returns a MAPI restriction for a defined set of filters. |
||||||
185 | * |
||||||
186 | * @param array $filters |
||||||
187 | * @param string $storeId (optional) mapi compatible storeid - required when using start+end filter |
||||||
188 | * |
||||||
189 | * @return null|array |
||||||
190 | */ |
||||||
191 | private function getRestrictionForFilters($filters, $storeId = null) { |
||||||
192 | $restrictions = []; |
||||||
193 | if (isset($filters['start'], $filters['end'], $storeId)) { |
||||||
194 | $this->logger->trace("getRestrictionForFilters - got start: %d and end: %d", $filters['start'], $filters['end']); |
||||||
195 | $subrestriction = $this->GetCalendarRestriction($storeId, $filters['start'], $filters['end']); |
||||||
196 | $restrictions[] = $subrestriction; |
||||||
197 | } |
||||||
198 | if (isset($filters['types'])) { |
||||||
199 | $this->logger->trace("getRestrictionForFilters - got types: %s", $filters['types']); |
||||||
200 | $arr = []; |
||||||
201 | foreach ($filters['types'] as $filter) { |
||||||
202 | $arr[] = [RES_PROPERTY, |
||||||
203 | [RELOP => RELOP_EQ, |
||||||
204 | ULPROPTAG => PR_MESSAGE_CLASS, |
||||||
205 | VALUE => $filter, |
||||||
206 | ], |
||||||
207 | ]; |
||||||
208 | } |
||||||
209 | $restrictions[] = [RES_OR, $arr]; |
||||||
210 | } |
||||||
211 | if (!empty($restrictions)) { |
||||||
212 | $restriction = [RES_AND, $restrictions]; |
||||||
213 | $this->logger->trace("getRestrictionForFilters - got restriction: %s", simplifyRestriction($restriction)); |
||||||
214 | |||||||
215 | return $restriction; |
||||||
216 | } |
||||||
217 | |||||||
218 | return null; |
||||||
219 | } |
||||||
220 | |||||||
221 | /** |
||||||
222 | * Returns a list of objects for a folder given by the id. |
||||||
223 | * |
||||||
224 | * @param string $id |
||||||
225 | * @param string $fileExtension |
||||||
226 | * @param array $filters |
||||||
227 | * |
||||||
228 | * @return array |
||||||
229 | */ |
||||||
230 | public function GetObjects($id, $fileExtension, $filters = []) { |
||||||
231 | $folder = $this->GetMapiFolder($id); |
||||||
232 | $properties = $this->GetCustomProperties($id); |
||||||
233 | $table = mapi_folder_getcontentstable($folder, MAPI_DEFERRED_ERRORS); |
||||||
234 | $restriction = $this->getRestrictionForFilters($filters, $this->GetStoreById($id)); |
||||||
235 | if ($restriction) { |
||||||
236 | mapi_table_restrict($table, $restriction); |
||||||
237 | } |
||||||
238 | |||||||
239 | $rows = mapi_table_queryallrows($table, [PR_SOURCE_KEY, PR_LAST_MODIFICATION_TIME, PR_MESSAGE_SIZE, $properties['goid']]); |
||||||
240 | |||||||
241 | $results = []; |
||||||
242 | foreach ($rows as $row) { |
||||||
243 | $realId = ""; |
||||||
244 | if (isset($row[$properties['goid']])) { |
||||||
245 | $realId = getUidFromGoid($row[$properties['goid']]); |
||||||
246 | } |
||||||
247 | if (!$realId) { |
||||||
248 | $realId = bin2hex($row[PR_SOURCE_KEY]); |
||||||
249 | } |
||||||
250 | $realId = rawurlencode($realId); |
||||||
251 | |||||||
252 | $result = [ |
||||||
253 | 'id' => $realId, |
||||||
254 | 'uri' => $realId . $fileExtension, |
||||||
255 | 'etag' => '"' . $row[PR_LAST_MODIFICATION_TIME] . '"', |
||||||
256 | 'lastmodified' => $row[PR_LAST_MODIFICATION_TIME], |
||||||
257 | 'size' => $row[PR_MESSAGE_SIZE], // only approximation |
||||||
258 | ]; |
||||||
259 | |||||||
260 | if ($fileExtension == GrommunioCalDavBackend::FILE_EXTENSION) { |
||||||
261 | $result['calendarid'] = $id; |
||||||
262 | } |
||||||
263 | elseif ($fileExtension == GrommunioCardDavBackend::FILE_EXTENSION) { |
||||||
264 | $result['addressbookid'] = $id; |
||||||
265 | } |
||||||
266 | $results[] = $result; |
||||||
267 | } |
||||||
268 | |||||||
269 | return $results; |
||||||
270 | } |
||||||
271 | |||||||
272 | /** |
||||||
273 | * Create the object and set appttsref. |
||||||
274 | * |
||||||
275 | * @param mixed $folderId |
||||||
276 | * @param mixed $folder |
||||||
277 | * @param string $objectId |
||||||
278 | * |
||||||
279 | * @return mixed |
||||||
280 | */ |
||||||
281 | public function CreateObject($folderId, $folder, $objectId) { |
||||||
282 | $mapimessage = mapi_folder_createmessage($folder); |
||||||
283 | // we save the objectId in PROP_APPTTSREF so we find it by this id |
||||||
284 | $properties = $this->GetCustomProperties($folderId); |
||||||
285 | // FIXME: uid for contacts |
||||||
286 | $goid = getGoidFromUid($objectId); |
||||||
287 | mapi_setprops($mapimessage, [$properties['goid'] => $goid]); |
||||||
288 | |||||||
289 | return $mapimessage; |
||||||
290 | } |
||||||
291 | |||||||
292 | /** |
||||||
293 | * Returns a mapi folder resource for a folderid (PR_SOURCE_KEY). |
||||||
294 | * |
||||||
295 | * @param string $folderid |
||||||
296 | * |
||||||
297 | * @return mixed |
||||||
298 | */ |
||||||
299 | public function GetMapiFolder($folderid) { |
||||||
300 | $this->logger->trace('Id: %s', $folderid); |
||||||
301 | $arr = explode(':', $folderid); |
||||||
302 | $entryid = mapi_msgstore_entryidfromsourcekey($this->GetStore($arr[0]), hex2bin($arr[1])); |
||||||
303 | |||||||
304 | return mapi_msgstore_openentry($this->GetStore($arr[0]), $entryid); |
||||||
305 | } |
||||||
306 | |||||||
307 | /** |
||||||
308 | * Returns MAPI addressbook. |
||||||
309 | * |
||||||
310 | * @return mixed |
||||||
311 | */ |
||||||
312 | public function GetAddressBook() { |
||||||
313 | // TODO should be a singleton |
||||||
314 | return mapi_openaddressbook($this->session); |
||||||
315 | } |
||||||
316 | |||||||
317 | /** |
||||||
318 | * Opens MAPI store for the user. |
||||||
319 | * |
||||||
320 | * @param string $username |
||||||
321 | * |
||||||
322 | * @return mixed |
||||||
323 | */ |
||||||
324 | public function OpenMapiStore($username = null) { |
||||||
325 | $msgstorestable = mapi_getmsgstorestable($this->session); |
||||||
326 | $msgstores = mapi_table_queryallrows($msgstorestable, [PR_DEFAULT_STORE, PR_ENTRYID, PR_MDB_PROVIDER]); |
||||||
327 | |||||||
328 | $defaultstore = null; |
||||||
329 | $publicstore = null; |
||||||
330 | foreach ($msgstores as $row) { |
||||||
331 | if (isset($row[PR_DEFAULT_STORE]) && $row[PR_DEFAULT_STORE]) { |
||||||
332 | $defaultstore = $row[PR_ENTRYID]; |
||||||
333 | } |
||||||
334 | if (isset($row[PR_MDB_PROVIDER]) && $row[PR_MDB_PROVIDER] == ZARAFA_STORE_PUBLIC_GUID) { |
||||||
335 | $publicstore = $row[PR_ENTRYID]; |
||||||
336 | } |
||||||
337 | } |
||||||
338 | |||||||
339 | /* user's own store or public store */ |
||||||
340 | if ($username == $this->GetUser() && $defaultstore != null) { |
||||||
341 | return mapi_openmsgstore($this->session, $defaultstore); |
||||||
342 | } |
||||||
343 | if ($username == 'public' && $publicstore != null) { |
||||||
344 | return mapi_openmsgstore($this->session, $publicstore); |
||||||
345 | } |
||||||
346 | |||||||
347 | /* otherwise other user's store */ |
||||||
348 | $store = mapi_openmsgstore($this->session, $defaultstore); |
||||||
349 | if (!$store) { |
||||||
350 | return false; |
||||||
351 | } |
||||||
352 | $otherstore = mapi_msgstore_createentryid($store, $username); |
||||||
353 | |||||||
354 | return mapi_openmsgstore($this->session, $otherstore); |
||||||
355 | } |
||||||
356 | |||||||
357 | /** |
||||||
358 | * Returns store for the user. |
||||||
359 | * |
||||||
360 | * @param string $storename |
||||||
361 | * |
||||||
362 | * @return mixed |
||||||
363 | */ |
||||||
364 | public function GetStore($storename) { |
||||||
365 | if ($storename == null) { |
||||||
366 | $storename = $this->GetUser(); |
||||||
367 | } |
||||||
368 | else { |
||||||
369 | $storename = str_replace('principals/', '', $storename); |
||||||
370 | } |
||||||
371 | $this->logger->trace("storename %s", $storename); |
||||||
372 | |||||||
373 | /* We already got the store */ |
||||||
374 | if (isset($this->stores[$storename]) && $this->stores[$storename] != null) { |
||||||
375 | return $this->stores[$storename]; |
||||||
376 | } |
||||||
377 | |||||||
378 | $this->stores[$storename] = $this->OpenMapiStore($storename); |
||||||
379 | if (!$this->stores[$storename]) { |
||||||
380 | $this->logger->info("Auth: ERROR - unable to open store for %s (0x%08X)", $storename, mapi_last_hresult()); |
||||||
381 | |||||||
382 | return false; |
||||||
383 | } |
||||||
384 | |||||||
385 | return $this->stores[$storename]; |
||||||
386 | } |
||||||
387 | |||||||
388 | /** |
||||||
389 | * Returns store from the id. |
||||||
390 | * |
||||||
391 | * @param mixed $id |
||||||
392 | * |
||||||
393 | * @return mixed |
||||||
394 | */ |
||||||
395 | public function GetStoreById($id) { |
||||||
396 | $arr = explode(':', $id); |
||||||
397 | |||||||
398 | return $this->GetStore($arr[0]); |
||||||
399 | } |
||||||
400 | |||||||
401 | /** |
||||||
402 | * Returns logon session. |
||||||
403 | * |
||||||
404 | * @return mixed |
||||||
405 | */ |
||||||
406 | public function GetSession() { |
||||||
407 | return $this->session; |
||||||
408 | } |
||||||
409 | |||||||
410 | /** |
||||||
411 | * Returns an object ID of a mapi object. |
||||||
412 | * If set, goid will be preferred. If not the PR_SOURCE_KEY of the message (as hex) will be returned. |
||||||
413 | * |
||||||
414 | * This order is reflected as well when searching for a message with these ids in GrommunioDavBackend->GetMapiMessageForId(). |
||||||
415 | * |
||||||
416 | * @param string $folderId |
||||||
417 | * @param mixed $mapimessage |
||||||
418 | * |
||||||
419 | * @return string |
||||||
420 | */ |
||||||
421 | public function GetIdOfMapiMessage($folderId, $mapimessage) { |
||||||
422 | $this->logger->trace("Finding ID of %s", $mapimessage); |
||||||
423 | $properties = $this->GetCustomProperties($folderId); |
||||||
424 | |||||||
425 | // It's one of these, order: |
||||||
426 | // - GOID (if set) |
||||||
427 | // - PROP_VCARDUID (if set) |
||||||
428 | // - PR_SOURCE_KEY |
||||||
429 | $props = mapi_getprops($mapimessage, [$properties['goid'], PR_SOURCE_KEY]); |
||||||
430 | if (isset($props[$properties['goid']])) { |
||||||
431 | $id = getUidFromGoid($props[$properties['goid']]); |
||||||
432 | $this->logger->debug("Found uid %s from goid: %s", $id, bin2hex($props[$properties['goid']])); |
||||||
433 | if ($id != null) { |
||||||
434 | return rawurlencode($id); |
||||||
435 | } |
||||||
436 | } |
||||||
437 | // PR_SOURCE_KEY is always available |
||||||
438 | $id = bin2hex($props[PR_SOURCE_KEY]); |
||||||
439 | $this->logger->debug("Found PR_SOURCE_KEY: %s", $id); |
||||||
440 | |||||||
441 | return $id; |
||||||
442 | } |
||||||
443 | |||||||
444 | /** |
||||||
445 | * Finds and opens a MapiMessage from an objectId. |
||||||
446 | * The id can be a PROP_APPTTSREF or a PR_SOURCE_KEY (as hex). |
||||||
447 | * |
||||||
448 | * @param string $folderId |
||||||
449 | * @param string $objectUri |
||||||
450 | * @param mixed $mapifolder optional |
||||||
451 | * @param string $extension optional |
||||||
452 | * |
||||||
453 | * @return mixed |
||||||
454 | */ |
||||||
455 | public function GetMapiMessageForId($folderId, $objectUri, $mapifolder = null, $extension = null) { |
||||||
456 | $this->logger->trace("Searching for '%s' in '%s' (%s) (%s)", $objectUri, $folderId, $mapifolder, $extension); |
||||||
457 | |||||||
458 | if (!$mapifolder) { |
||||||
459 | $mapifolder = $this->GetMapiFolder($folderId); |
||||||
460 | } |
||||||
461 | |||||||
462 | $id = rawurldecode($this->GetObjectIdFromObjectUri($objectUri, $extension)); |
||||||
463 | |||||||
464 | /* The ID can be several different things: |
||||||
465 | * - a UID that is saved in goid |
||||||
466 | * - a PROP_VCARDUID |
||||||
467 | * - a PR_SOURCE_KEY |
||||||
468 | * |
||||||
469 | * If it's a sourcekey, we can open the message directly. |
||||||
470 | * If the $extension is set: |
||||||
471 | * if it's ics: |
||||||
472 | * - search GOID with this value |
||||||
473 | * if it's vcf: |
||||||
474 | * - search PROP_VCARDUID value |
||||||
475 | */ |
||||||
476 | $entryid = false; |
||||||
477 | $restriction = false; |
||||||
478 | |||||||
479 | if (ctype_xdigit($id) && strlen($id) % 2 == 0) { |
||||||
480 | $this->logger->trace("Try PR_SOURCE_KEY %s", $id); |
||||||
481 | $arr = explode(':', $folderId); |
||||||
482 | $entryid = mapi_msgstore_entryidfromsourcekey($this->GetStoreById($arr[0]), hex2bin($arr[1]), hex2bin($id)); |
||||||
483 | } |
||||||
484 | |||||||
485 | if (!$entryid) { |
||||||
486 | $this->logger->trace("Entryid not found. Try goid/vcarduid %s", $id); |
||||||
487 | |||||||
488 | $properties = $this->GetCustomProperties($folderId); |
||||||
489 | $restriction = []; |
||||||
490 | |||||||
491 | if ($extension) { |
||||||
492 | if ($extension == GrommunioCalDavBackend::FILE_EXTENSION) { |
||||||
493 | $this->logger->trace("Try goid %s", $id); |
||||||
494 | $goid = getGoidFromUid($id); |
||||||
495 | $this->logger->trace("Try goid 0x%08X => %s", $properties["goid"], bin2hex($goid)); |
||||||
496 | $goid0 = getGoidFromUidZero($id); |
||||||
497 | $restriction[] = [RES_OR, [ |
||||||
498 | [RES_PROPERTY, [RELOP => RELOP_EQ, ULPROPTAG => $properties["goid"], VALUE => $goid]], |
||||||
499 | [RES_PROPERTY, [RELOP => RELOP_EQ, ULPROPTAG => $properties["goid"], VALUE => $goid0]], |
||||||
500 | ]]; |
||||||
501 | } |
||||||
502 | elseif ($extension == GrommunioCardDavBackend::FILE_EXTENSION) { |
||||||
503 | $this->logger->trace("Try vcarduid %s", $id); |
||||||
504 | $restriction[] = [RES_PROPERTY, [RELOP => RELOP_EQ, ULPROPTAG => $properties["vcarduid"], VALUE => $id]]; |
||||||
505 | } |
||||||
506 | } |
||||||
507 | } |
||||||
508 | |||||||
509 | // find the message if we have a restriction |
||||||
510 | if ($restriction) { |
||||||
511 | $table = mapi_folder_getcontentstable($mapifolder, MAPI_DEFERRED_ERRORS); |
||||||
512 | mapi_table_restrict($table, [RES_OR, $restriction]); |
||||||
513 | // Get requested properties, plus whatever we need |
||||||
514 | $proplist = [PR_ENTRYID]; |
||||||
515 | $rows = mapi_table_queryallrows($table, $proplist); |
||||||
516 | if (count($rows) > 1) { |
||||||
517 | $this->logger->warn("Found %d entries for id '%s' searching for message, returnin first in the list", count($rows), $id); |
||||||
518 | } |
||||||
519 | if (isset($rows[0], $rows[0][PR_ENTRYID])) { |
||||||
520 | $entryid = $rows[0][PR_ENTRYID]; |
||||||
521 | } |
||||||
522 | } |
||||||
523 | if (!$entryid) { |
||||||
524 | $this->logger->debug("Try to get entryid from appttsref"); |
||||||
525 | $arr = explode(':', $folderId); |
||||||
526 | $sk = $this->syncstate->getSourcekey($arr[1], $id); |
||||||
527 | if ($sk !== null) { |
||||||
528 | $this->logger->debug("Found sourcekey from appttsref %s", $sk); |
||||||
529 | $entryid = mapi_msgstore_entryidfromsourcekey($this->GetStoreById($arr[0]), hex2bin($arr[1]), hex2bin($sk)); |
||||||
530 | } |
||||||
531 | } |
||||||
532 | if ($entryid) { |
||||||
533 | $mapimessage = mapi_msgstore_openentry($this->GetStoreById($folderId), $entryid); |
||||||
534 | if (!$mapimessage) { |
||||||
535 | $this->logger->warn("Error, unable to open entry id: %s 0x%X", bin2hex($entryid), mapi_last_hresult()); |
||||||
536 | |||||||
537 | return null; |
||||||
538 | } |
||||||
539 | |||||||
540 | return $mapimessage; |
||||||
541 | } |
||||||
542 | $this->logger->debug("Nothing found for %s", $id); |
||||||
543 | |||||||
544 | return null; |
||||||
545 | } |
||||||
546 | |||||||
547 | /** |
||||||
548 | * Returns the objectId from an objectUri. It strips the file extension |
||||||
549 | * if it matches the passed one. |
||||||
550 | * |
||||||
551 | * @param string $objectUri |
||||||
552 | * @param string $extension |
||||||
553 | * |
||||||
554 | * @return string |
||||||
555 | */ |
||||||
556 | public function GetObjectIdFromObjectUri($objectUri, $extension) { |
||||||
557 | if (!$extension) { |
||||||
558 | return $objectUri; |
||||||
559 | } |
||||||
560 | $extLength = strlen($extension); |
||||||
561 | if (substr($objectUri, -$extLength) === $extension) { |
||||||
562 | return substr($objectUri, 0, -$extLength); |
||||||
563 | } |
||||||
564 | |||||||
565 | return $objectUri; |
||||||
566 | } |
||||||
567 | |||||||
568 | /** |
||||||
569 | * Checks if the PHP-MAPI extension is available and in a requested version. |
||||||
570 | * |
||||||
571 | * @param string $version the version to be checked ("6.30.10-18495", parts or build number) |
||||||
572 | * |
||||||
573 | * @return bool installed version is superior to the checked string |
||||||
574 | */ |
||||||
575 | protected function checkMapiExtVersion($version = "") { |
||||||
576 | if (!extension_loaded("mapi")) { |
||||||
577 | return false; |
||||||
578 | } |
||||||
579 | // compare build number if requested |
||||||
580 | if (preg_match('/^\d+$/', $version) && strlen($version) > 3) { |
||||||
581 | $vs = preg_split('/-/', phpversion("mapi")); |
||||||
582 | |||||||
583 | return $version <= $vs[1]; |
||||||
584 | } |
||||||
585 | if (version_compare(phpversion("mapi"), $version) == -1) { |
||||||
586 | return false; |
||||||
587 | } |
||||||
588 | |||||||
589 | return true; |
||||||
590 | } |
||||||
591 | |||||||
592 | /** |
||||||
593 | * Get named (custom) properties. Currently only PROP_APPTTSREF. |
||||||
594 | * |
||||||
595 | * @param string $id the folder id |
||||||
596 | * |
||||||
597 | * @return mixed |
||||||
598 | */ |
||||||
599 | protected function GetCustomProperties($id) { |
||||||
600 | if (!isset($this->customprops[$id])) { |
||||||
601 | $this->logger->trace("Fetching properties id:%s", $id); |
||||||
602 | $store = $this->GetStoreById($id); |
||||||
603 | $properties = getPropIdsFromStrings($store, [ |
||||||
604 | "goid" => "PT_BINARY:PSETID_Meeting:" . PidLidGlobalObjectId, |
||||||
605 | "vcarduid" => MapiProps::PROP_VCARDUID, |
||||||
606 | ]); |
||||||
607 | $this->customprops[$id] = $properties; |
||||||
608 | } |
||||||
609 | |||||||
610 | return $this->customprops[$id]; |
||||||
611 | } |
||||||
612 | |||||||
613 | /** |
||||||
614 | * Create a MAPI restriction to use in the calendar which will |
||||||
615 | * return future calendar items (until $end), plus those since $start. |
||||||
616 | * Origins: Z-Push. |
||||||
617 | * |
||||||
618 | * @param mixed $store the MAPI store |
||||||
619 | * @param int $start Timestamp since when to include messages |
||||||
620 | * @param int $end Ending timestamp |
||||||
621 | * |
||||||
622 | * @return array |
||||||
623 | */ |
||||||
624 | // TODO getting named properties |
||||||
625 | public function GetCalendarRestriction($store, $start, $end) { |
||||||
626 | $props = MapiProps::GetAppointmentProperties(); |
||||||
627 | $props = getPropIdsFromStrings($store, $props); |
||||||
628 | |||||||
629 | return [RES_OR, |
||||||
630 | [ |
||||||
631 | // OR |
||||||
632 | // item.end > window.start && item.start < window.end |
||||||
633 | [RES_AND, |
||||||
634 | [ |
||||||
635 | [RES_PROPERTY, |
||||||
636 | [RELOP => RELOP_LE, |
||||||
637 | ULPROPTAG => $props["starttime"], |
||||||
638 | VALUE => $end, |
||||||
639 | ], |
||||||
640 | ], |
||||||
641 | [RES_PROPERTY, |
||||||
642 | [RELOP => RELOP_GE, |
||||||
643 | ULPROPTAG => $props["endtime"], |
||||||
644 | VALUE => $start, |
||||||
645 | ], |
||||||
646 | ], |
||||||
647 | ], |
||||||
648 | ], |
||||||
649 | // OR |
||||||
650 | [RES_OR, |
||||||
651 | [ |
||||||
652 | // OR |
||||||
653 | // (EXIST(recurrence_enddate_property) && item[isRecurring] == true && recurrence_enddate_property >= start) |
||||||
654 | [RES_AND, |
||||||
655 | [ |
||||||
656 | [RES_EXIST, |
||||||
657 | [ULPROPTAG => $props["recurrenceend"], |
||||||
658 | ], |
||||||
659 | ], |
||||||
660 | [RES_PROPERTY, |
||||||
661 | [RELOP => RELOP_EQ, |
||||||
662 | ULPROPTAG => $props["isrecurring"], |
||||||
663 | VALUE => true, |
||||||
664 | ], |
||||||
665 | ], |
||||||
666 | [RES_PROPERTY, |
||||||
667 | [RELOP => RELOP_GE, |
||||||
668 | ULPROPTAG => $props["recurrenceend"], |
||||||
669 | VALUE => $start, |
||||||
670 | ], |
||||||
671 | ], |
||||||
672 | ], |
||||||
673 | ], |
||||||
674 | // OR |
||||||
675 | // (!EXIST(recurrence_enddate_property) && item[isRecurring] == true && item[start] <= end) |
||||||
676 | [RES_AND, |
||||||
677 | [ |
||||||
678 | [RES_NOT, |
||||||
679 | [ |
||||||
680 | [RES_EXIST, |
||||||
681 | [ULPROPTAG => $props["recurrenceend"], |
||||||
682 | ], |
||||||
683 | ], |
||||||
684 | ], |
||||||
685 | ], |
||||||
686 | [RES_PROPERTY, |
||||||
687 | [RELOP => RELOP_LE, |
||||||
688 | ULPROPTAG => $props["starttime"], |
||||||
689 | VALUE => $end, |
||||||
690 | ], |
||||||
691 | ], |
||||||
692 | [RES_PROPERTY, |
||||||
693 | [RELOP => RELOP_EQ, |
||||||
694 | ULPROPTAG => $props["isrecurring"], |
||||||
695 | VALUE => true, |
||||||
696 | ], |
||||||
697 | ], |
||||||
698 | ], |
||||||
699 | ], |
||||||
700 | ], |
||||||
701 | ], // EXISTS OR |
||||||
702 | ], |
||||||
703 | ]; // global OR |
||||||
704 | } |
||||||
705 | |||||||
706 | /** |
||||||
707 | * Performs ICS based sync used from getChangesForAddressBook |
||||||
708 | * / getChangesForCalendar. |
||||||
709 | * |
||||||
710 | * @param string $folderId |
||||||
711 | * @param string $syncToken |
||||||
712 | * @param string $fileExtension |
||||||
713 | * @param int $limit |
||||||
714 | * @param array $filters |
||||||
715 | * |
||||||
716 | * @return null|array |
||||||
717 | */ |
||||||
718 | public function Sync($folderId, $syncToken, $fileExtension, $limit = null, $filters = []) { |
||||||
719 | $arr = explode(':', $folderId); |
||||||
720 | $phpwrapper = new PHPWrapper($this->GetStoreById($folderId), $this->logger, $this->GetCustomProperties($folderId), $fileExtension, $this->syncstate, $arr[1]); |
||||||
721 | $mapiimporter = mapi_wrap_importcontentschanges($phpwrapper); |
||||||
0 ignored issues
–
show
The function
mapi_wrap_importcontentschanges was not found. Maybe you did not declare it correctly or list all dependencies?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
722 | |||||||
723 | $mapifolder = $this->GetMapiFolder($folderId); |
||||||
724 | $exporter = mapi_openproperty($mapifolder, PR_CONTENTS_SYNCHRONIZER, IID_IExchangeExportChanges, 0, 0); |
||||||
725 | if (!$exporter) { |
||||||
726 | $this->logger->error("Unable to get exporter"); |
||||||
727 | |||||||
728 | return null; |
||||||
729 | } |
||||||
730 | |||||||
731 | $stream = mapi_stream_create(); |
||||||
0 ignored issues
–
show
The function
mapi_stream_create was not found. Maybe you did not declare it correctly or list all dependencies?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
732 | if ($syncToken == null) { |
||||||
733 | mapi_stream_write($stream, hex2bin("0000000000000000")); |
||||||
734 | } |
||||||
735 | else { |
||||||
736 | $value = $this->syncstate->getState($arr[1], $syncToken); |
||||||
737 | if ($value === null) { |
||||||
738 | $this->logger->error("Unable to get value from token: %s - folderId: %s", $syncToken, $folderId); |
||||||
739 | |||||||
740 | return null; |
||||||
741 | } |
||||||
742 | mapi_stream_write($stream, hex2bin($value)); |
||||||
743 | } |
||||||
744 | |||||||
745 | // force restriction of "types" to export only appointments or contacts |
||||||
746 | $restriction = $this->getRestrictionForFilters($filters); |
||||||
747 | |||||||
748 | // The last parameter in mapi_exportchanges_config is buffer size for mapi_exportchanges_synchronize - how many |
||||||
749 | // changes will be processed in its call. Setting it to MAX_SYNC_ITEMS won't export more items than is set in |
||||||
750 | // the config. If there are more changes than MAX_SYNC_ITEMS the client will eventually catch up and sync |
||||||
751 | // the rest on the subsequent sync request(s). |
||||||
752 | $bufferSize = ($limit !== null && $limit > 0) ? $limit : MAX_SYNC_ITEMS; |
||||||
753 | mapi_exportchanges_config($exporter, $stream, SYNC_NORMAL | SYNC_UNICODE, $mapiimporter, $restriction, false, false, $bufferSize); |
||||||
0 ignored issues
–
show
The function
mapi_exportchanges_config was not found. Maybe you did not declare it correctly or list all dependencies?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
754 | $changesCount = mapi_exportchanges_getchangecount($exporter); |
||||||
0 ignored issues
–
show
The function
mapi_exportchanges_getchangecount was not found. Maybe you did not declare it correctly or list all dependencies?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
755 | $this->logger->debug("Exporter found %d changes, buffer size for mapi_exportchanges_synchronize %d", $changesCount, $bufferSize); |
||||||
756 | while (is_array(mapi_exportchanges_synchronize($exporter))) { |
||||||
0 ignored issues
–
show
The function
mapi_exportchanges_synchronize was not found. Maybe you did not declare it correctly or list all dependencies?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
757 | if ($changesCount > $bufferSize) { |
||||||
758 | $this->logger->info("There were too many changes to be exported in this request. Total changes %d, exported %d.", $changesCount, $phpwrapper->Total()); |
||||||
759 | |||||||
760 | break; |
||||||
761 | } |
||||||
762 | } |
||||||
763 | $exportedChanges = $phpwrapper->Total(); |
||||||
764 | $this->logger->debug("Exported %d changes, pending %d", $exportedChanges, $changesCount - $exportedChanges); |
||||||
765 | |||||||
766 | mapi_exportchanges_updatestate($exporter, $stream); |
||||||
0 ignored issues
–
show
The function
mapi_exportchanges_updatestate was not found. Maybe you did not declare it correctly or list all dependencies?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
767 | mapi_stream_seek($stream, 0, STREAM_SEEK_SET); |
||||||
0 ignored issues
–
show
The function
mapi_stream_seek was not found. Maybe you did not declare it correctly or list all dependencies?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
768 | $state = ""; |
||||||
769 | while (true) { |
||||||
770 | $data = mapi_stream_read($stream, 4096); |
||||||
0 ignored issues
–
show
The function
mapi_stream_read was not found. Maybe you did not declare it correctly or list all dependencies?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
771 | if (strlen($data) > 0) { |
||||||
772 | $state .= $data; |
||||||
773 | } |
||||||
774 | else { |
||||||
775 | break; |
||||||
776 | } |
||||||
777 | } |
||||||
778 | |||||||
779 | $newtoken = ($phpwrapper->Total() > 0) ? uniqid() : $syncToken; |
||||||
780 | |||||||
781 | $this->syncstate->setState($arr[1], $newtoken, bin2hex($state)); |
||||||
782 | |||||||
783 | $result = [ |
||||||
784 | "syncToken" => $newtoken, |
||||||
785 | "added" => $phpwrapper->GetAdded(), |
||||||
786 | "modified" => $phpwrapper->GetModified(), |
||||||
787 | "deleted" => $phpwrapper->GetDeleted(), |
||||||
788 | ]; |
||||||
789 | |||||||
790 | $this->logger->trace("Returning %s", $result); |
||||||
791 | |||||||
792 | return $result; |
||||||
793 | } |
||||||
794 | |||||||
795 | /** |
||||||
796 | * Returns an array of necessary properties to set with default values. |
||||||
797 | * |
||||||
798 | * @see MapiProps::GetDefault...Properties() |
||||||
799 | * |
||||||
800 | * @param mixed $id storeid |
||||||
801 | * @param mixed $mapimessage mapi message to check |
||||||
802 | * @param array $propList array of mapped properties |
||||||
803 | * @param array $defaultProps array of necessary properties with default values |
||||||
804 | * |
||||||
805 | * @return array |
||||||
806 | */ |
||||||
807 | public function GetPropsToSet($id, $mapimessage, $propList, $defaultProps) { |
||||||
808 | $propsToSet = []; |
||||||
809 | $store = $this->GetStoreById($id); |
||||||
810 | $propList = getPropIdsFromStrings($store, $propList); |
||||||
811 | $props = mapi_getprops($mapimessage); |
||||||
812 | |||||||
813 | foreach ($defaultProps as $prop => $value) { |
||||||
814 | if (!isset($props[$propList[$prop]])) { |
||||||
815 | $propsToSet[$propList[$prop]] = $value; |
||||||
816 | } |
||||||
817 | } |
||||||
818 | |||||||
819 | return $propsToSet; |
||||||
820 | } |
||||||
821 | |||||||
822 | /** |
||||||
823 | * Checks whether the user is enabled for grommunio-dav. |
||||||
824 | * |
||||||
825 | * @return bool |
||||||
826 | */ |
||||||
827 | private function isGdavEnabled() { |
||||||
828 | $storeProps = mapi_getprops($this->GetStore($this->GetUser()), [PR_EC_ENABLED_FEATURES_L]); |
||||||
829 | if ($storeProps[PR_EC_ENABLED_FEATURES_L] & UP_DAV) { |
||||||
830 | $this->logger->debug("user %s is enabled for grommunio-dav", $this->user); |
||||||
831 | |||||||
832 | return true; |
||||||
833 | } |
||||||
834 | $this->logger->debug("user %s is disabled for grommunio-dav", $this->user); |
||||||
835 | |||||||
836 | return false; |
||||||
837 | } |
||||||
838 | } |
||||||
839 |