Total Complexity | 750 |
Total Lines | 4790 |
Duplicated Lines | 0 % |
Changes | 16 | ||
Bugs | 9 | Features | 0 |
Complex classes like Operations often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use Operations, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
14 | class Operations { |
||
15 | /** |
||
16 | * Gets the hierarchy list of all required stores. |
||
17 | * |
||
18 | * getHierarchyList builds an entire hierarchy list of all folders that should be shown in various places. Most importantly, |
||
19 | * it generates the list of folders to be show in the hierarchylistmodule (left-hand folder browser) on the client. |
||
20 | * |
||
21 | * It is also used to generate smaller hierarchy lists, for example for the 'create folder' dialog. |
||
22 | * |
||
23 | * The returned array is a flat array of folders, so if the caller wishes to build a tree, it is up to the caller to correlate |
||
24 | * the entryids and the parent_entryids of all the folders to build the tree. |
||
25 | * |
||
26 | * The return value is an associated array with the following keys: |
||
27 | * - store: array of stores |
||
28 | * |
||
29 | * Each store contains: |
||
30 | * - array("store_entryid" => entryid of store, name => name of store, subtree => entryid of viewable root, type => default|public|other, folder_type => "all") |
||
31 | * - folder: array of folders with each an array of properties (see Operations::setFolder() for properties) |
||
32 | * |
||
33 | * @param array $properties MAPI property mapping for folders |
||
34 | * @param int $type Which stores to fetch (HIERARCHY_GET_ALL | HIERARCHY_GET_DEFAULT | HIERARCHY_GET_ONE) |
||
35 | * @param object $store Only when $type == HIERARCHY_GET_ONE |
||
36 | * @param array $storeOptions Only when $type == HIERARCHY_GET_ONE, this overrides the loading options which is normally |
||
37 | * obtained from the settings for loading the store (e.g. only load calendar). |
||
38 | * @param string $username The username |
||
39 | * |
||
40 | * @return array Return structure |
||
41 | */ |
||
42 | public function getHierarchyList($properties, $type = HIERARCHY_GET_ALL, $store = null, $storeOptions = null, $username = null) { |
||
43 | switch ($type) { |
||
44 | case HIERARCHY_GET_ALL: |
||
45 | $storelist = $GLOBALS["mapisession"]->getAllMessageStores(); |
||
46 | break; |
||
47 | |||
48 | case HIERARCHY_GET_DEFAULT: |
||
49 | $storelist = [$GLOBALS["mapisession"]->getDefaultMessageStore()]; |
||
50 | break; |
||
51 | |||
52 | case HIERARCHY_GET_ONE: |
||
53 | // Get single store and it's archive store as well |
||
54 | $storelist = $GLOBALS["mapisession"]->getSingleMessageStores($store, $storeOptions, $username); |
||
55 | break; |
||
56 | } |
||
57 | |||
58 | $data = []; |
||
59 | $data["item"] = []; |
||
60 | |||
61 | // Get the other store options |
||
62 | if (isset($storeOptions)) { |
||
63 | $otherUsers = $storeOptions; |
||
64 | } |
||
65 | else { |
||
66 | $otherUsers = $GLOBALS["mapisession"]->retrieveOtherUsersFromSettings(); |
||
67 | } |
||
68 | |||
69 | foreach ($storelist as $store) { |
||
|
|||
70 | $msgstore_props = mapi_getprops($store, [PR_ENTRYID, PR_DISPLAY_NAME, PR_IPM_SUBTREE_ENTRYID, PR_IPM_OUTBOX_ENTRYID, PR_IPM_SENTMAIL_ENTRYID, PR_IPM_WASTEBASKET_ENTRYID, PR_MDB_PROVIDER, PR_IPM_PUBLIC_FOLDERS_ENTRYID, PR_IPM_FAVORITES_ENTRYID, PR_OBJECT_TYPE, PR_STORE_SUPPORT_MASK, PR_MAILBOX_OWNER_ENTRYID, PR_MAILBOX_OWNER_NAME, PR_USER_ENTRYID, PR_USER_NAME, PR_QUOTA_WARNING_THRESHOLD, PR_QUOTA_SEND_THRESHOLD, PR_QUOTA_RECEIVE_THRESHOLD, PR_MESSAGE_SIZE_EXTENDED, PR_COMMON_VIEWS_ENTRYID, PR_FINDER_ENTRYID]); |
||
71 | |||
72 | $inboxProps = []; |
||
73 | $storeType = $msgstore_props[PR_MDB_PROVIDER]; |
||
74 | |||
75 | /* |
||
76 | * storetype is public and if public folder is disabled |
||
77 | * then continue in loop for next store. |
||
78 | */ |
||
79 | if ($storeType == ZARAFA_STORE_PUBLIC_GUID && ENABLE_PUBLIC_FOLDERS === false) { |
||
80 | continue; |
||
81 | } |
||
82 | |||
83 | // Obtain the real username for the store when dealing with a shared store |
||
84 | if ($storeType == ZARAFA_STORE_DELEGATE_GUID) { |
||
85 | $storeUserName = $GLOBALS["mapisession"]->getUserNameOfStore($msgstore_props[PR_ENTRYID]); |
||
86 | } |
||
87 | else { |
||
88 | $storeUserName = $msgstore_props[PR_USER_NAME] ?? $GLOBALS["mapisession"]->getUserName(); |
||
89 | } |
||
90 | |||
91 | $storeData = [ |
||
92 | "store_entryid" => bin2hex((string) $msgstore_props[PR_ENTRYID]), |
||
93 | "props" => [ |
||
94 | // Showing the store as 'Inbox - Name' is confusing, so we strip the 'Inbox - ' part. |
||
95 | "display_name" => str_replace('Inbox - ', '', $msgstore_props[PR_DISPLAY_NAME]), |
||
96 | "subtree_entryid" => bin2hex((string) $msgstore_props[PR_IPM_SUBTREE_ENTRYID]), |
||
97 | "mdb_provider" => bin2hex((string) $msgstore_props[PR_MDB_PROVIDER]), |
||
98 | "object_type" => $msgstore_props[PR_OBJECT_TYPE], |
||
99 | "store_support_mask" => $msgstore_props[PR_STORE_SUPPORT_MASK], |
||
100 | "user_name" => $storeUserName, |
||
101 | "store_size" => round($msgstore_props[PR_MESSAGE_SIZE_EXTENDED] / 1024), |
||
102 | "quota_warning" => $msgstore_props[PR_QUOTA_WARNING_THRESHOLD] ?? 0, |
||
103 | "quota_soft" => $msgstore_props[PR_QUOTA_SEND_THRESHOLD] ?? 0, |
||
104 | "quota_hard" => $msgstore_props[PR_QUOTA_RECEIVE_THRESHOLD] ?? 0, |
||
105 | "common_view_entryid" => isset($msgstore_props[PR_COMMON_VIEWS_ENTRYID]) ? bin2hex((string) $msgstore_props[PR_COMMON_VIEWS_ENTRYID]) : "", |
||
106 | "finder_entryid" => isset($msgstore_props[PR_FINDER_ENTRYID]) ? bin2hex((string) $msgstore_props[PR_FINDER_ENTRYID]) : "", |
||
107 | "todolist_entryid" => bin2hex(TodoList::getEntryId()), |
||
108 | ], |
||
109 | ]; |
||
110 | |||
111 | // these properties doesn't exist in public store |
||
112 | if (isset($msgstore_props[PR_MAILBOX_OWNER_ENTRYID], $msgstore_props[PR_MAILBOX_OWNER_NAME])) { |
||
113 | $storeData["props"]["mailbox_owner_entryid"] = bin2hex((string) $msgstore_props[PR_MAILBOX_OWNER_ENTRYID]); |
||
114 | $storeData["props"]["mailbox_owner_name"] = $msgstore_props[PR_MAILBOX_OWNER_NAME]; |
||
115 | } |
||
116 | |||
117 | // public store doesn't have inbox |
||
118 | try { |
||
119 | $inbox = mapi_msgstore_getreceivefolder($store); |
||
120 | $inboxProps = mapi_getprops($inbox, [PR_ENTRYID]); |
||
121 | } |
||
122 | catch (MAPIException $e) { |
||
123 | // don't propagate this error to parent handlers, if store doesn't support it |
||
124 | if ($e->getCode() === MAPI_E_NO_SUPPORT) { |
||
125 | $e->setHandled(); |
||
126 | } |
||
127 | } |
||
128 | |||
129 | $root = mapi_msgstore_openentry($store); |
||
130 | $rootProps = mapi_getprops($root, [PR_IPM_APPOINTMENT_ENTRYID, PR_IPM_CONTACT_ENTRYID, PR_IPM_DRAFTS_ENTRYID, PR_IPM_JOURNAL_ENTRYID, PR_IPM_NOTE_ENTRYID, PR_IPM_TASK_ENTRYID, PR_ADDITIONAL_REN_ENTRYIDS]); |
||
131 | |||
132 | $additional_ren_entryids = []; |
||
133 | if (isset($rootProps[PR_ADDITIONAL_REN_ENTRYIDS])) { |
||
134 | $additional_ren_entryids = $rootProps[PR_ADDITIONAL_REN_ENTRYIDS]; |
||
135 | } |
||
136 | |||
137 | $defaultfolders = [ |
||
138 | "default_folder_inbox" => ["inbox" => PR_ENTRYID], |
||
139 | "default_folder_outbox" => ["store" => PR_IPM_OUTBOX_ENTRYID], |
||
140 | "default_folder_sent" => ["store" => PR_IPM_SENTMAIL_ENTRYID], |
||
141 | "default_folder_wastebasket" => ["store" => PR_IPM_WASTEBASKET_ENTRYID], |
||
142 | "default_folder_favorites" => ["store" => PR_IPM_FAVORITES_ENTRYID], |
||
143 | "default_folder_publicfolders" => ["store" => PR_IPM_PUBLIC_FOLDERS_ENTRYID], |
||
144 | "default_folder_calendar" => ["root" => PR_IPM_APPOINTMENT_ENTRYID], |
||
145 | "default_folder_contact" => ["root" => PR_IPM_CONTACT_ENTRYID], |
||
146 | "default_folder_drafts" => ["root" => PR_IPM_DRAFTS_ENTRYID], |
||
147 | "default_folder_journal" => ["root" => PR_IPM_JOURNAL_ENTRYID], |
||
148 | "default_folder_note" => ["root" => PR_IPM_NOTE_ENTRYID], |
||
149 | "default_folder_task" => ["root" => PR_IPM_TASK_ENTRYID], |
||
150 | "default_folder_junk" => ["additional" => 4], |
||
151 | "default_folder_syncissues" => ["additional" => 1], |
||
152 | "default_folder_conflicts" => ["additional" => 0], |
||
153 | "default_folder_localfailures" => ["additional" => 2], |
||
154 | "default_folder_serverfailures" => ["additional" => 3], |
||
155 | ]; |
||
156 | |||
157 | foreach ($defaultfolders as $key => $prop) { |
||
158 | $tag = reset($prop); |
||
159 | $from = key($prop); |
||
160 | |||
161 | switch ($from) { |
||
162 | case "inbox": |
||
163 | if (isset($inboxProps[$tag])) { |
||
164 | $storeData["props"][$key] = bin2hex((string) $inboxProps[$tag]); |
||
165 | } |
||
166 | break; |
||
167 | |||
168 | case "store": |
||
169 | if (isset($msgstore_props[$tag])) { |
||
170 | $storeData["props"][$key] = bin2hex((string) $msgstore_props[$tag]); |
||
171 | } |
||
172 | break; |
||
173 | |||
174 | case "root": |
||
175 | if (isset($rootProps[$tag])) { |
||
176 | $storeData["props"][$key] = bin2hex((string) $rootProps[$tag]); |
||
177 | } |
||
178 | break; |
||
179 | |||
180 | case "additional": |
||
181 | if (isset($additional_ren_entryids[$tag])) { |
||
182 | $storeData["props"][$key] = bin2hex((string) $additional_ren_entryids[$tag]); |
||
183 | } |
||
184 | break; |
||
185 | } |
||
186 | } |
||
187 | |||
188 | $storeData["folders"] = ["item" => []]; |
||
189 | |||
190 | if (isset($msgstore_props[PR_IPM_SUBTREE_ENTRYID])) { |
||
191 | $subtreeFolderEntryID = $msgstore_props[PR_IPM_SUBTREE_ENTRYID]; |
||
192 | |||
193 | $openWholeStore = true; |
||
194 | if ($storeType == ZARAFA_STORE_DELEGATE_GUID) { |
||
195 | $username = strtolower((string) $storeData["props"]["user_name"]); |
||
196 | $sharedFolders = []; |
||
197 | |||
198 | // Check whether we should open the whole store or just single folders |
||
199 | if (isset($otherUsers[$username])) { |
||
200 | $sharedFolders = $otherUsers[$username]; |
||
201 | if (!isset($otherUsers[$username]['all'])) { |
||
202 | $openWholeStore = false; |
||
203 | } |
||
204 | } |
||
205 | |||
206 | // Update the store properties when this function was called to |
||
207 | // only open a particular shared store. |
||
208 | if (is_array($storeOptions)) { |
||
209 | // Update the store properties to mark previously opened |
||
210 | $prevSharedFolders = $GLOBALS["settings"]->get("zarafa/v1/contexts/hierarchy/shared_stores/" . $username, null); |
||
211 | if (!empty($prevSharedFolders)) { |
||
212 | foreach ($prevSharedFolders as $type => $prevSharedFolder) { |
||
213 | // Update the store properties to refer to the shared folder, |
||
214 | // note that we don't care if we have access to the folder or not. |
||
215 | $type = $prevSharedFolder["folder_type"]; |
||
216 | if ($type == "all") { |
||
217 | $propname = "subtree_entryid"; |
||
218 | } |
||
219 | else { |
||
220 | $propname = "default_folder_" . $prevSharedFolder["folder_type"]; |
||
221 | } |
||
222 | |||
223 | if (isset($storeData["props"][$propname])) { |
||
224 | $folderEntryID = hex2bin($storeData["props"][$propname]); |
||
225 | $storeData["props"]["shared_folder_" . $prevSharedFolder["folder_type"]] = bin2hex($folderEntryID); |
||
226 | } |
||
227 | } |
||
228 | } |
||
229 | } |
||
230 | } |
||
231 | |||
232 | // Get the IPMSUBTREE object |
||
233 | $storeAccess = true; |
||
234 | |||
235 | try { |
||
236 | $subtreeFolder = mapi_msgstore_openentry($store, $subtreeFolderEntryID); |
||
237 | // Add root folder |
||
238 | $subtree = $this->setFolder(mapi_getprops($subtreeFolder, $properties)); |
||
239 | if (!$openWholeStore) { |
||
240 | $subtree['props']['access'] = 0; |
||
241 | } |
||
242 | array_push($storeData["folders"]["item"], $subtree); |
||
243 | } |
||
244 | catch (MAPIException $e) { |
||
245 | if ($openWholeStore) { |
||
246 | /* |
||
247 | * if we are going to open whole store and we are not able to open the subtree folder |
||
248 | * then it should be considered as an error |
||
249 | * but if we are only opening single folder then it could be possible that we don't have |
||
250 | * permission to open subtree folder so add a dummy subtree folder in the response and don't consider this as an error |
||
251 | */ |
||
252 | $storeAccess = false; |
||
253 | |||
254 | // Add properties to the store response to indicate to the client |
||
255 | // that the store could not be loaded. |
||
256 | $this->invalidateResponseStore($storeData, 'all', $subtreeFolderEntryID); |
||
257 | } |
||
258 | else { |
||
259 | // Add properties to the store response to add a placeholder IPMSubtree. |
||
260 | $this->getDummyIPMSubtreeFolder($storeData, $subtreeFolderEntryID); |
||
261 | } |
||
262 | |||
263 | // We've handled the event |
||
264 | $e->setHandled(); |
||
265 | } |
||
266 | |||
267 | if ($storeAccess) { |
||
268 | // Open the whole store and be done with it |
||
269 | if ($openWholeStore) { |
||
270 | try { |
||
271 | // Update the store properties to refer to the shared folder, |
||
272 | // note that we don't care if we have access to the folder or not. |
||
273 | $storeData["props"]["shared_folder_all"] = bin2hex((string) $subtreeFolderEntryID); |
||
274 | $this->getSubFolders($subtreeFolder, $store, $properties, $storeData); |
||
275 | |||
276 | if ($storeType == ZARAFA_SERVICE_GUID) { |
||
277 | // If store type ZARAFA_SERVICE_GUID (own store) then get the |
||
278 | // IPM_COMMON_VIEWS folder and set it to folders array. |
||
279 | $storeData["favorites"] = ["item" => []]; |
||
280 | $commonViewFolderEntryid = $msgstore_props[PR_COMMON_VIEWS_ENTRYID]; |
||
281 | |||
282 | $this->setDefaultFavoritesFolder($commonViewFolderEntryid, $store, $storeData); |
||
283 | |||
284 | $commonViewFolder = mapi_msgstore_openentry($store, $commonViewFolderEntryid); |
||
285 | $this->getFavoritesFolders($commonViewFolder, $storeData); |
||
286 | |||
287 | $commonViewFolderProps = mapi_getprops($commonViewFolder); |
||
288 | array_push($storeData["folders"]["item"], $this->setFolder($commonViewFolderProps)); |
||
289 | |||
290 | // Get the To-do list folder and add it to the hierarchy |
||
291 | $todoSearchFolder = todoList::getTodoSearchFolder($store); |
||
292 | if ($todoSearchFolder) { |
||
293 | $todoSearchFolderProps = mapi_getprops($todoSearchFolder); |
||
294 | |||
295 | // Change the parent so the folder will be shown in the hierarchy |
||
296 | $todoSearchFolderProps[PR_PARENT_ENTRYID] = $subtreeFolderEntryID; |
||
297 | // Change the display name of the folder |
||
298 | $todoSearchFolderProps[PR_DISPLAY_NAME] = _('To-Do List'); |
||
299 | // Never show unread content for the To-do list |
||
300 | $todoSearchFolderProps[PR_CONTENT_UNREAD] = 0; |
||
301 | $todoSearchFolderProps[PR_CONTENT_COUNT] = 0; |
||
302 | array_push($storeData["folders"]["item"], $this->setFolder($todoSearchFolderProps)); |
||
303 | $storeData["props"]['default_folder_todolist'] = bin2hex((string) $todoSearchFolderProps[PR_ENTRYID]); |
||
304 | } |
||
305 | } |
||
306 | } |
||
307 | catch (MAPIException $e) { |
||
308 | // Add properties to the store response to indicate to the client |
||
309 | // that the store could not be loaded. |
||
310 | $this->invalidateResponseStore($storeData, 'all', $subtreeFolderEntryID); |
||
311 | |||
312 | // We've handled the event |
||
313 | $e->setHandled(); |
||
314 | } |
||
315 | |||
316 | // Open single folders under the store object |
||
317 | } |
||
318 | else { |
||
319 | foreach ($sharedFolders as $type => $sharedFolder) { |
||
320 | $openSubFolders = ($sharedFolder["show_subfolders"] == true); |
||
321 | |||
322 | // See if the folders exists by checking if it is in the default folders entryid list |
||
323 | $store_access = true; |
||
324 | if (!isset($storeData["props"]["default_folder_" . $sharedFolder["folder_type"]])) { |
||
325 | // Create a fake folder entryid which must be used for referencing this folder |
||
326 | $folderEntryID = "default_folder_" . $sharedFolder["folder_type"]; |
||
327 | |||
328 | // Add properties to the store response to indicate to the client |
||
329 | // that the store could not be loaded. |
||
330 | $this->invalidateResponseStore($storeData, $type, $folderEntryID); |
||
331 | |||
332 | // Update the store properties to refer to the shared folder, |
||
333 | // note that we don't care if we have access to the folder or not. |
||
334 | $storeData["props"]["shared_folder_" . $sharedFolder["folder_type"]] = bin2hex($folderEntryID); |
||
335 | |||
336 | // Indicate that we don't have access to the store, |
||
337 | // so no more attempts to read properties or open entries. |
||
338 | $store_access = false; |
||
339 | |||
340 | // If you access according to the above check, go ahead and retrieve the MAPIFolder object |
||
341 | } |
||
342 | else { |
||
343 | $folderEntryID = hex2bin($storeData["props"]["default_folder_" . $sharedFolder["folder_type"]]); |
||
344 | |||
345 | // Update the store properties to refer to the shared folder, |
||
346 | // note that we don't care if we have access to the folder or not. |
||
347 | $storeData["props"]["shared_folder_" . $sharedFolder["folder_type"]] = bin2hex($folderEntryID); |
||
348 | |||
349 | try { |
||
350 | // load folder props |
||
351 | $folder = mapi_msgstore_openentry($store, $folderEntryID); |
||
352 | } |
||
353 | catch (MAPIException $e) { |
||
354 | // Add properties to the store response to indicate to the client |
||
355 | // that the store could not be loaded. |
||
356 | $this->invalidateResponseStore($storeData, $type, $folderEntryID); |
||
357 | |||
358 | // Indicate that we don't have access to the store, |
||
359 | // so no more attempts to read properties or open entries. |
||
360 | $store_access = false; |
||
361 | |||
362 | // We've handled the event |
||
363 | $e->setHandled(); |
||
364 | } |
||
365 | } |
||
366 | |||
367 | // Check if a error handler already inserted a error folder, |
||
368 | // or if we can insert the real folders here. |
||
369 | if ($store_access === true) { |
||
370 | // check if we need subfolders or not |
||
371 | if ($openSubFolders === true) { |
||
372 | // add folder data (with all subfolders recursively) |
||
373 | // get parent folder's properties |
||
374 | $folderProps = mapi_getprops($folder, $properties); |
||
375 | $tempFolderProps = $this->setFolder($folderProps); |
||
376 | |||
377 | array_push($storeData["folders"]["item"], $tempFolderProps); |
||
378 | |||
379 | // get subfolders |
||
380 | if ($tempFolderProps["props"]["has_subfolder"] != false) { |
||
381 | $subfoldersData = []; |
||
382 | $subfoldersData["folders"]["item"] = []; |
||
383 | $this->getSubFolders($folder, $store, $properties, $subfoldersData); |
||
384 | |||
385 | $storeData["folders"]["item"] = array_merge($storeData["folders"]["item"], $subfoldersData["folders"]["item"]); |
||
386 | } |
||
387 | } |
||
388 | else { |
||
389 | $folderProps = mapi_getprops($folder, $properties); |
||
390 | $tempFolderProps = $this->setFolder($folderProps); |
||
391 | // We don't load subfolders, this means the user isn't allowed |
||
392 | // to create subfolders, as they should normally be hidden immediately. |
||
393 | $tempFolderProps["props"]["access"] = ($tempFolderProps["props"]["access"] & ~MAPI_ACCESS_CREATE_HIERARCHY); |
||
394 | // We don't load subfolders, so force the 'has_subfolder' property |
||
395 | // to be false, so the UI will not consider loading subfolders. |
||
396 | $tempFolderProps["props"]["has_subfolder"] = false; |
||
397 | array_push($storeData["folders"]["item"], $tempFolderProps); |
||
398 | } |
||
399 | } |
||
400 | } |
||
401 | } |
||
402 | } |
||
403 | array_push($data["item"], $storeData); |
||
404 | } |
||
405 | } |
||
406 | |||
407 | return $data; |
||
408 | } |
||
409 | |||
410 | /** |
||
411 | * Helper function to get the subfolders of a Personal Store. |
||
412 | * |
||
413 | * @param object $folder mapi Folder Object |
||
414 | * @param object $store Message Store Object |
||
415 | * @param array $properties MAPI property mappings for folders |
||
416 | * @param array $storeData Reference to an array. The folder properties are added to this array. |
||
417 | * @param mixed $parentEntryid |
||
418 | */ |
||
419 | public function getSubFolders($folder, $store, $properties, &$storeData, $parentEntryid = false) { |
||
460 | } |
||
461 | } |
||
462 | |||
463 | /** |
||
464 | * Convert MAPI properties into useful XML properties for a folder. |
||
465 | * |
||
466 | * @param array $folderProps Properties of a folder |
||
467 | * |
||
468 | * @return array List of properties of a folder |
||
469 | * |
||
470 | * @todo The name of this function is misleading because it doesn't 'set' anything, it just reads some properties. |
||
471 | */ |
||
472 | public function setFolder($folderProps) { |
||
473 | $props = [ |
||
474 | // Identification properties |
||
475 | "entryid" => bin2hex((string) $folderProps[PR_ENTRYID]), |
||
476 | "parent_entryid" => bin2hex((string) $folderProps[PR_PARENT_ENTRYID]), |
||
477 | "store_entryid" => bin2hex((string) $folderProps[PR_STORE_ENTRYID]), |
||
478 | // Scalar properties |
||
479 | "props" => [ |
||
480 | "display_name" => $folderProps[PR_DISPLAY_NAME], |
||
481 | "object_type" => $folderProps[PR_OBJECT_TYPE] ?? MAPI_FOLDER, // FIXME: Why isn't this always set? |
||
482 | "content_count" => $folderProps[PR_CONTENT_COUNT] ?? 0, |
||
483 | "content_unread" => $folderProps[PR_CONTENT_UNREAD] ?? 0, |
||
484 | "has_subfolder" => $folderProps[PR_SUBFOLDERS] ?? false, |
||
485 | "container_class" => $folderProps[PR_CONTAINER_CLASS] ?? "IPF.Note", |
||
486 | "access" => $folderProps[PR_ACCESS], |
||
487 | "rights" => $folderProps[PR_RIGHTS] ?? ecRightsNone, |
||
488 | "assoc_content_count" => $folderProps[PR_ASSOC_CONTENT_COUNT] ?? 0, |
||
489 | ], |
||
490 | ]; |
||
491 | |||
492 | $this->setExtendedFolderFlags($folderProps, $props); |
||
493 | |||
494 | return $props; |
||
495 | } |
||
496 | |||
497 | /** |
||
498 | * Function is used to retrieve the favorites and search folders from |
||
499 | * respective favorites(IPM.Microsoft.WunderBar.Link) and search (IPM.Microsoft.WunderBar.SFInfo) |
||
500 | * link messages which belongs to associated contains table of IPM_COMMON_VIEWS folder. |
||
501 | * |
||
502 | * @param object $commonViewFolder MAPI Folder Object in which the favorites link messages lives |
||
503 | * @param array $storeData Reference to an array. The favorites folder properties are added to this array. |
||
504 | */ |
||
505 | public function getFavoritesFolders($commonViewFolder, &$storeData) { |
||
506 | $table = mapi_folder_getcontentstable($commonViewFolder, MAPI_ASSOCIATED); |
||
507 | |||
508 | $restriction = [RES_OR, |
||
509 | [ |
||
510 | [RES_PROPERTY, |
||
511 | [ |
||
512 | RELOP => RELOP_EQ, |
||
513 | ULPROPTAG => PR_MESSAGE_CLASS, |
||
514 | VALUE => [PR_MESSAGE_CLASS => "IPM.Microsoft.WunderBar.Link"], |
||
515 | ], |
||
516 | ], |
||
517 | [RES_PROPERTY, |
||
518 | [ |
||
519 | RELOP => RELOP_EQ, |
||
520 | ULPROPTAG => PR_MESSAGE_CLASS, |
||
521 | VALUE => [PR_MESSAGE_CLASS => "IPM.Microsoft.WunderBar.SFInfo"], |
||
522 | ], |
||
523 | ], |
||
524 | ], |
||
525 | ]; |
||
526 | |||
527 | // Get hierarchy table from all FINDERS_ROOT folders of |
||
528 | // all message stores. |
||
529 | $stores = $GLOBALS["mapisession"]->getAllMessageStores(); |
||
530 | $finderHierarchyTables = []; |
||
531 | foreach ($stores as $entryid => $store) { |
||
532 | $props = mapi_getprops($store, [PR_DEFAULT_STORE, PR_FINDER_ENTRYID]); |
||
533 | if (!$props[PR_DEFAULT_STORE]) { |
||
534 | continue; |
||
535 | } |
||
536 | |||
537 | try { |
||
538 | $finderFolder = mapi_msgstore_openentry($store, $props[PR_FINDER_ENTRYID]); |
||
539 | $hierarchyTable = mapi_folder_gethierarchytable($finderFolder, MAPI_DEFERRED_ERRORS); |
||
540 | $finderHierarchyTables[$props[PR_FINDER_ENTRYID]] = $hierarchyTable; |
||
541 | } |
||
542 | catch (MAPIException $e) { |
||
543 | $e->setHandled(); |
||
544 | $props = mapi_getprops($store, [PR_DISPLAY_NAME]); |
||
545 | error_log(sprintf( |
||
546 | "Unable to open FINDER_ROOT for store \"%s\": %s (%s)", |
||
547 | $props[PR_DISPLAY_NAME], |
||
548 | mapi_strerror($e->getCode()), |
||
549 | get_mapi_error_name($e->getCode()) |
||
550 | )); |
||
551 | } |
||
552 | } |
||
553 | |||
554 | $rows = mapi_table_queryallrows($table, $GLOBALS["properties"]->getFavoritesFolderProperties(), $restriction); |
||
555 | $faultyLinkMsg = []; |
||
556 | foreach ($rows as $row) { |
||
557 | if (isset($row[PR_WLINK_TYPE]) && $row[PR_WLINK_TYPE] > wblSharedFolder) { |
||
558 | continue; |
||
559 | } |
||
560 | |||
561 | try { |
||
562 | if ($row[PR_MESSAGE_CLASS] === "IPM.Microsoft.WunderBar.Link") { |
||
563 | // Find faulty link messages which does not linked to any message. if link message |
||
564 | // does not contains store entryid in which actual message is located then it consider as |
||
565 | // faulty link message. |
||
566 | if (isset($row[PR_WLINK_STORE_ENTRYID]) && empty($row[PR_WLINK_STORE_ENTRYID]) || |
||
567 | !isset($row[PR_WLINK_STORE_ENTRYID])) { |
||
568 | // Outlook apparently doesn't set PR_WLINK_STORE_ENTRYID |
||
569 | // for with free/busy permission only opened shared calendars, |
||
570 | // so do not remove them from the IPM_COMMON_VIEWS |
||
571 | if ((isset($row[PR_WLINK_SECTION]) && $row[PR_WLINK_SECTION] != wbsidCalendar) || |
||
572 | !isset($row[PR_WLINK_SECTION])) { |
||
573 | array_push($faultyLinkMsg, $row[PR_ENTRYID]); |
||
574 | } |
||
575 | |||
576 | continue; |
||
577 | } |
||
578 | $props = $this->getFavoriteLinkedFolderProps($row); |
||
579 | if (empty($props)) { |
||
580 | continue; |
||
581 | } |
||
582 | } |
||
583 | elseif ($row[PR_MESSAGE_CLASS] === "IPM.Microsoft.WunderBar.SFInfo") { |
||
584 | $props = $this->getFavoritesLinkedSearchFolderProps($row[PR_WB_SF_ID], $finderHierarchyTables); |
||
585 | if (empty($props)) { |
||
586 | continue; |
||
587 | } |
||
588 | } |
||
589 | } |
||
590 | catch (MAPIException) { |
||
591 | continue; |
||
592 | } |
||
593 | |||
594 | array_push($storeData['favorites']['item'], $this->setFavoritesFolder($props)); |
||
595 | } |
||
596 | |||
597 | if (!empty($faultyLinkMsg)) { |
||
598 | // remove faulty link messages from common view folder. |
||
599 | mapi_folder_deletemessages($commonViewFolder, $faultyLinkMsg); |
||
600 | } |
||
601 | } |
||
602 | |||
603 | /** |
||
604 | * Function which checks whether given linked Message is faulty or not. |
||
605 | * It will store faulty linked messages in given &$faultyLinkMsg array. |
||
606 | * Returns true if linked message of favorite item is faulty. |
||
607 | * |
||
608 | * @param array &$faultyLinkMsg reference in which faulty linked messages will be stored |
||
609 | * @param array $allMessageStores Associative array with entryid -> mapistore of all open stores (private, public, delegate) |
||
610 | * @param object $linkedMessage link message which belongs to associated contains table of IPM_COMMON_VIEWS folder |
||
611 | * |
||
612 | * @return true if linked message of favorite item is faulty or false |
||
613 | */ |
||
614 | public function checkFaultyFavoritesLinkedFolder(&$faultyLinkMsg, $allMessageStores, $linkedMessage) { |
||
615 | // Find faulty link messages which does not linked to any message. if link message |
||
616 | // does not contains store entryid in which actual message is located then it consider as |
||
617 | // faulty link message. |
||
618 | if (isset($linkedMessage[PR_WLINK_STORE_ENTRYID]) && empty($linkedMessage[PR_WLINK_STORE_ENTRYID])) { |
||
619 | array_push($faultyLinkMsg, $linkedMessage[PR_ENTRYID]); |
||
620 | |||
621 | return true; |
||
622 | } |
||
623 | |||
624 | // Check if store of a favorite Item does not exist in Hierarchy then |
||
625 | // delete link message of that favorite item. |
||
626 | // i.e. If a user is unhooked then remove its favorite items. |
||
627 | $storeExist = array_key_exists($linkedMessage[PR_WLINK_STORE_ENTRYID], $allMessageStores); |
||
628 | if (!$storeExist) { |
||
629 | array_push($faultyLinkMsg, $linkedMessage[PR_ENTRYID]); |
||
630 | |||
631 | return true; |
||
632 | } |
||
633 | |||
634 | return false; |
||
635 | } |
||
636 | |||
637 | /** |
||
638 | * Function which get the favorites marked folders from favorites link message |
||
639 | * which belongs to associated contains table of IPM_COMMON_VIEWS folder. |
||
640 | * |
||
641 | * @param array $linkMessageProps properties of link message which belongs to |
||
642 | * associated contains table of IPM_COMMON_VIEWS folder |
||
643 | * |
||
644 | * @return array List of properties of a folder |
||
645 | */ |
||
646 | public function getFavoriteLinkedFolderProps($linkMessageProps) { |
||
647 | // In webapp we use IPM_SUBTREE as root folder for the Hierarchy but OL is use IMsgStore as a |
||
648 | // Root folder. OL never mark favorites to IPM_SUBTREE. So to make favorites compatible with OL |
||
649 | // we need this check. |
||
650 | // Here we check PR_WLINK_STORE_ENTRYID and PR_WLINK_ENTRYID is same. Which same only in one case |
||
651 | // where some user has mark favorites to root(Inbox-<user name>) folder from OL. So here if condition |
||
652 | // gets true we get the IPM_SUBTREE and send it to response as favorites folder to webapp. |
||
653 | try { |
||
654 | if ($GLOBALS['entryid']->compareEntryIds($linkMessageProps[PR_WLINK_STORE_ENTRYID], $linkMessageProps[PR_WLINK_ENTRYID])) { |
||
655 | $storeObj = $GLOBALS["mapisession"]->openMessageStore($linkMessageProps[PR_WLINK_STORE_ENTRYID]); |
||
656 | $subTreeEntryid = mapi_getprops($storeObj, [PR_IPM_SUBTREE_ENTRYID]); |
||
657 | $folderObj = mapi_msgstore_openentry($storeObj, $subTreeEntryid[PR_IPM_SUBTREE_ENTRYID]); |
||
658 | } |
||
659 | else { |
||
660 | $storeObj = $GLOBALS["mapisession"]->openMessageStore($linkMessageProps[PR_WLINK_STORE_ENTRYID]); |
||
661 | if (!is_resource($storeObj)) { |
||
662 | return false; |
||
663 | } |
||
664 | $folderObj = mapi_msgstore_openentry($storeObj, $linkMessageProps[PR_WLINK_ENTRYID]); |
||
665 | } |
||
666 | |||
667 | return mapi_getprops($folderObj, $GLOBALS["properties"]->getFavoritesFolderProperties()); |
||
668 | } |
||
669 | catch (Exception) { |
||
670 | // in some cases error_log was causing an endless loop, so disable it for now |
||
671 | // error_log($e); |
||
672 | } |
||
673 | |||
674 | return false; |
||
675 | } |
||
676 | |||
677 | /** |
||
678 | * Function which retrieve the search folder from FINDERS_ROOT folder of all open |
||
679 | * message store. |
||
680 | * |
||
681 | * @param string $searchFolderId contains a GUID that identifies the search folder. |
||
682 | * The value of this property MUST NOT change. |
||
683 | * @param array $finderHierarchyTables hierarchy tables which belongs to FINDERS_ROOT |
||
684 | * folder of message stores |
||
685 | * |
||
686 | * @return array list of search folder properties |
||
687 | */ |
||
688 | public function getFavoritesLinkedSearchFolderProps($searchFolderId, $finderHierarchyTables) { |
||
689 | $restriction = [RES_EXIST, |
||
690 | [ |
||
691 | ULPROPTAG => PR_EXTENDED_FOLDER_FLAGS, |
||
692 | ], |
||
693 | ]; |
||
694 | |||
695 | foreach ($finderHierarchyTables as $finderEntryid => $hierarchyTable) { |
||
696 | $rows = mapi_table_queryallrows($hierarchyTable, $GLOBALS["properties"]->getFavoritesFolderProperties(), $restriction); |
||
697 | foreach ($rows as $row) { |
||
698 | $flags = unpack("H2ExtendedFlags-Id/H2ExtendedFlags-Cb/H8ExtendedFlags-Data/H2SearchFolderTag-Id/H2SearchFolderTag-Cb/H8SearchFolderTag-Data/H2SearchFolderId-Id/H2SearchFolderId-Cb/H32SearchFolderId-Data", (string) $row[PR_EXTENDED_FOLDER_FLAGS]); |
||
699 | if ($flags["SearchFolderId-Data"] === bin2hex($searchFolderId)) { |
||
700 | return $row; |
||
701 | } |
||
702 | } |
||
703 | } |
||
704 | } |
||
705 | |||
706 | /** |
||
707 | * Create link messages for default favorites(Inbox and Sent Items) folders in associated contains table of IPM_COMMON_VIEWS folder |
||
708 | * and remove all other link message from the same. |
||
709 | * |
||
710 | * @param string $commonViewFolderEntryid IPM_COMMON_VIEWS folder entryid |
||
711 | * @param object $store Message Store Object |
||
712 | * @param array $storeData the store data which use to create restriction |
||
713 | */ |
||
714 | public function setDefaultFavoritesFolder($commonViewFolderEntryid, $store, $storeData) { |
||
715 | if ($GLOBALS["settings"]->get("zarafa/v1/contexts/hierarchy/show_default_favorites") !== false) { |
||
716 | $commonViewFolder = mapi_msgstore_openentry($store, $commonViewFolderEntryid); |
||
717 | |||
718 | $inboxFolderEntryid = hex2bin((string) $storeData["props"]["default_folder_inbox"]); |
||
719 | $sentFolderEntryid = hex2bin((string) $storeData["props"]["default_folder_sent"]); |
||
720 | |||
721 | $table = mapi_folder_getcontentstable($commonViewFolder, MAPI_ASSOCIATED); |
||
722 | |||
723 | // Restriction for get all link message(IPM.Microsoft.WunderBar.Link) |
||
724 | // and search link message (IPM.Microsoft.WunderBar.SFInfo) from |
||
725 | // Associated contains table of IPM_COMMON_VIEWS folder. |
||
726 | $findLinkMsgRestriction = [RES_OR, |
||
727 | [ |
||
728 | [RES_PROPERTY, |
||
729 | [ |
||
730 | RELOP => RELOP_EQ, |
||
731 | ULPROPTAG => PR_MESSAGE_CLASS, |
||
732 | VALUE => [PR_MESSAGE_CLASS => "IPM.Microsoft.WunderBar.Link"], |
||
733 | ], |
||
734 | ], |
||
735 | [RES_PROPERTY, |
||
736 | [ |
||
737 | RELOP => RELOP_EQ, |
||
738 | ULPROPTAG => PR_MESSAGE_CLASS, |
||
739 | VALUE => [PR_MESSAGE_CLASS => "IPM.Microsoft.WunderBar.SFInfo"], |
||
740 | ], |
||
741 | ], |
||
742 | ], |
||
743 | ]; |
||
744 | |||
745 | // Restriction for find Inbox and/or Sent folder link message from |
||
746 | // Associated contains table of IPM_COMMON_VIEWS folder. |
||
747 | $findInboxOrSentLinkMessage = [RES_OR, |
||
748 | [ |
||
749 | [RES_PROPERTY, |
||
750 | [ |
||
751 | RELOP => RELOP_EQ, |
||
752 | ULPROPTAG => PR_WLINK_ENTRYID, |
||
753 | VALUE => [PR_WLINK_ENTRYID => $inboxFolderEntryid], |
||
754 | ], |
||
755 | ], |
||
756 | [RES_PROPERTY, |
||
757 | [ |
||
758 | RELOP => RELOP_EQ, |
||
759 | ULPROPTAG => PR_WLINK_ENTRYID, |
||
760 | VALUE => [PR_WLINK_ENTRYID => $sentFolderEntryid], |
||
761 | ], |
||
762 | ], |
||
763 | ], |
||
764 | ]; |
||
765 | |||
766 | // Restriction to get all link messages except Inbox and Sent folder's link messages from |
||
767 | // Associated contains table of IPM_COMMON_VIEWS folder, if exist in it. |
||
768 | $restriction = [RES_AND, |
||
769 | [ |
||
770 | $findLinkMsgRestriction, |
||
771 | [RES_NOT, |
||
772 | [ |
||
773 | $findInboxOrSentLinkMessage, |
||
774 | ], |
||
775 | ], |
||
776 | ], |
||
777 | ]; |
||
778 | |||
779 | $rows = mapi_table_queryallrows($table, [PR_ENTRYID], $restriction); |
||
780 | if (!empty($rows)) { |
||
781 | $deleteMessages = []; |
||
782 | foreach ($rows as $row) { |
||
783 | array_push($deleteMessages, $row[PR_ENTRYID]); |
||
784 | } |
||
785 | mapi_folder_deletemessages($commonViewFolder, $deleteMessages); |
||
786 | } |
||
787 | |||
788 | // We need to remove all search folder from FIND_ROOT(search root folder) |
||
789 | // when reset setting was triggered because on reset setting we remove all |
||
790 | // link messages from common view folder which are linked with either |
||
791 | // favorites or search folder. |
||
792 | $finderFolderEntryid = hex2bin((string) $storeData["props"]["finder_entryid"]); |
||
793 | $finderFolder = mapi_msgstore_openentry($store, $finderFolderEntryid); |
||
794 | $hierarchyTable = mapi_folder_gethierarchytable($finderFolder, MAPI_DEFERRED_ERRORS); |
||
795 | $folders = mapi_table_queryallrows($hierarchyTable, [PR_ENTRYID]); |
||
796 | foreach ($folders as $folder) { |
||
797 | try { |
||
798 | mapi_folder_deletefolder($finderFolder, $folder[PR_ENTRYID]); |
||
799 | } |
||
800 | catch (MAPIException $e) { |
||
801 | $msg = "Problem in deleting search folder while reset settings. MAPI Error %s."; |
||
802 | $formattedMsg = sprintf($msg, get_mapi_error_name($e->getCode())); |
||
803 | error_log($formattedMsg); |
||
804 | Log::Write(LOGLEVEL_ERROR, "Operations:setDefaultFavoritesFolder() " . $formattedMsg); |
||
805 | } |
||
806 | } |
||
807 | // Restriction used to find only Inbox and Sent folder's link messages from |
||
808 | // Associated contains table of IPM_COMMON_VIEWS folder, if exist in it. |
||
809 | $restriction = [RES_AND, |
||
810 | [ |
||
811 | $findLinkMsgRestriction, |
||
812 | $findInboxOrSentLinkMessage, |
||
813 | ], |
||
814 | ]; |
||
815 | |||
816 | $rows = mapi_table_queryallrows($table, [PR_WLINK_ENTRYID], $restriction); |
||
817 | |||
818 | // If Inbox and Sent folder's link messages are not exist then create the |
||
819 | // link message for those in associated contains table of IPM_COMMON_VIEWS folder. |
||
820 | if (empty($rows)) { |
||
821 | $defaultFavFoldersKeys = ["inbox", "sent"]; |
||
822 | foreach ($defaultFavFoldersKeys as $folderKey) { |
||
823 | $folderObj = $GLOBALS["mapisession"]->openMessage(hex2bin((string) $storeData["props"]["default_folder_" . $folderKey])); |
||
824 | $props = mapi_getprops($folderObj, [PR_ENTRYID, PR_STORE_ENTRYID, PR_DISPLAY_NAME]); |
||
825 | $this->createFavoritesLink($commonViewFolder, $props); |
||
826 | } |
||
827 | } |
||
828 | elseif (count($rows) < 2) { |
||
829 | // If rows count is less than 2 it means associated contains table of IPM_COMMON_VIEWS folder |
||
830 | // can have either Inbox or Sent folder link message in it. So we have to create link message |
||
831 | // for Inbox or Sent folder which ever not exist in associated contains table of IPM_COMMON_VIEWS folder |
||
832 | // to maintain default favorites folder. |
||
833 | $row = $rows[0]; |
||
834 | $wlinkEntryid = $row[PR_WLINK_ENTRYID]; |
||
835 | |||
836 | $isInboxFolder = $GLOBALS['entryid']->compareEntryIds($wlinkEntryid, $inboxFolderEntryid); |
||
837 | |||
838 | if (!$isInboxFolder) { |
||
839 | $folderObj = $GLOBALS["mapisession"]->openMessage($inboxFolderEntryid); |
||
840 | } |
||
841 | else { |
||
842 | $folderObj = $GLOBALS["mapisession"]->openMessage($sentFolderEntryid); |
||
843 | } |
||
844 | |||
845 | $props = mapi_getprops($folderObj, [PR_ENTRYID, PR_STORE_ENTRYID, PR_DISPLAY_NAME]); |
||
846 | $this->createFavoritesLink($commonViewFolder, $props); |
||
847 | } |
||
848 | $GLOBALS["settings"]->set("zarafa/v1/contexts/hierarchy/show_default_favorites", false, true); |
||
849 | } |
||
850 | } |
||
851 | |||
852 | /** |
||
853 | * Create favorites link message (IPM.Microsoft.WunderBar.Link) or |
||
854 | * search link message ("IPM.Microsoft.WunderBar.SFInfo") in associated |
||
855 | * contains table of IPM_COMMON_VIEWS folder. |
||
856 | * |
||
857 | * @param object $commonViewFolder MAPI Message Folder Object |
||
858 | * @param array $folderProps Properties of a folder |
||
859 | * @param bool|string $searchFolderId search folder id which is used to identify the |
||
860 | * linked search folder from search link message. by default it is false. |
||
861 | */ |
||
862 | public function createFavoritesLink($commonViewFolder, $folderProps, $searchFolderId = false) { |
||
883 | } |
||
884 | |||
885 | /** |
||
886 | * Convert MAPI properties into useful and human readable string for favorites folder. |
||
887 | * |
||
888 | * @param array $folderProps Properties of a folder |
||
889 | * |
||
890 | * @return array List of properties of a folder |
||
891 | */ |
||
892 | public function setFavoritesFolder($folderProps) { |
||
893 | $props = $this->setFolder($folderProps); |
||
894 | // Add and Make isFavorites to true, this allows the client to properly |
||
895 | // indicate to the user that this is a favorites item/folder. |
||
896 | $props["props"]["isFavorites"] = true; |
||
897 | $props["props"]["folder_type"] = $folderProps[PR_FOLDER_TYPE]; |
||
898 | |||
899 | return $props; |
||
900 | } |
||
901 | |||
902 | /** |
||
903 | * Fetches extended flags for folder. If PR_EXTENDED_FLAGS is not set then we assume that client |
||
904 | * should handle which property to display. |
||
905 | * |
||
906 | * @param array $folderProps Properties of a folder |
||
907 | * @param array $props properties in which flags should be set |
||
908 | */ |
||
909 | public function setExtendedFolderFlags($folderProps, &$props) { |
||
910 | if (isset($folderProps[PR_EXTENDED_FOLDER_FLAGS])) { |
||
911 | $flags = unpack("Cid/Cconst/Cflags", $folderProps[PR_EXTENDED_FOLDER_FLAGS]); |
||
912 | |||
913 | // ID property is '1' this means 'Data' property contains extended flags |
||
914 | if ($flags["id"] == 1) { |
||
915 | $props["props"]["extended_flags"] = $flags["flags"]; |
||
916 | } |
||
917 | } |
||
918 | } |
||
919 | |||
920 | /** |
||
921 | * Used to update the storeData with a folder and properties that will |
||
922 | * inform the user that the store could not be opened. |
||
923 | * |
||
924 | * @param array &$storeData The store data which will be updated |
||
925 | * @param string $folderType The foldertype which was attempted to be loaded |
||
926 | * @param array $folderEntryID The entryid of the which was attempted to be opened |
||
927 | */ |
||
928 | public function invalidateResponseStore(&$storeData, $folderType, $folderEntryID) { |
||
929 | $folderName = "Folder"; |
||
930 | $containerClass = "IPF.Note"; |
||
931 | |||
932 | switch ($folderType) { |
||
933 | case "all": |
||
934 | $folderName = "IPM_SUBTREE"; |
||
935 | $containerClass = "IPF.Note"; |
||
936 | break; |
||
937 | |||
938 | case "calendar": |
||
939 | $folderName = _("Calendar"); |
||
940 | $containerClass = "IPF.Appointment"; |
||
941 | break; |
||
942 | |||
943 | case "contact": |
||
944 | $folderName = _("Contacts"); |
||
945 | $containerClass = "IPF.Contact"; |
||
946 | break; |
||
947 | |||
948 | case "inbox": |
||
949 | $folderName = _("Inbox"); |
||
950 | $containerClass = "IPF.Note"; |
||
951 | break; |
||
952 | |||
953 | case "note": |
||
954 | $folderName = _("Notes"); |
||
955 | $containerClass = "IPF.StickyNote"; |
||
956 | break; |
||
957 | |||
958 | case "task": |
||
959 | $folderName = _("Tasks"); |
||
960 | $containerClass = "IPF.Task"; |
||
961 | break; |
||
962 | } |
||
963 | |||
964 | // Insert a fake folder which will be shown to the user |
||
965 | // to acknowledge that he has a shared store, but also |
||
966 | // to indicate that he can't open it. |
||
967 | $tempFolderProps = $this->setFolder([ |
||
968 | PR_ENTRYID => $folderEntryID, |
||
969 | PR_PARENT_ENTRYID => hex2bin((string) $storeData["props"]["subtree_entryid"]), |
||
970 | PR_STORE_ENTRYID => hex2bin((string) $storeData["store_entryid"]), |
||
971 | PR_DISPLAY_NAME => $folderName, |
||
972 | PR_OBJECT_TYPE => MAPI_FOLDER, |
||
973 | PR_SUBFOLDERS => false, |
||
974 | PR_CONTAINER_CLASS => $containerClass, |
||
975 | PR_ACCESS => 0, |
||
976 | ]); |
||
977 | |||
978 | // Mark the folder as unavailable, this allows the client to properly |
||
979 | // indicate to the user that this is a fake entry. |
||
980 | $tempFolderProps['props']['is_unavailable'] = true; |
||
981 | |||
982 | array_push($storeData["folders"]["item"], $tempFolderProps); |
||
983 | |||
984 | /* TRANSLATORS: This indicates that the opened folder belongs to a particular user, |
||
985 | * for example: 'Calendar of Holiday', in this case %1$s is 'Calendar' (the foldername) |
||
986 | * and %2$s is 'Holiday' (the username). |
||
987 | */ |
||
988 | $storeData["props"]["display_name"] = ($folderType === "all") ? $storeData["props"]["display_name"] : sprintf(_('%1$s of %2$s'), $folderName, $storeData["props"]["mailbox_owner_name"]); |
||
989 | $storeData["props"]["subtree_entryid"] = $tempFolderProps["parent_entryid"]; |
||
990 | $storeData["props"]["folder_type"] = $folderType; |
||
991 | } |
||
992 | |||
993 | /** |
||
994 | * Used to update the storeData with a folder and properties that will function as a |
||
995 | * placeholder for the IPMSubtree that could not be opened. |
||
996 | * |
||
997 | * @param array &$storeData The store data which will be updated |
||
998 | * @param array $folderEntryID The entryid of the which was attempted to be opened |
||
999 | */ |
||
1000 | public function getDummyIPMSubtreeFolder(&$storeData, $folderEntryID) { |
||
1001 | // Insert a fake folder which will be shown to the user |
||
1002 | // to acknowledge that he has a shared store. |
||
1003 | $tempFolderProps = $this->setFolder([ |
||
1004 | PR_ENTRYID => $folderEntryID, |
||
1005 | PR_PARENT_ENTRYID => hex2bin((string) $storeData["props"]["subtree_entryid"]), |
||
1006 | PR_STORE_ENTRYID => hex2bin((string) $storeData["store_entryid"]), |
||
1007 | PR_DISPLAY_NAME => "IPM_SUBTREE", |
||
1008 | PR_OBJECT_TYPE => MAPI_FOLDER, |
||
1009 | PR_SUBFOLDERS => true, |
||
1010 | PR_CONTAINER_CLASS => "IPF.Note", |
||
1011 | PR_ACCESS => 0, |
||
1012 | ]); |
||
1013 | |||
1014 | array_push($storeData["folders"]["item"], $tempFolderProps); |
||
1015 | $storeData["props"]["subtree_entryid"] = $tempFolderProps["parent_entryid"]; |
||
1016 | } |
||
1017 | |||
1018 | /** |
||
1019 | * Create a MAPI folder. |
||
1020 | * |
||
1021 | * This function simply creates a MAPI folder at a specific location with a specific folder |
||
1022 | * type. |
||
1023 | * |
||
1024 | * @param object $store MAPI Message Store Object in which the folder lives |
||
1025 | * @param string $parententryid The parent entryid in which the new folder should be created |
||
1026 | * @param string $name The name of the new folder |
||
1027 | * @param string $type The type of the folder (PR_CONTAINER_CLASS, so value should be 'IPM.Appointment', etc) |
||
1028 | * @param array $folderProps reference to an array which will be filled with PR_ENTRYID and PR_STORE_ENTRYID of new folder |
||
1029 | * |
||
1030 | * @return bool true if action succeeded, false if not |
||
1031 | */ |
||
1032 | public function createFolder($store, $parententryid, $name, $type, &$folderProps) { |
||
1033 | $result = false; |
||
1034 | $folder = mapi_msgstore_openentry($store, $parententryid); |
||
1035 | |||
1036 | if ($folder) { |
||
1037 | /** |
||
1038 | * @TODO: If parent folder has any sub-folder with the same name than this will return |
||
1039 | * MAPI_E_COLLISION error, so show this error to client and don't close the dialog. |
||
1040 | */ |
||
1041 | $new_folder = mapi_folder_createfolder($folder, $name); |
||
1042 | |||
1043 | if ($new_folder) { |
||
1044 | mapi_setprops($new_folder, [PR_CONTAINER_CLASS => $type]); |
||
1045 | $result = true; |
||
1046 | |||
1047 | $folderProps = mapi_getprops($new_folder, [PR_ENTRYID, PR_STORE_ENTRYID]); |
||
1048 | } |
||
1049 | } |
||
1050 | |||
1051 | return $result; |
||
1052 | } |
||
1053 | |||
1054 | /** |
||
1055 | * Rename a folder. |
||
1056 | * |
||
1057 | * This function renames the specified folder. However, a conflict situation can arise |
||
1058 | * if the specified folder name already exists. In this case, the folder name is postfixed with |
||
1059 | * an ever-higher integer to create a unique folder name. |
||
1060 | * |
||
1061 | * @param object $store MAPI Message Store Object |
||
1062 | * @param string $entryid The entryid of the folder to rename |
||
1063 | * @param string $name The new name of the folder |
||
1064 | * @param array $folderProps reference to an array which will be filled with PR_ENTRYID and PR_STORE_ENTRYID |
||
1065 | * |
||
1066 | * @return bool true if action succeeded, false if not |
||
1067 | */ |
||
1068 | public function renameFolder($store, $entryid, $name, &$folderProps) { |
||
1069 | $folder = mapi_msgstore_openentry($store, $entryid); |
||
1070 | if (!$folder) { |
||
1071 | return false; |
||
1072 | } |
||
1073 | $result = false; |
||
1074 | $folderProps = mapi_getprops($folder, [PR_ENTRYID, PR_STORE_ENTRYID, PR_DISPLAY_NAME]); |
||
1075 | |||
1076 | /* |
||
1077 | * If parent folder has any sub-folder with the same name than this will return |
||
1078 | * MAPI_E_COLLISION error while renaming folder, so show this error to client, |
||
1079 | * and revert changes in view. |
||
1080 | */ |
||
1081 | try { |
||
1082 | mapi_setprops($folder, [PR_DISPLAY_NAME => $name]); |
||
1083 | mapi_savechanges($folder); |
||
1084 | $result = true; |
||
1085 | } |
||
1086 | catch (MAPIException $e) { |
||
1087 | if ($e->getCode() == MAPI_E_COLLISION) { |
||
1088 | /* |
||
1089 | * revert folder name to original one |
||
1090 | * There is a bug in php-mapi that updates folder name in hierarchy table with null value |
||
1091 | * so we need to revert those change by again setting the old folder name |
||
1092 | * (ZCP-11586) |
||
1093 | */ |
||
1094 | mapi_setprops($folder, [PR_DISPLAY_NAME => $folderProps[PR_DISPLAY_NAME]]); |
||
1095 | mapi_savechanges($folder); |
||
1096 | } |
||
1097 | |||
1098 | // rethrow exception so we will send error to client |
||
1099 | throw $e; |
||
1100 | } |
||
1101 | |||
1102 | return $result; |
||
1103 | } |
||
1104 | |||
1105 | /** |
||
1106 | * Check if a folder is 'special'. |
||
1107 | * |
||
1108 | * All default MAPI folders such as 'inbox', 'outbox', etc have special permissions; you can not rename them for example. This |
||
1109 | * function returns TRUE if the specified folder is 'special'. |
||
1110 | * |
||
1111 | * @param object $store MAPI Message Store Object |
||
1112 | * @param string $entryid The entryid of the folder |
||
1113 | * |
||
1114 | * @return bool true if folder is a special folder, false if not |
||
1115 | */ |
||
1116 | public function isSpecialFolder($store, $entryid) { |
||
1117 | $msgstore_props = mapi_getprops($store, [PR_MDB_PROVIDER]); |
||
1118 | |||
1119 | // "special" folders don't exists in public store |
||
1120 | if ($msgstore_props[PR_MDB_PROVIDER] == ZARAFA_STORE_PUBLIC_GUID) { |
||
1121 | return false; |
||
1122 | } |
||
1123 | |||
1124 | // Check for the Special folders which are provided on the store |
||
1125 | $msgstore_props = mapi_getprops($store, [ |
||
1126 | PR_IPM_SUBTREE_ENTRYID, |
||
1127 | PR_IPM_OUTBOX_ENTRYID, |
||
1128 | PR_IPM_SENTMAIL_ENTRYID, |
||
1129 | PR_IPM_WASTEBASKET_ENTRYID, |
||
1130 | PR_IPM_PUBLIC_FOLDERS_ENTRYID, |
||
1131 | PR_IPM_FAVORITES_ENTRYID, |
||
1132 | ]); |
||
1133 | |||
1134 | if (array_search($entryid, $msgstore_props)) { |
||
1135 | return true; |
||
1136 | } |
||
1137 | |||
1138 | // Check for the Special folders which are provided on the root folder |
||
1139 | $root = mapi_msgstore_openentry($store); |
||
1140 | $rootProps = mapi_getprops($root, [ |
||
1141 | PR_IPM_APPOINTMENT_ENTRYID, |
||
1142 | PR_IPM_CONTACT_ENTRYID, |
||
1143 | PR_IPM_DRAFTS_ENTRYID, |
||
1144 | PR_IPM_JOURNAL_ENTRYID, |
||
1145 | PR_IPM_NOTE_ENTRYID, |
||
1146 | PR_IPM_TASK_ENTRYID, |
||
1147 | PR_ADDITIONAL_REN_ENTRYIDS, |
||
1148 | ]); |
||
1149 | |||
1150 | if (array_search($entryid, $rootProps)) { |
||
1151 | return true; |
||
1152 | } |
||
1153 | |||
1154 | // The PR_ADDITIONAL_REN_ENTRYIDS are a bit special |
||
1155 | if (isset($rootProps[PR_ADDITIONAL_REN_ENTRYIDS]) && is_array($rootProps[PR_ADDITIONAL_REN_ENTRYIDS])) { |
||
1156 | if (array_search($entryid, $rootProps[PR_ADDITIONAL_REN_ENTRYIDS])) { |
||
1157 | return true; |
||
1158 | } |
||
1159 | } |
||
1160 | |||
1161 | // Check if the given folder is the inbox, note that we are unsure |
||
1162 | // if we have permissions on that folder, so we need a try catch. |
||
1163 | try { |
||
1164 | $inbox = mapi_msgstore_getreceivefolder($store); |
||
1165 | $props = mapi_getprops($inbox, [PR_ENTRYID]); |
||
1166 | |||
1167 | if ($props[PR_ENTRYID] == $entryid) { |
||
1168 | return true; |
||
1169 | } |
||
1170 | } |
||
1171 | catch (MAPIException $e) { |
||
1172 | if ($e->getCode() !== MAPI_E_NO_ACCESS) { |
||
1173 | throw $e; |
||
1174 | } |
||
1175 | } |
||
1176 | |||
1177 | return false; |
||
1178 | } |
||
1179 | |||
1180 | /** |
||
1181 | * Delete a folder. |
||
1182 | * |
||
1183 | * Deleting a folder normally just moves the folder to the wastebasket, which is what this function does. However, |
||
1184 | * if the folder was already in the wastebasket, then the folder is really deleted. |
||
1185 | * |
||
1186 | * @param object $store MAPI Message Store Object |
||
1187 | * @param string $parententryid The parent in which the folder should be deleted |
||
1188 | * @param string $entryid The entryid of the folder which will be deleted |
||
1189 | * @param array $folderProps reference to an array which will be filled with PR_ENTRYID, PR_STORE_ENTRYID of the deleted object |
||
1190 | * @param bool $softDelete flag for indicating that folder should be soft deleted which can be recovered from |
||
1191 | * restore deleted items |
||
1192 | * @param bool $hardDelete flag for indicating that folder should be hard deleted from system and can not be |
||
1193 | * recovered from restore soft deleted items |
||
1194 | * |
||
1195 | * @return bool true if action succeeded, false if not |
||
1196 | * |
||
1197 | * @todo subfolders of folders in the wastebasket should also be hard-deleted |
||
1198 | */ |
||
1199 | public function deleteFolder($store, $parententryid, $entryid, &$folderProps, $softDelete = false, $hardDelete = false) { |
||
1200 | $result = false; |
||
1201 | $msgprops = mapi_getprops($store, [PR_IPM_WASTEBASKET_ENTRYID]); |
||
1202 | $folder = mapi_msgstore_openentry($store, $parententryid); |
||
1203 | |||
1204 | if ($folder && !$this->isSpecialFolder($store, $entryid)) { |
||
1205 | if ($hardDelete === true) { |
||
1206 | // hard delete the message if requested |
||
1207 | // beware that folder can not be recovered after this and will be deleted from system entirely |
||
1208 | if (mapi_folder_deletefolder($folder, $entryid, DEL_MESSAGES | DEL_FOLDERS | DELETE_HARD_DELETE)) { |
||
1209 | $result = true; |
||
1210 | |||
1211 | // if exists, also delete settings made for this folder (client don't need an update for this) |
||
1212 | $GLOBALS["settings"]->delete("zarafa/v1/state/folders/" . bin2hex($entryid)); |
||
1213 | } |
||
1214 | } |
||
1215 | else { |
||
1216 | if (isset($msgprops[PR_IPM_WASTEBASKET_ENTRYID])) { |
||
1217 | // TODO: check if not only $parententryid=wastebasket, but also the parents of that parent... |
||
1218 | // if folder is already in wastebasket or softDelete is requested then delete the message |
||
1219 | if ($msgprops[PR_IPM_WASTEBASKET_ENTRYID] == $parententryid || $softDelete === true) { |
||
1220 | if (mapi_folder_deletefolder($folder, $entryid, DEL_MESSAGES | DEL_FOLDERS)) { |
||
1221 | $result = true; |
||
1222 | |||
1223 | // if exists, also delete settings made for this folder (client don't need an update for this) |
||
1224 | $GLOBALS["settings"]->delete("zarafa/v1/state/folders/" . bin2hex($entryid)); |
||
1225 | } |
||
1226 | } |
||
1227 | else { |
||
1228 | // move the folder to wastebasket |
||
1229 | $wastebasket = mapi_msgstore_openentry($store, $msgprops[PR_IPM_WASTEBASKET_ENTRYID]); |
||
1230 | |||
1231 | $deleted_folder = mapi_msgstore_openentry($store, $entryid); |
||
1232 | $props = mapi_getprops($deleted_folder, [PR_DISPLAY_NAME]); |
||
1233 | |||
1234 | try { |
||
1235 | /* |
||
1236 | * To decrease overload of checking for conflicting folder names on modification of every folder |
||
1237 | * we should first try to copy folder and if it returns MAPI_E_COLLISION then |
||
1238 | * only we should check for the conflicting folder names and generate a new name |
||
1239 | * and copy folder with the generated name. |
||
1240 | */ |
||
1241 | mapi_folder_copyfolder($folder, $entryid, $wastebasket, $props[PR_DISPLAY_NAME], FOLDER_MOVE); |
||
1242 | $folderProps = mapi_getprops($deleted_folder, [PR_ENTRYID, PR_STORE_ENTRYID]); |
||
1243 | $result = true; |
||
1244 | } |
||
1245 | catch (MAPIException $e) { |
||
1246 | if ($e->getCode() == MAPI_E_COLLISION) { |
||
1247 | $foldername = $this->checkFolderNameConflict($store, $wastebasket, $props[PR_DISPLAY_NAME]); |
||
1248 | |||
1249 | mapi_folder_copyfolder($folder, $entryid, $wastebasket, $foldername, FOLDER_MOVE); |
||
1250 | $folderProps = mapi_getprops($deleted_folder, [PR_ENTRYID, PR_STORE_ENTRYID]); |
||
1251 | $result = true; |
||
1252 | } |
||
1253 | else { |
||
1254 | // all other errors should be propagated to higher level exception handlers |
||
1255 | throw $e; |
||
1256 | } |
||
1257 | } |
||
1258 | } |
||
1259 | } |
||
1260 | else { |
||
1261 | if (mapi_folder_deletefolder($folder, $entryid, DEL_MESSAGES | DEL_FOLDERS)) { |
||
1262 | $result = true; |
||
1263 | |||
1264 | // if exists, also delete settings made for this folder (client don't need an update for this) |
||
1265 | $GLOBALS["settings"]->delete("zarafa/v1/state/folders/" . bin2hex($entryid)); |
||
1266 | } |
||
1267 | } |
||
1268 | } |
||
1269 | } |
||
1270 | |||
1271 | return $result; |
||
1272 | } |
||
1273 | |||
1274 | /** |
||
1275 | * Empty folder. |
||
1276 | * |
||
1277 | * Removes all items from a folder. This is a real delete, not a move. |
||
1278 | * |
||
1279 | * @param object $store MAPI Message Store Object |
||
1280 | * @param string $entryid The entryid of the folder which will be emptied |
||
1281 | * @param array $folderProps reference to an array which will be filled with PR_ENTRYID and PR_STORE_ENTRYID of the emptied folder |
||
1282 | * @param bool $hardDelete flag to indicate if messages will be hard deleted and can not be recoved using restore soft deleted items |
||
1283 | * @param bool $emptySubFolders true to remove all messages with child folders of selected folder else false will |
||
1284 | * remove only message of selected folder |
||
1285 | * |
||
1286 | * @return bool true if action succeeded, false if not |
||
1287 | */ |
||
1288 | public function emptyFolder($store, $entryid, &$folderProps, $hardDelete = false, $emptySubFolders = true) { |
||
1289 | $result = false; |
||
1290 | $folder = mapi_msgstore_openentry($store, $entryid); |
||
1291 | |||
1292 | if ($folder) { |
||
1293 | $flag = DEL_ASSOCIATED; |
||
1294 | |||
1295 | if ($hardDelete) { |
||
1296 | $flag |= DELETE_HARD_DELETE; |
||
1297 | } |
||
1298 | |||
1299 | if ($emptySubFolders) { |
||
1300 | $result = mapi_folder_emptyfolder($folder, $flag); |
||
1301 | } |
||
1302 | else { |
||
1303 | // Delete all items of selected folder without |
||
1304 | // removing child folder and it's content. |
||
1305 | // FIXME: it is effecting performance because mapi_folder_emptyfolder function not provide facility to |
||
1306 | // remove only selected folder items without touching child folder and it's items. |
||
1307 | // for more check KC-1268 |
||
1308 | $table = mapi_folder_getcontentstable($folder, MAPI_DEFERRED_ERRORS); |
||
1309 | $rows = mapi_table_queryallrows($table, [PR_ENTRYID]); |
||
1310 | $messages = []; |
||
1311 | foreach ($rows as $row) { |
||
1312 | array_push($messages, $row[PR_ENTRYID]); |
||
1313 | } |
||
1314 | $result = mapi_folder_deletemessages($folder, $messages, $flag); |
||
1315 | } |
||
1316 | |||
1317 | $folderProps = mapi_getprops($folder, [PR_ENTRYID, PR_STORE_ENTRYID]); |
||
1318 | $result = true; |
||
1319 | } |
||
1320 | |||
1321 | return $result; |
||
1322 | } |
||
1323 | |||
1324 | /** |
||
1325 | * Copy or move a folder. |
||
1326 | * |
||
1327 | * @param object $store MAPI Message Store Object |
||
1328 | * @param string $parentfolderentryid The parent entryid of the folder which will be copied or moved |
||
1329 | * @param string $sourcefolderentryid The entryid of the folder which will be copied or moved |
||
1330 | * @param string $destfolderentryid The entryid of the folder which the folder will be copied or moved to |
||
1331 | * @param bool $moveFolder true - move folder, false - copy folder |
||
1332 | * @param array $folderProps reference to an array which will be filled with entryids |
||
1333 | * @param mixed $deststore |
||
1334 | * |
||
1335 | * @return bool true if action succeeded, false if not |
||
1336 | */ |
||
1337 | public function copyFolder($store, $parentfolderentryid, $sourcefolderentryid, $destfolderentryid, $deststore, $moveFolder, &$folderProps) { |
||
1338 | $result = false; |
||
1339 | $sourceparentfolder = mapi_msgstore_openentry($store, $parentfolderentryid); |
||
1340 | $destfolder = mapi_msgstore_openentry($deststore, $destfolderentryid); |
||
1341 | if (!$this->isSpecialFolder($store, $sourcefolderentryid) && $sourceparentfolder && $destfolder && $deststore) { |
||
1342 | $folder = mapi_msgstore_openentry($store, $sourcefolderentryid); |
||
1343 | $props = mapi_getprops($folder, [PR_DISPLAY_NAME]); |
||
1344 | |||
1345 | try { |
||
1346 | /* |
||
1347 | * To decrease overload of checking for conflicting folder names on modification of every folder |
||
1348 | * we should first try to copy/move folder and if it returns MAPI_E_COLLISION then |
||
1349 | * only we should check for the conflicting folder names and generate a new name |
||
1350 | * and copy/move folder with the generated name. |
||
1351 | */ |
||
1352 | if ($moveFolder) { |
||
1353 | mapi_folder_copyfolder($sourceparentfolder, $sourcefolderentryid, $destfolder, $props[PR_DISPLAY_NAME], FOLDER_MOVE); |
||
1354 | $folderProps = mapi_getprops($folder, [PR_ENTRYID, PR_STORE_ENTRYID]); |
||
1355 | // In some cases PR_PARENT_ENTRYID is not available in mapi_getprops, add it manually |
||
1356 | $folderProps[PR_PARENT_ENTRYID] = $destfolderentryid; |
||
1357 | $result = true; |
||
1358 | } |
||
1359 | else { |
||
1360 | mapi_folder_copyfolder($sourceparentfolder, $sourcefolderentryid, $destfolder, $props[PR_DISPLAY_NAME], COPY_SUBFOLDERS); |
||
1361 | $result = true; |
||
1362 | } |
||
1363 | } |
||
1364 | catch (MAPIException $e) { |
||
1365 | if ($e->getCode() == MAPI_E_COLLISION) { |
||
1366 | $foldername = $this->checkFolderNameConflict($deststore, $destfolder, $props[PR_DISPLAY_NAME]); |
||
1367 | if ($moveFolder) { |
||
1368 | mapi_folder_copyfolder($sourceparentfolder, $sourcefolderentryid, $destfolder, $foldername, FOLDER_MOVE); |
||
1369 | $folderProps = mapi_getprops($folder, [PR_ENTRYID, PR_STORE_ENTRYID]); |
||
1370 | $result = true; |
||
1371 | } |
||
1372 | else { |
||
1373 | mapi_folder_copyfolder($sourceparentfolder, $sourcefolderentryid, $destfolder, $foldername, COPY_SUBFOLDERS); |
||
1374 | $result = true; |
||
1375 | } |
||
1376 | } |
||
1377 | else { |
||
1378 | // all other errors should be propagated to higher level exception handlers |
||
1379 | throw $e; |
||
1380 | } |
||
1381 | } |
||
1382 | } |
||
1383 | |||
1384 | return $result; |
||
1385 | } |
||
1386 | |||
1387 | /** |
||
1388 | * Read MAPI table. |
||
1389 | * |
||
1390 | * This function performs various operations to open, setup, and read all rows from a MAPI table. |
||
1391 | * |
||
1392 | * The output from this function is an XML array structure which can be sent directly to XML serialisation. |
||
1393 | * |
||
1394 | * @param object $store MAPI Message Store Object |
||
1395 | * @param string $entryid The entryid of the folder to read the table from |
||
1396 | * @param array $properties The set of properties which will be read |
||
1397 | * @param array $sort The set properties which the table will be sort on (formatted as a MAPI sort order) |
||
1398 | * @param int $start Starting row at which to start reading rows |
||
1399 | * @param int $rowcount Number of rows which should be read |
||
1400 | * @param array $restriction Table restriction to apply to the table (formatted as MAPI restriction) |
||
1401 | * @param mixed $getHierarchy |
||
1402 | * @param mixed $flags |
||
1403 | * |
||
1404 | * @return array XML array structure with row data |
||
1405 | */ |
||
1406 | public function getTable($store, $entryid, $properties, $sort, $start, $rowcount = false, $restriction = false, $getHierarchy = false, $flags = MAPI_DEFERRED_ERRORS) { |
||
1407 | $data = []; |
||
1408 | $folder = mapi_msgstore_openentry($store, $entryid); |
||
1409 | |||
1410 | if (!$folder) { |
||
1411 | return $data; |
||
1412 | } |
||
1413 | |||
1414 | $table = $getHierarchy ? mapi_folder_gethierarchytable($folder, $flags) : mapi_folder_getcontentstable($folder, $flags); |
||
1415 | |||
1416 | if (!$rowcount) { |
||
1417 | $rowcount = $GLOBALS['settings']->get('zarafa/v1/main/page_size', 50); |
||
1418 | } |
||
1419 | |||
1420 | if (is_array($restriction)) { |
||
1421 | mapi_table_restrict($table, $restriction, TBL_BATCH); |
||
1422 | } |
||
1423 | |||
1424 | if (is_array($sort) && !empty($sort)) { |
||
1425 | /* |
||
1426 | * If the sort array contains the PR_SUBJECT column we should change this to |
||
1427 | * PR_NORMALIZED_SUBJECT to make sure that when sorting on subjects: "sweet" and |
||
1428 | * "RE: sweet", the first one is displayed before the latter one. If the subject |
||
1429 | * is used for sorting the PR_MESSAGE_DELIVERY_TIME must be added as well as |
||
1430 | * Outlook behaves the same way in this case. |
||
1431 | */ |
||
1432 | if (isset($sort[PR_SUBJECT])) { |
||
1433 | $sortReplace = []; |
||
1434 | foreach ($sort as $key => $value) { |
||
1435 | if ($key == PR_SUBJECT) { |
||
1436 | $sortReplace[PR_NORMALIZED_SUBJECT] = $value; |
||
1437 | $sortReplace[PR_MESSAGE_DELIVERY_TIME] = TABLE_SORT_DESCEND; |
||
1438 | } |
||
1439 | else { |
||
1440 | $sortReplace[$key] = $value; |
||
1441 | } |
||
1442 | } |
||
1443 | $sort = $sortReplace; |
||
1444 | } |
||
1445 | |||
1446 | mapi_table_sort($table, $sort, TBL_BATCH); |
||
1447 | } |
||
1448 | |||
1449 | $data["item"] = []; |
||
1450 | |||
1451 | $rows = mapi_table_queryrows($table, $properties, $start, $rowcount); |
||
1452 | $actualCount = count($rows); |
||
1453 | foreach ($rows as $row) { |
||
1454 | $itemData = Conversion::mapMAPI2XML($properties, $row); |
||
1455 | |||
1456 | // For ZARAFA type users the email_address properties are filled with the username |
||
1457 | // Here we will copy that property to the *_username property for consistency with |
||
1458 | // the getMessageProps() function |
||
1459 | // We will not retrieve the real email address (like the getMessageProps function does) |
||
1460 | // for all items because that would be a performance decrease! |
||
1461 | if (isset($itemData['props']["sent_representing_email_address"])) { |
||
1462 | $itemData['props']["sent_representing_username"] = $itemData['props']["sent_representing_email_address"]; |
||
1463 | } |
||
1464 | if (isset($itemData['props']["sender_email_address"])) { |
||
1465 | $itemData['props']["sender_username"] = $itemData['props']["sender_email_address"]; |
||
1466 | } |
||
1467 | if (isset($itemData['props']["received_by_email_address"])) { |
||
1468 | $itemData['props']["received_by_username"] = $itemData['props']["received_by_email_address"]; |
||
1469 | } |
||
1470 | |||
1471 | array_push($data["item"], $itemData); |
||
1472 | } |
||
1473 | |||
1474 | // Update the page information |
||
1475 | $data["page"] = []; |
||
1476 | $data["page"]["start"] = $start; |
||
1477 | $data["page"]["rowcount"] = $rowcount; |
||
1478 | $data["page"]["totalrowcount"] = $start + $actualCount; |
||
1479 | if ($actualCount === $rowcount) { |
||
1480 | $data["page"]["totalrowcount"]++; |
||
1481 | } |
||
1482 | |||
1483 | return $data; |
||
1484 | } |
||
1485 | |||
1486 | /** |
||
1487 | * Returns TRUE of the MAPI message only has inline attachments. |
||
1488 | * |
||
1489 | * @param mapimessage $message The MAPI message object to check |
||
1490 | * |
||
1491 | * @return bool TRUE if the item contains only inline attachments, FALSE otherwise |
||
1492 | * |
||
1493 | * @deprecated This function is not used, because it is much too slow to run on all messages in your inbox |
||
1494 | */ |
||
1495 | public function hasOnlyInlineAttachments($message) { |
||
1496 | $attachmentTable = mapi_message_getattachmenttable($message); |
||
1497 | if ($attachmentTable) { |
||
1498 | $attachments = mapi_table_queryallrows($attachmentTable, [PR_ATTACHMENT_HIDDEN]); |
||
1499 | foreach ($attachments as $attachmentRow) { |
||
1500 | if (!isset($attachmentRow[PR_ATTACHMENT_HIDDEN]) || !$attachmentRow[PR_ATTACHMENT_HIDDEN]) { |
||
1501 | return false; |
||
1502 | } |
||
1503 | } |
||
1504 | } |
||
1505 | |||
1506 | return true; |
||
1507 | } |
||
1508 | |||
1509 | /** |
||
1510 | * Read message properties. |
||
1511 | * |
||
1512 | * Reads a message and returns the data as an XML array structure with all data from the message that is needed |
||
1513 | * to show a message (for example in the preview pane) |
||
1514 | * |
||
1515 | * @param object $store MAPI Message Store Object |
||
1516 | * @param object $message The MAPI Message Object |
||
1517 | * @param array $properties Mapping of properties that should be read |
||
1518 | * @param bool $html2text true - body will be converted from html to text, false - html body will be returned |
||
1519 | * |
||
1520 | * @return array item properties |
||
1521 | * |
||
1522 | * @todo Function name is misleading as it doesn't just get message properties |
||
1523 | */ |
||
1524 | public function getMessageProps($store, $message, $properties, $html2text = false) { |
||
1525 | $props = []; |
||
1526 | |||
1527 | if ($message) { |
||
1528 | $itemprops = mapi_getprops($message, $properties); |
||
1529 | |||
1530 | /* If necessary stream the property, if it's > 8KB */ |
||
1531 | if (isset($itemprops[PR_TRANSPORT_MESSAGE_HEADERS]) || propIsError(PR_TRANSPORT_MESSAGE_HEADERS, $itemprops) == MAPI_E_NOT_ENOUGH_MEMORY) { |
||
1532 | $itemprops[PR_TRANSPORT_MESSAGE_HEADERS] = mapi_openproperty($message, PR_TRANSPORT_MESSAGE_HEADERS); |
||
1533 | } |
||
1534 | |||
1535 | $props = Conversion::mapMAPI2XML($properties, $itemprops); |
||
1536 | |||
1537 | // Get actual SMTP address for sent_representing_email_address and received_by_email_address |
||
1538 | $smtpprops = mapi_getprops($message, [PR_SENT_REPRESENTING_ENTRYID, PR_RECEIVED_BY_ENTRYID, PR_SENDER_ENTRYID]); |
||
1539 | |||
1540 | if (isset($smtpprops[PR_SENT_REPRESENTING_ENTRYID])) { |
||
1541 | try { |
||
1542 | $user = mapi_ab_openentry($GLOBALS['mapisession']->getAddressbook(true), $smtpprops[PR_SENT_REPRESENTING_ENTRYID]); |
||
1543 | if (isset($user)) { |
||
1544 | $user_props = mapi_getprops($user, [PR_EMS_AB_THUMBNAIL_PHOTO]); |
||
1545 | if (isset($user_props[PR_EMS_AB_THUMBNAIL_PHOTO])) { |
||
1546 | $props["props"]['thumbnail_photo'] = "data:image/jpeg;base64," . base64_encode((string) $user_props[PR_EMS_AB_THUMBNAIL_PHOTO]); |
||
1547 | } |
||
1548 | } |
||
1549 | } |
||
1550 | catch (MAPIException) { |
||
1551 | // do nothing |
||
1552 | } |
||
1553 | } |
||
1554 | |||
1555 | /* |
||
1556 | * Check that we have PR_SENT_REPRESENTING_ENTRYID for the item, and also |
||
1557 | * Check that we have sent_representing_email_address property there in the message, |
||
1558 | * but for contacts we are not using sent_representing_* properties so we are not |
||
1559 | * getting it from the message. So basically this will be used for mail items only |
||
1560 | */ |
||
1561 | if (isset($smtpprops[PR_SENT_REPRESENTING_ENTRYID], $props["props"]["sent_representing_email_address"])) { |
||
1562 | $props["props"]["sent_representing_username"] = $props["props"]["sent_representing_email_address"]; |
||
1563 | $sentRepresentingSearchKey = isset($props['props']['sent_representing_search_key']) ? hex2bin($props['props']['sent_representing_search_key']) : false; |
||
1564 | $props["props"]["sent_representing_email_address"] = $this->getEmailAddress($smtpprops[PR_SENT_REPRESENTING_ENTRYID], $sentRepresentingSearchKey); |
||
1565 | } |
||
1566 | |||
1567 | if (isset($smtpprops[PR_SENDER_ENTRYID], $props["props"]["sender_email_address"])) { |
||
1568 | $props["props"]["sender_username"] = $props["props"]["sender_email_address"]; |
||
1569 | $senderSearchKey = isset($props['props']['sender_search_key']) ? hex2bin($props['props']['sender_search_key']) : false; |
||
1570 | $props["props"]["sender_email_address"] = $this->getEmailAddress($smtpprops[PR_SENDER_ENTRYID], $senderSearchKey); |
||
1571 | } |
||
1572 | |||
1573 | if (isset($smtpprops[PR_RECEIVED_BY_ENTRYID], $props["props"]["received_by_email_address"])) { |
||
1574 | $props["props"]["received_by_username"] = $props["props"]["received_by_email_address"]; |
||
1575 | $receivedSearchKey = isset($props['props']['received_by_search_key']) ? hex2bin($props['props']['received_by_search_key']) : false; |
||
1576 | $props["props"]["received_by_email_address"] = $this->getEmailAddress($smtpprops[PR_RECEIVED_BY_ENTRYID], $receivedSearchKey); |
||
1577 | } |
||
1578 | |||
1579 | // Get body content |
||
1580 | // TODO: Move retrieving the body to a separate function. |
||
1581 | $plaintext = $this->isPlainText($message); |
||
1582 | $tmpProps = mapi_getprops($message, [PR_BODY, PR_HTML]); |
||
1583 | |||
1584 | if (empty($tmpProps[PR_HTML])) { |
||
1585 | $tmpProps = mapi_getprops($message, [PR_BODY, PR_RTF_COMPRESSED]); |
||
1586 | if (isset($tmpProps[PR_RTF_COMPRESSED])) { |
||
1587 | $tmpProps[PR_HTML] = mapi_decompressrtf($tmpProps[PR_RTF_COMPRESSED]); |
||
1588 | } |
||
1589 | } |
||
1590 | |||
1591 | $htmlcontent = ''; |
||
1592 | $plaincontent = ''; |
||
1593 | if (!$plaintext && isset($tmpProps[PR_HTML])) { |
||
1594 | $cpprops = mapi_message_getprops($message, [PR_INTERNET_CPID]); |
||
1595 | $codepage = $cpprops[PR_INTERNET_CPID] ?? 65001; |
||
1596 | $htmlcontent = Conversion::convertCodepageStringToUtf8($codepage, $tmpProps[PR_HTML]); |
||
1597 | if (!empty($htmlcontent)) { |
||
1598 | if ($html2text) { |
||
1599 | $htmlcontent = ''; |
||
1600 | } |
||
1601 | else { |
||
1602 | $props["props"]["isHTML"] = true; |
||
1603 | } |
||
1604 | } |
||
1605 | |||
1606 | $htmlcontent = trim($htmlcontent, "\0"); |
||
1607 | } |
||
1608 | |||
1609 | if (isset($tmpProps[PR_BODY])) { |
||
1610 | // only open property if it exists |
||
1611 | $plaincontent = mapi_message_openproperty($message, PR_BODY); |
||
1612 | $plaincontent = trim($plaincontent, "\0"); |
||
1613 | } |
||
1614 | else { |
||
1615 | if ($html2text && isset($tmpProps[PR_HTML])) { |
||
1616 | $plaincontent = strip_tags((string) $tmpProps[PR_HTML]); |
||
1617 | } |
||
1618 | } |
||
1619 | |||
1620 | if (!empty($htmlcontent)) { |
||
1621 | $props["props"]["html_body"] = $htmlcontent; |
||
1622 | $props["props"]["isHTML"] = true; |
||
1623 | } |
||
1624 | else { |
||
1625 | $props["props"]["isHTML"] = false; |
||
1626 | } |
||
1627 | $props["props"]["body"] = $plaincontent; |
||
1628 | |||
1629 | // Get reply-to information, otherwise consider the sender to be the reply-to person. |
||
1630 | $props['reply-to'] = ['item' => []]; |
||
1631 | $messageprops = mapi_getprops($message, [PR_REPLY_RECIPIENT_ENTRIES]); |
||
1632 | if (isset($messageprops[PR_REPLY_RECIPIENT_ENTRIES])) { |
||
1633 | $props['reply-to']['item'] = $this->readReplyRecipientEntry($messageprops[PR_REPLY_RECIPIENT_ENTRIES]); |
||
1634 | } |
||
1635 | if (!isset($messageprops[PR_REPLY_RECIPIENT_ENTRIES]) || count($props['reply-to']['item']) === 0) { |
||
1636 | if (isset($props['props']['sent_representing_email_address']) && !empty($props['props']['sent_representing_email_address'])) { |
||
1637 | $props['reply-to']['item'][] = [ |
||
1638 | 'rowid' => 0, |
||
1639 | 'props' => [ |
||
1640 | 'entryid' => $props['props']['sent_representing_entryid'], |
||
1641 | 'display_name' => $props['props']['sent_representing_name'], |
||
1642 | 'smtp_address' => $props['props']['sent_representing_email_address'], |
||
1643 | 'address_type' => $props['props']['sent_representing_address_type'], |
||
1644 | 'object_type' => MAPI_MAILUSER, |
||
1645 | 'search_key' => $props['props']['sent_representing_search_key'] ?? '', |
||
1646 | ], |
||
1647 | ]; |
||
1648 | } |
||
1649 | elseif (!empty($props['props']['sender_email_address'])) { |
||
1650 | $props['reply-to']['item'][] = [ |
||
1651 | 'rowid' => 0, |
||
1652 | 'props' => [ |
||
1653 | 'entryid' => $props['props']['sender_entryid'], |
||
1654 | 'display_name' => $props['props']['sender_name'], |
||
1655 | 'smtp_address' => $props['props']['sender_email_address'], |
||
1656 | 'address_type' => $props['props']['sender_address_type'], |
||
1657 | 'object_type' => MAPI_MAILUSER, |
||
1658 | 'search_key' => $props['props']['sender_search_key'], |
||
1659 | ], |
||
1660 | ]; |
||
1661 | } |
||
1662 | } |
||
1663 | |||
1664 | // Get recipients |
||
1665 | $recipients = $GLOBALS["operations"]->getRecipientsInfo($message); |
||
1666 | if (!empty($recipients)) { |
||
1667 | $props["recipients"] = [ |
||
1668 | "item" => $recipients, |
||
1669 | ]; |
||
1670 | } |
||
1671 | |||
1672 | // Get attachments |
||
1673 | $attachments = $GLOBALS["operations"]->getAttachmentsInfo($message); |
||
1674 | if (!empty($attachments)) { |
||
1675 | $props["attachments"] = [ |
||
1676 | "item" => $attachments, |
||
1677 | ]; |
||
1678 | $cid_found = false; |
||
1679 | foreach ($attachments as $attachment) { |
||
1680 | if (isset($attachment["props"]["cid"])) { |
||
1681 | $cid_found = true; |
||
1682 | } |
||
1683 | } |
||
1684 | if ($cid_found === true && isset($htmlcontent)) { |
||
1685 | preg_match_all('/src="cid:(.*)"/Uims', $htmlcontent, $matches); |
||
1686 | if (count($matches) > 0) { |
||
1687 | $search = []; |
||
1688 | $replace = []; |
||
1689 | foreach ($matches[1] as $match) { |
||
1690 | $idx = -1; |
||
1691 | foreach ($attachments as $key => $attachment) { |
||
1692 | if (isset($attachment["props"]["cid"]) && |
||
1693 | strcasecmp($match, $attachment["props"]["cid"]) == 0) { |
||
1694 | $idx = $key; |
||
1695 | $num = $attachment["props"]["attach_num"]; |
||
1696 | } |
||
1697 | } |
||
1698 | if ($idx == -1) { |
||
1699 | continue; |
||
1700 | } |
||
1701 | $attach = mapi_message_openattach($message, $num); |
||
1702 | if (empty($attach)) { |
||
1703 | continue; |
||
1704 | } |
||
1705 | $attachprop = mapi_getprops($attach, [PR_ATTACH_DATA_BIN, PR_ATTACH_MIME_TAG]); |
||
1706 | if (empty($attachprop) || !isset($attachprop[PR_ATTACH_DATA_BIN])) { |
||
1707 | continue; |
||
1708 | } |
||
1709 | if (!isset($attachprop[PR_ATTACH_MIME_TAG])) { |
||
1710 | $mime_tag = "text/plain"; |
||
1711 | } |
||
1712 | else { |
||
1713 | $mime_tag = $attachprop[PR_ATTACH_MIME_TAG]; |
||
1714 | } |
||
1715 | $search[] = "src=\"cid:{$match}\""; |
||
1716 | $replace[] = "src=\"data:{$mime_tag};base64," . base64_encode((string) $attachprop[PR_ATTACH_DATA_BIN]) . "\""; |
||
1717 | unset($props["attachments"]["item"][$idx]); |
||
1718 | } |
||
1719 | $props["attachments"]["item"] = array_values($props["attachments"]["item"]); |
||
1720 | $htmlcontent = str_replace($search, $replace, $htmlcontent); |
||
1721 | $props["props"]["html_body"] = $htmlcontent; |
||
1722 | } |
||
1723 | } |
||
1724 | } |
||
1725 | |||
1726 | // for distlists, we need to get members data |
||
1727 | if (isset($props["props"]["oneoff_members"], $props["props"]["members"])) { |
||
1728 | // remove non-client props |
||
1729 | unset($props["props"]["members"], $props["props"]["oneoff_members"]); |
||
1730 | |||
1731 | // get members |
||
1732 | $members = $GLOBALS["operations"]->getMembersFromDistributionList($store, $message, $properties); |
||
1733 | if (!empty($members)) { |
||
1734 | $props["members"] = [ |
||
1735 | "item" => $members, |
||
1736 | ]; |
||
1737 | } |
||
1738 | } |
||
1739 | } |
||
1740 | |||
1741 | return $props; |
||
1742 | } |
||
1743 | |||
1744 | /** |
||
1745 | * Get the email address either from entryid or search key. Function is helpful |
||
1746 | * to retrieve the email address of already deleted contact which is use as a |
||
1747 | * recipient in message. |
||
1748 | * |
||
1749 | * @param string $entryId the entryId of an item/recipient |
||
1750 | * @param bool|string $searchKey then search key of an item/recipient |
||
1751 | * |
||
1752 | * @return string email address if found else return empty string |
||
1753 | */ |
||
1754 | public function getEmailAddress($entryId, $searchKey = false) { |
||
1755 | $emailAddress = $this->getEmailAddressFromEntryID($entryId); |
||
1756 | if (empty($emailAddress) && $searchKey !== false) { |
||
1757 | $emailAddress = $this->getEmailAddressFromSearchKey($searchKey); |
||
1758 | } |
||
1759 | |||
1760 | return $emailAddress; |
||
1761 | } |
||
1762 | |||
1763 | /** |
||
1764 | * Get and convert properties of a message into an XML array structure. |
||
1765 | * |
||
1766 | * @param object $item The MAPI Object |
||
1767 | * @param array $properties Mapping of properties that should be read |
||
1768 | * |
||
1769 | * @return array XML array structure |
||
1770 | * |
||
1771 | * @todo Function name is misleading, especially compared to getMessageProps() |
||
1772 | */ |
||
1773 | public function getProps($item, $properties) { |
||
1774 | $props = []; |
||
1775 | |||
1776 | if ($item) { |
||
1777 | $itemprops = mapi_getprops($item, $properties); |
||
1778 | $props = Conversion::mapMAPI2XML($properties, $itemprops); |
||
1779 | } |
||
1780 | |||
1781 | return $props; |
||
1782 | } |
||
1783 | |||
1784 | /** |
||
1785 | * Get embedded message data. |
||
1786 | * |
||
1787 | * Returns the same data as getMessageProps, but then for a specific sub/sub/sub message |
||
1788 | * of a MAPI message. |
||
1789 | * |
||
1790 | * @param object $store MAPI Message Store Object |
||
1791 | * @param object $message MAPI Message Object |
||
1792 | * @param array $properties a set of properties which will be selected |
||
1793 | * @param array $parentMessage MAPI Message Object of parent |
||
1794 | * @param array $attach_num a list of attachment numbers (aka 2,1 means 'attachment nr 1 of attachment nr 2') |
||
1795 | * |
||
1796 | * @return array item XML array structure of the embedded message |
||
1797 | */ |
||
1798 | public function getEmbeddedMessageProps($store, $message, $properties, $parentMessage, $attach_num) { |
||
1799 | $msgprops = mapi_getprops($message, [PR_MESSAGE_CLASS]); |
||
1800 | |||
1801 | $html2text = match ($msgprops[PR_MESSAGE_CLASS]) { |
||
1802 | 'IPM.Note' => false, |
||
1803 | default => true, |
||
1804 | }; |
||
1805 | |||
1806 | $props = $this->getMessageProps($store, $message, $properties, $html2text); |
||
1807 | |||
1808 | // sub message will not be having entryid, so use parent's entryid |
||
1809 | $parentProps = mapi_getprops($parentMessage, [PR_ENTRYID, PR_PARENT_ENTRYID, PR_STORE_ENTRYID]); |
||
1810 | $props['entryid'] = bin2hex((string) $parentProps[PR_ENTRYID]); |
||
1811 | $props['parent_entryid'] = bin2hex((string) $parentProps[PR_PARENT_ENTRYID]); |
||
1812 | $props['store_entryid'] = bin2hex((string) $parentProps[PR_STORE_ENTRYID]); |
||
1813 | $props['attach_num'] = $attach_num; |
||
1814 | |||
1815 | return $props; |
||
1816 | } |
||
1817 | |||
1818 | /** |
||
1819 | * Create a MAPI message. |
||
1820 | * |
||
1821 | * @param object $store MAPI Message Store Object |
||
1822 | * @param string $parententryid The entryid of the folder in which the new message is to be created |
||
1823 | * |
||
1824 | * @return mapimessage Created MAPI message resource |
||
1825 | */ |
||
1826 | public function createMessage($store, $parententryid) { |
||
1830 | } |
||
1831 | |||
1832 | /** |
||
1833 | * Open a MAPI message. |
||
1834 | * |
||
1835 | * @param object $store MAPI Message Store Object |
||
1836 | * @param string $entryid entryid of the message |
||
1837 | * @param array $attach_num a list of attachment numbers (aka 2,1 means 'attachment nr 1 of attachment nr 2') |
||
1838 | * @param bool $parse_smime (optional) call parse_smime on the opened message or not |
||
1839 | * |
||
1840 | * @return object MAPI Message |
||
1841 | */ |
||
1842 | public function openMessage($store, $entryid, $attach_num = false, $parse_smime = false) { |
||
1873 | } |
||
1874 | |||
1875 | /** |
||
1876 | * Save a MAPI message. |
||
1877 | * |
||
1878 | * The to-be-saved message can be of any type, including e-mail items, appointments, contacts, etc. The message may be pre-existing |
||
1879 | * or it may be a new message. |
||
1880 | * |
||
1881 | * The dialog_attachments parameter represents a unique ID which for the dialog in the client for which this function was called; This |
||
1882 | * is used as follows; Whenever a user uploads an attachment, the attachment is stored in a temporary place on the server. At the same time, |
||
1883 | * the temporary server location of the attachment is saved in the session information, accompanied by the $dialog_attachments unique ID. This |
||
1884 | * way, when we save the message into MAPI, we know which attachment was previously uploaded ready for this message, because when the user saves |
||
1885 | * the message, we pass the same $dialog_attachments ID as when we uploaded the file. |
||
1886 | * |
||
1887 | * @param object $store MAPI Message Store Object |
||
1888 | * @param binary $entryid entryid of the message |
||
1889 | * @param binary $parententryid Parent entryid of the message |
||
1890 | * @param array $props The MAPI properties to be saved |
||
1891 | * @param array $messageProps reference to an array which will be filled with PR_ENTRYID and PR_STORE_ENTRYID of the saved message |
||
1892 | * @param array $recipients XML array structure of recipients for the recipient table |
||
1893 | * @param array $attachments attachments array containing unique check number which checks if attachments should be added |
||
1894 | * @param array $propertiesToDelete Properties specified in this array are deleted from the MAPI message |
||
1895 | * @param MAPIMessage $copyFromMessage resource of the message from which we should |
||
1896 | * copy attachments and/or recipients to the current message |
||
1897 | * @param bool $copyAttachments if set we copy all attachments from the $copyFromMessage |
||
1898 | * @param bool $copyRecipients if set we copy all recipients from the $copyFromMessage |
||
1899 | * @param bool $copyInlineAttachmentsOnly if true then copy only inline attachments |
||
1900 | * @param bool $saveChanges if true then save all change in mapi message |
||
1901 | * @param bool $send true if this function is called from submitMessage else false |
||
1902 | * @param bool $isPlainText if true then message body will be generated using PR_BODY otherwise PR_HTML will be used in saveMessage() function |
||
1903 | * |
||
1904 | * @return mapimessage Saved MAPI message resource |
||
1905 | */ |
||
1906 | public function saveMessage($store, $entryid, $parententryid, $props, &$messageProps, $recipients = [], $attachments = [], $propertiesToDelete = [], $copyFromMessage = false, $copyAttachments = false, $copyRecipients = false, $copyInlineAttachmentsOnly = false, $saveChanges = true, $send = false, $isPlainText = false) { |
||
1907 | $message = false; |
||
1908 | |||
1909 | // Check if an entryid is set, otherwise create a new message |
||
1910 | if ($entryid && !empty($entryid)) { |
||
1911 | $message = $this->openMessage($store, $entryid); |
||
1912 | } |
||
1913 | else { |
||
1914 | $message = $this->createMessage($store, $parententryid); |
||
1915 | } |
||
1916 | |||
1917 | if ($message) { |
||
1918 | $property = false; |
||
1919 | $body = ""; |
||
1920 | |||
1921 | // Check if the body is set. |
||
1922 | if (isset($props[PR_BODY])) { |
||
1923 | $body = $props[PR_BODY]; |
||
1924 | $property = PR_BODY; |
||
1925 | $bodyPropertiesToDelete = [PR_HTML, PR_RTF_COMPRESSED]; |
||
1926 | |||
1927 | if (isset($props[PR_HTML])) { |
||
1928 | $subject = ''; |
||
1929 | if (isset($props[PR_SUBJECT])) { |
||
1930 | $subject = $props[PR_SUBJECT]; |
||
1931 | // If subject is not updated we need to get it from the message |
||
1932 | } |
||
1933 | else { |
||
1934 | $subjectProp = mapi_getprops($message, [PR_SUBJECT]); |
||
1935 | if (isset($subjectProp[PR_SUBJECT])) { |
||
1936 | $subject = $subjectProp[PR_SUBJECT]; |
||
1937 | } |
||
1938 | } |
||
1939 | $body = $this->generateBodyHTML($isPlainText ? $props[PR_BODY] : $props[PR_HTML], $subject); |
||
1940 | $property = PR_HTML; |
||
1941 | $bodyPropertiesToDelete = [PR_BODY, PR_RTF_COMPRESSED]; |
||
1942 | unset($props[PR_HTML]); |
||
1943 | } |
||
1944 | unset($props[PR_BODY]); |
||
1945 | |||
1946 | $propertiesToDelete = array_unique(array_merge($propertiesToDelete, $bodyPropertiesToDelete)); |
||
1947 | } |
||
1948 | |||
1949 | if (!isset($props[PR_SENT_REPRESENTING_ENTRYID]) && |
||
1950 | isset($props[PR_SENT_REPRESENTING_EMAIL_ADDRESS]) && !empty($props[PR_SENT_REPRESENTING_EMAIL_ADDRESS]) && |
||
1951 | isset($props[PR_SENT_REPRESENTING_ADDRTYPE]) && !empty($props[PR_SENT_REPRESENTING_ADDRTYPE]) && |
||
1952 | isset($props[PR_SENT_REPRESENTING_NAME]) && !empty($props[PR_SENT_REPRESENTING_NAME])) { |
||
1953 | // Set FROM field properties |
||
1954 | $props[PR_SENT_REPRESENTING_ENTRYID] = mapi_createoneoff($props[PR_SENT_REPRESENTING_NAME], $props[PR_SENT_REPRESENTING_ADDRTYPE], $props[PR_SENT_REPRESENTING_EMAIL_ADDRESS]); |
||
1955 | } |
||
1956 | |||
1957 | /* |
||
1958 | * Delete PR_SENT_REPRESENTING_ENTRYID and PR_SENT_REPRESENTING_SEARCH_KEY properties, if PR_SENT_REPRESENTING_* properties are configured with empty string. |
||
1959 | * Because, this is the case while user removes recipient from FROM field and send that particular draft without saving it. |
||
1960 | */ |
||
1961 | if (isset($props[PR_SENT_REPRESENTING_EMAIL_ADDRESS]) && empty($props[PR_SENT_REPRESENTING_EMAIL_ADDRESS]) && |
||
1962 | isset($props[PR_SENT_REPRESENTING_ADDRTYPE]) && empty($props[PR_SENT_REPRESENTING_ADDRTYPE]) && |
||
1963 | isset($props[PR_SENT_REPRESENTING_NAME]) && empty($props[PR_SENT_REPRESENTING_NAME])) { |
||
1964 | array_push($propertiesToDelete, PR_SENT_REPRESENTING_ENTRYID, PR_SENT_REPRESENTING_SEARCH_KEY); |
||
1965 | } |
||
1966 | |||
1967 | // remove mv properties when needed |
||
1968 | foreach ($props as $propTag => $propVal) { |
||
1969 | switch (mapi_prop_type($propTag)) { |
||
1970 | case PT_SYSTIME: |
||
1971 | // Empty PT_SYSTIME values mean they should be deleted (there is no way to set an empty PT_SYSTIME) |
||
1972 | // case PT_STRING8: // not enabled at this moment |
||
1973 | // Empty Strings |
||
1974 | case PT_MV_LONG: |
||
1975 | // Empty multivalued long |
||
1976 | if (empty($propVal)) { |
||
1977 | $propertiesToDelete[] = $propTag; |
||
1978 | } |
||
1979 | break; |
||
1980 | |||
1981 | case PT_MV_STRING8: |
||
1982 | // Empty multivalued string |
||
1983 | if (empty($propVal)) { |
||
1984 | $props[$propTag] = []; |
||
1985 | } |
||
1986 | break; |
||
1987 | } |
||
1988 | } |
||
1989 | |||
1990 | foreach ($propertiesToDelete as $prop) { |
||
1991 | unset($props[$prop]); |
||
1992 | } |
||
1993 | |||
1994 | // Set the properties |
||
1995 | mapi_setprops($message, $props); |
||
1996 | |||
1997 | // Delete the properties we don't need anymore |
||
1998 | mapi_deleteprops($message, $propertiesToDelete); |
||
1999 | |||
2000 | if ($property != false) { |
||
2001 | // Stream the body to the PR_BODY or PR_HTML property |
||
2002 | $stream = mapi_openproperty($message, $property, IID_IStream, 0, MAPI_CREATE | MAPI_MODIFY); |
||
2003 | mapi_stream_setsize($stream, strlen((string) $body)); |
||
2004 | mapi_stream_write($stream, $body); |
||
2005 | mapi_stream_commit($stream); |
||
2006 | } |
||
2007 | |||
2008 | /* |
||
2009 | * Save recipients |
||
2010 | * |
||
2011 | * If we are sending mail from delegator's folder, then we need to copy |
||
2012 | * all recipients from original message first - need to pass message |
||
2013 | * |
||
2014 | * if delegate has added or removed any recipients then they will be |
||
2015 | * added/removed using recipients array. |
||
2016 | */ |
||
2017 | if ($copyRecipients !== false && $copyFromMessage !== false) { |
||
2018 | $this->copyRecipients($message, $copyFromMessage); |
||
2019 | } |
||
2020 | |||
2021 | $this->setRecipients($message, $recipients, $send); |
||
2022 | |||
2023 | // Save the attachments with the $dialog_attachments, for attachments we have to obtain |
||
2024 | // some additional information from the state. |
||
2025 | if (!empty($attachments)) { |
||
2026 | $attachment_state = new AttachmentState(); |
||
2027 | $attachment_state->open(); |
||
2028 | |||
2029 | if ($copyFromMessage !== false) { |
||
2030 | $this->copyAttachments($message, $attachments, $copyFromMessage, $copyInlineAttachmentsOnly, $attachment_state); |
||
2031 | } |
||
2032 | |||
2033 | $this->setAttachments($message, $attachments, $attachment_state); |
||
2034 | |||
2035 | $attachment_state->close(); |
||
2036 | } |
||
2037 | |||
2038 | // Set 'hideattachments' if message has only inline attachments. |
||
2039 | $properties = $GLOBALS['properties']->getMailProperties(); |
||
2040 | if ($this->hasOnlyInlineAttachments($message)) { |
||
2041 | mapi_setprops($message, [$properties['hide_attachments'] => true]); |
||
2042 | } |
||
2043 | else { |
||
2044 | mapi_deleteprops($message, [$properties['hide_attachments']]); |
||
2045 | } |
||
2046 | |||
2047 | $this->convertInlineImage($message); |
||
2048 | // Save changes |
||
2049 | if ($saveChanges) { |
||
2050 | mapi_savechanges($message); |
||
2051 | } |
||
2052 | |||
2053 | // Get the PR_ENTRYID, PR_PARENT_ENTRYID and PR_STORE_ENTRYID properties of this message |
||
2054 | $messageProps = mapi_getprops($message, [PR_ENTRYID, PR_PARENT_ENTRYID, PR_STORE_ENTRYID]); |
||
2055 | } |
||
2056 | |||
2057 | return $message; |
||
2058 | } |
||
2059 | |||
2060 | /** |
||
2061 | * Save an appointment item. |
||
2062 | * |
||
2063 | * This is basically the same as saving any other type of message with the added complexity that |
||
2064 | * we support saving exceptions to recurrence here. This means that if the client sends a basedate |
||
2065 | * in the action, that we will attempt to open an existing exception and change that, and if that |
||
2066 | * fails, create a new exception with the specified data. |
||
2067 | * |
||
2068 | * @param mapistore $store MAPI store of the message |
||
2069 | * @param string $entryid entryid of the message |
||
2070 | * @param string $parententryid Parent entryid of the message (folder entryid, NOT message entryid) |
||
2071 | * @param array $action Action array containing XML request |
||
2072 | * @param string $actionType The action type which triggered this action |
||
2073 | * @param bool $directBookingMeetingRequest Indicates if a Meeting Request should use direct booking or not. Defaults to true. |
||
2074 | * |
||
2075 | * @return array of PR_ENTRYID, PR_PARENT_ENTRYID and PR_STORE_ENTRYID properties of modified item |
||
2076 | */ |
||
2077 | public function saveAppointment($store, $entryid, $parententryid, $action, $actionType = 'save', $directBookingMeetingRequest = true) { |
||
2078 | $messageProps = []; |
||
2079 | // It stores the values that is exception allowed or not false -> not allowed |
||
2080 | $isExceptionAllowed = true; |
||
2081 | $delete = $actionType == 'delete'; // Flag for MeetingRequest Class whether to send update or cancel mail. |
||
2082 | $basedate = false; // Flag for MeetingRequest Class whether to send an exception or not. |
||
2083 | $isReminderTimeAllowed = true; // Flag to check reminder minutes is in range of the occurrences |
||
2084 | $properties = $GLOBALS['properties']->getAppointmentProperties(); |
||
2085 | $send = false; |
||
2086 | $oldProps = []; |
||
2087 | $pasteRecord = false; |
||
2088 | |||
2089 | if (isset($action['message_action'], $action['message_action']['send'])) { |
||
2090 | $send = $action['message_action']['send']; |
||
2091 | } |
||
2092 | |||
2093 | if (isset($action['message_action'], $action['message_action']['paste'])) { |
||
2094 | $pasteRecord = true; |
||
2095 | } |
||
2096 | |||
2097 | if (!empty($action['recipients'])) { |
||
2098 | $recips = $action['recipients']; |
||
2099 | } |
||
2100 | else { |
||
2101 | $recips = false; |
||
2102 | } |
||
2103 | |||
2104 | // Set PidLidAppointmentTimeZoneDefinitionStartDisplay and |
||
2105 | // PidLidAppointmentTimeZoneDefinitionEndDisplay so that the allday |
||
2106 | // events are displayed correctly |
||
2107 | if (!empty($action['props']['timezone_iana'])) { |
||
2108 | try { |
||
2109 | $tzdef = mapi_ianatz_to_tzdef($action['props']['timezone_iana']); |
||
2110 | } |
||
2111 | catch (Exception) { |
||
2112 | } |
||
2113 | if ($tzdef !== false) { |
||
2114 | $action['props']['tzdefstart'] = $action['props']['tzdefend'] = bin2hex($tzdef); |
||
2115 | if (isset($action['props']['recurring']) && $action['props']['recurring'] == true) { |
||
2116 | $action['props']['tzdefrecur'] = $action['props']['tzdefstart']; |
||
2117 | } |
||
2118 | } |
||
2119 | } |
||
2120 | |||
2121 | if ($store && $parententryid) { |
||
2122 | // @FIXME: check for $action['props'] array |
||
2123 | if (isset($entryid) && $entryid) { |
||
2124 | // Modify existing or add/change exception |
||
2125 | $message = mapi_msgstore_openentry($store, $entryid); |
||
2126 | |||
2127 | if ($message) { |
||
2128 | $props = mapi_getprops($message, $properties); |
||
2129 | // Do not update timezone information if the appointment times haven't changed |
||
2130 | if (!isset($action['props']['commonstart']) && |
||
2131 | !isset($action['props']['commonend']) && |
||
2132 | !isset($action['props']['startdate']) && |
||
2133 | !isset($action['props']['enddate']) |
||
2134 | ) { |
||
2135 | unset($action['props']['tzdefstart'], $action['props']['tzdefend'], $action['props']['tzdefrecur']); |
||
2136 | } |
||
2137 | // Check if appointment is an exception to a recurring item |
||
2138 | if (isset($action['basedate']) && $action['basedate'] > 0) { |
||
2139 | // Create recurrence object |
||
2140 | $recurrence = new Recurrence($store, $message); |
||
2141 | |||
2142 | $basedate = $action['basedate']; |
||
2143 | $exceptionatt = $recurrence->getExceptionAttachment($basedate); |
||
2144 | if ($exceptionatt) { |
||
2145 | // get properties of existing exception. |
||
2146 | $exceptionattProps = mapi_getprops($exceptionatt, [PR_ATTACH_NUM]); |
||
2147 | $attach_num = $exceptionattProps[PR_ATTACH_NUM]; |
||
2148 | } |
||
2149 | |||
2150 | if ($delete === true) { |
||
2151 | $isExceptionAllowed = $recurrence->createException([], $basedate, true); |
||
2152 | } |
||
2153 | else { |
||
2154 | $exception_recips = []; |
||
2155 | if (isset($recips['add'])) { |
||
2156 | $savedUnsavedRecipients = []; |
||
2157 | foreach ($recips["add"] as $recip) { |
||
2158 | $savedUnsavedRecipients["unsaved"][] = $recip; |
||
2159 | } |
||
2160 | // convert all local distribution list members to ideal recipient. |
||
2161 | $members = $this->convertLocalDistlistMembersToRecipients($savedUnsavedRecipients); |
||
2162 | |||
2163 | $recips['add'] = $members['add']; |
||
2164 | $exception_recips['add'] = $this->createRecipientList($recips['add'], 'add', true, true); |
||
2165 | } |
||
2166 | if (isset($recips['remove'])) { |
||
2167 | $exception_recips['remove'] = $this->createRecipientList($recips['remove'], 'remove'); |
||
2168 | } |
||
2169 | if (isset($recips['modify'])) { |
||
2170 | $exception_recips['modify'] = $this->createRecipientList($recips['modify'], 'modify', true, true); |
||
2171 | } |
||
2172 | |||
2173 | if (isset($action['props']['reminder_minutes'], $action['props']['startdate'])) { |
||
2174 | $isReminderTimeAllowed = $recurrence->isValidReminderTime($basedate, $action['props']['reminder_minutes'], $action['props']['startdate']); |
||
2175 | } |
||
2176 | |||
2177 | // As the reminder minutes occurs before other occurrences don't modify the item. |
||
2178 | if ($isReminderTimeAllowed) { |
||
2179 | if ($recurrence->isException($basedate)) { |
||
2180 | $oldProps = $recurrence->getExceptionProperties($recurrence->getChangeException($basedate)); |
||
2181 | |||
2182 | $isExceptionAllowed = $recurrence->modifyException(Conversion::mapXML2MAPI($properties, $action['props']), $basedate, $exception_recips); |
||
2183 | } |
||
2184 | else { |
||
2185 | $oldProps[$properties['startdate']] = $recurrence->getOccurrenceStart($basedate); |
||
2186 | $oldProps[$properties['duedate']] = $recurrence->getOccurrenceEnd($basedate); |
||
2187 | |||
2188 | $isExceptionAllowed = $recurrence->createException(Conversion::mapXML2MAPI($properties, $action['props']), $basedate, false, $exception_recips); |
||
2189 | } |
||
2190 | mapi_savechanges($message); |
||
2191 | } |
||
2192 | } |
||
2193 | } |
||
2194 | else { |
||
2195 | $oldProps = mapi_getprops($message, [$properties['startdate'], $properties['duedate']]); |
||
2196 | // Modifying non-exception (the series) or normal appointment item |
||
2197 | $message = $GLOBALS['operations']->saveMessage($store, $entryid, $parententryid, Conversion::mapXML2MAPI($properties, $action['props']), $messageProps, $recips ?: [], $action['attachments'] ?? [], [], false, false, false, false, false, false, $send); |
||
2198 | |||
2199 | $recurrenceProps = mapi_getprops($message, [$properties['startdate_recurring'], $properties['enddate_recurring'], $properties["recurring"]]); |
||
2200 | // Check if the meeting is recurring |
||
2201 | if ($recips && $recurrenceProps[$properties["recurring"]] && isset($recurrenceProps[$properties['startdate_recurring']], $recurrenceProps[$properties['enddate_recurring']])) { |
||
2202 | // If recipient of meeting is modified than that modification needs to be applied |
||
2203 | // to recurring exception as well, if any. |
||
2204 | $exception_recips = []; |
||
2205 | if (isset($recips['add'])) { |
||
2206 | $exception_recips['add'] = $this->createRecipientList($recips['add'], 'add', true, true); |
||
2207 | } |
||
2208 | if (isset($recips['remove'])) { |
||
2209 | $exception_recips['remove'] = $this->createRecipientList($recips['remove'], 'remove'); |
||
2210 | } |
||
2211 | if (isset($recips['modify'])) { |
||
2212 | $exception_recips['modify'] = $this->createRecipientList($recips['modify'], 'modify', true, true); |
||
2213 | } |
||
2214 | |||
2215 | // Create recurrence object |
||
2216 | $recurrence = new Recurrence($store, $message); |
||
2217 | |||
2218 | $recurItems = $recurrence->getItems($recurrenceProps[$properties['startdate_recurring']], $recurrenceProps[$properties['enddate_recurring']]); |
||
2219 | |||
2220 | foreach ($recurItems as $recurItem) { |
||
2221 | if (isset($recurItem["exception"])) { |
||
2222 | $recurrence->modifyException([], $recurItem["basedate"], $exception_recips); |
||
2223 | } |
||
2224 | } |
||
2225 | } |
||
2226 | |||
2227 | // Only save recurrence if it has been changed by the user (because otherwise we'll reset |
||
2228 | // the exceptions) |
||
2229 | if (isset($action['props']['recurring_reset']) && $action['props']['recurring_reset'] == true) { |
||
2230 | $recur = new Recurrence($store, $message); |
||
2231 | |||
2232 | if (isset($action['props']['timezone'])) { |
||
2233 | $tzprops = ['timezone', 'timezonedst', 'dststartmonth', 'dststartweek', 'dststartday', 'dststarthour', 'dstendmonth', 'dstendweek', 'dstendday', 'dstendhour']; |
||
2234 | |||
2235 | // Get timezone info |
||
2236 | $tz = []; |
||
2237 | foreach ($tzprops as $tzprop) { |
||
2238 | $tz[$tzprop] = $action['props'][$tzprop]; |
||
2239 | } |
||
2240 | } |
||
2241 | |||
2242 | /** |
||
2243 | * Check if any recurrence property is missing, if yes then prepare |
||
2244 | * the set of properties required to update the recurrence. For more info |
||
2245 | * please refer detailed description of parseRecurrence function of |
||
2246 | * BaseRecurrence class". |
||
2247 | * |
||
2248 | * Note : this is a special case of changing the time of |
||
2249 | * recurrence meeting from scheduling tab. |
||
2250 | */ |
||
2251 | $recurrence = $recur->getRecurrence(); |
||
2252 | if (isset($recurrence)) { |
||
2253 | unset($recurrence['changed_occurrences'], $recurrence['deleted_occurrences']); |
||
2254 | |||
2255 | foreach ($recurrence as $key => $value) { |
||
2256 | if (!isset($action['props'][$key])) { |
||
2257 | $action['props'][$key] = $value; |
||
2258 | } |
||
2259 | } |
||
2260 | } |
||
2261 | // Act like the 'props' are the recurrence pattern; it has more information but that |
||
2262 | // is ignored |
||
2263 | $recur->setRecurrence($tz ?? false, $action['props']); |
||
2264 | } |
||
2265 | } |
||
2266 | |||
2267 | // Get the properties of the main object of which the exception was changed, and post |
||
2268 | // that message as being modified. This will cause the update function to update all |
||
2269 | // occurrences of the item to the client |
||
2270 | $messageProps = mapi_getprops($message, [PR_ENTRYID, PR_PARENT_ENTRYID, PR_STORE_ENTRYID]); |
||
2271 | |||
2272 | // if opened appointment is exception then it will add |
||
2273 | // the attach_num and basedate in messageProps. |
||
2274 | if (isset($attach_num)) { |
||
2275 | $messageProps[PR_ATTACH_NUM] = [$attach_num]; |
||
2276 | $messageProps[$properties["basedate"]] = $action['basedate']; |
||
2277 | } |
||
2278 | } |
||
2279 | } |
||
2280 | else { |
||
2281 | $tz = null; |
||
2282 | $hasRecipient = false; |
||
2283 | $copyAttachments = false; |
||
2284 | $sourceRecord = false; |
||
2285 | if (isset($action['message_action'], $action['message_action']['source_entryid'])) { |
||
2286 | $sourceEntryId = $action['message_action']['source_entryid']; |
||
2287 | $sourceStoreEntryId = $action['message_action']['source_store_entryid']; |
||
2288 | |||
2289 | $sourceStore = $GLOBALS['mapisession']->openMessageStore(hex2bin((string) $sourceStoreEntryId)); |
||
2290 | $sourceRecord = mapi_msgstore_openentry($sourceStore, hex2bin($sourceEntryId)); |
||
2291 | if ($pasteRecord) { |
||
2292 | $sourceRecordProps = mapi_getprops($sourceRecord, [$properties["meeting"], $properties["responsestatus"]]); |
||
2293 | // Don't copy recipient if source record is received message. |
||
2294 | if ($sourceRecordProps[$properties["meeting"]] === olMeeting && |
||
2295 | $sourceRecordProps[$properties["meeting"]] === olResponseOrganized) { |
||
2296 | $table = mapi_message_getrecipienttable($sourceRecord); |
||
2297 | $hasRecipient = mapi_table_getrowcount($table) > 0; |
||
2298 | } |
||
2299 | } |
||
2300 | else { |
||
2301 | $copyAttachments = true; |
||
2302 | // Set sender of new Appointment. |
||
2303 | $this->setSenderAddress($store, $action); |
||
2304 | } |
||
2305 | } |
||
2306 | else { |
||
2307 | // Set sender of new Appointment. |
||
2308 | $this->setSenderAddress($store, $action); |
||
2309 | } |
||
2310 | |||
2311 | $message = $this->saveMessage($store, $entryid, $parententryid, Conversion::mapXML2MAPI($properties, $action['props']), $messageProps, $recips ?: [], $action['attachments'] ?? [], [], $sourceRecord, $copyAttachments, $hasRecipient, false, false, false, $send); |
||
2312 | |||
2313 | if (isset($action['props']['timezone'])) { |
||
2314 | $tzprops = ['timezone', 'timezonedst', 'dststartmonth', 'dststartweek', 'dststartday', 'dststarthour', 'dstendmonth', 'dstendweek', 'dstendday', 'dstendhour']; |
||
2315 | |||
2316 | // Get timezone info |
||
2317 | $tz = []; |
||
2318 | foreach ($tzprops as $tzprop) { |
||
2319 | $tz[$tzprop] = $action['props'][$tzprop]; |
||
2320 | } |
||
2321 | } |
||
2322 | |||
2323 | // Set recurrence |
||
2324 | if (isset($action['props']['recurring']) && $action['props']['recurring'] == true) { |
||
2325 | $recur = new Recurrence($store, $message); |
||
2326 | $recur->setRecurrence($tz, $action['props']); |
||
2327 | } |
||
2328 | } |
||
2329 | } |
||
2330 | |||
2331 | $result = false; |
||
2332 | // Check to see if it should be sent as a meeting request |
||
2333 | if ($send === true && $isExceptionAllowed) { |
||
2334 | $savedUnsavedRecipients = []; |
||
2335 | $remove = []; |
||
2336 | if (!isset($action['basedate'])) { |
||
2337 | // retrieve recipients from saved message |
||
2338 | $savedRecipients = $GLOBALS['operations']->getRecipientsInfo($message); |
||
2339 | foreach ($savedRecipients as $recipient) { |
||
2340 | $savedUnsavedRecipients["saved"][] = $recipient['props']; |
||
2341 | } |
||
2342 | |||
2343 | // retrieve removed recipients. |
||
2344 | if (!empty($recips) && !empty($recips["remove"])) { |
||
2345 | $remove = $recips["remove"]; |
||
2346 | } |
||
2347 | |||
2348 | // convert all local distribution list members to ideal recipient. |
||
2349 | $members = $this->convertLocalDistlistMembersToRecipients($savedUnsavedRecipients, $remove); |
||
2350 | |||
2351 | // Before sending meeting request we set the recipient to message |
||
2352 | // which are converted from local distribution list members. |
||
2353 | $this->setRecipients($message, $members); |
||
2354 | } |
||
2355 | |||
2356 | $request = new Meetingrequest($store, $message, $GLOBALS['mapisession']->getSession(), $directBookingMeetingRequest); |
||
2357 | |||
2358 | /* |
||
2359 | * check write access for delegate, make sure that we will not send meeting request |
||
2360 | * if we don't have permission to save calendar item |
||
2361 | */ |
||
2362 | if ($request->checkFolderWriteAccess($parententryid, $store) !== true) { |
||
2363 | // Throw an exception that we don't have write permissions on calendar folder, |
||
2364 | // error message will be filled by module |
||
2365 | throw new MAPIException(null, MAPI_E_NO_ACCESS); |
||
2366 | } |
||
2367 | |||
2368 | $request->updateMeetingRequest($basedate); |
||
2369 | |||
2370 | $isRecurrenceChanged = isset($action['props']['recurring_reset']) && $action['props']['recurring_reset'] == true; |
||
2371 | $request->checkSignificantChanges($oldProps, $basedate, $isRecurrenceChanged); |
||
2372 | |||
2373 | // Update extra body information |
||
2374 | if (isset($action['message_action']['meetingTimeInfo']) && !empty($action['message_action']['meetingTimeInfo'])) { |
||
2375 | // Append body if the request action requires this |
||
2376 | if (isset($action['message_action'], $action['message_action']['append_body'])) { |
||
2377 | $bodyProps = mapi_getprops($message, [PR_BODY]); |
||
2378 | if (isset($bodyProps[PR_BODY]) || propIsError(PR_BODY, $bodyProps) == MAPI_E_NOT_ENOUGH_MEMORY) { |
||
2379 | $bodyProps[PR_BODY] = streamProperty($message, PR_BODY); |
||
2380 | } |
||
2381 | |||
2382 | if (isset($action['message_action']['meetingTimeInfo'], $bodyProps[PR_BODY])) { |
||
2383 | $action['message_action']['meetingTimeInfo'] .= $bodyProps[PR_BODY]; |
||
2384 | } |
||
2385 | } |
||
2386 | |||
2387 | $request->setMeetingTimeInfo($action['message_action']['meetingTimeInfo']); |
||
2388 | unset($action['message_action']['meetingTimeInfo']); |
||
2389 | } |
||
2390 | |||
2391 | $modifiedRecipients = false; |
||
2392 | $deletedRecipients = false; |
||
2393 | if ($recips) { |
||
2394 | if (isset($action['message_action']['send_update']) && $action['message_action']['send_update'] == 'modified') { |
||
2395 | if (isset($recips['add']) && !empty($recips['add'])) { |
||
2396 | $modifiedRecipients = $modifiedRecipients ?: []; |
||
2397 | $modifiedRecipients = array_merge($modifiedRecipients, $this->createRecipientList($recips['add'], 'add')); |
||
2398 | } |
||
2399 | |||
2400 | if (isset($recips['modify']) && !empty($recips['modify'])) { |
||
2401 | $modifiedRecipients = $modifiedRecipients ?: []; |
||
2402 | $modifiedRecipients = array_merge($modifiedRecipients, $this->createRecipientList($recips['modify'], 'modify')); |
||
2403 | } |
||
2404 | } |
||
2405 | |||
2406 | // lastUpdateCounter is represent that how many times this message is updated(send) |
||
2407 | $lastUpdateCounter = $request->getLastUpdateCounter(); |
||
2408 | if ($lastUpdateCounter !== false && $lastUpdateCounter > 0) { |
||
2409 | if (isset($recips['remove']) && !empty($recips['remove'])) { |
||
2410 | $deletedRecipients = $deletedRecipients ?: []; |
||
2411 | $deletedRecipients = array_merge($deletedRecipients, $this->createRecipientList($recips['remove'], 'remove')); |
||
2412 | if (isset($action['message_action']['send_update']) && $action['message_action']['send_update'] != 'all') { |
||
2413 | $modifiedRecipients = $modifiedRecipients ?: []; |
||
2414 | } |
||
2415 | } |
||
2416 | } |
||
2417 | } |
||
2418 | |||
2419 | $sendMeetingRequestResult = $request->sendMeetingRequest($delete, false, $basedate, $modifiedRecipients, $deletedRecipients); |
||
2420 | |||
2421 | $this->addRecipientsToRecipientHistory($this->getRecipientsInfo($message)); |
||
2422 | |||
2423 | if ($sendMeetingRequestResult === true) { |
||
2424 | $this->parseDistListAndAddToRecipientHistory($savedUnsavedRecipients, $remove); |
||
2425 | |||
2426 | mapi_savechanges($message); |
||
2427 | |||
2428 | // We want to sent the 'request_sent' property, to have it properly |
||
2429 | // deserialized we must also send some type properties. |
||
2430 | $props = mapi_getprops($message, [PR_MESSAGE_CLASS, PR_OBJECT_TYPE]); |
||
2431 | $messageProps[PR_MESSAGE_CLASS] = $props[PR_MESSAGE_CLASS]; |
||
2432 | $messageProps[PR_OBJECT_TYPE] = $props[PR_OBJECT_TYPE]; |
||
2433 | |||
2434 | // Indicate that the message was correctly sent |
||
2435 | $messageProps[$properties['request_sent']] = true; |
||
2436 | |||
2437 | // Return message properties that can be sent to the bus to notify changes |
||
2438 | $result = $messageProps; |
||
2439 | } |
||
2440 | else { |
||
2441 | $sendMeetingRequestResult[PR_ENTRYID] = $messageProps[PR_ENTRYID]; |
||
2442 | $sendMeetingRequestResult[PR_PARENT_ENTRYID] = $messageProps[PR_PARENT_ENTRYID]; |
||
2443 | $sendMeetingRequestResult[PR_STORE_ENTRYID] = $messageProps[PR_STORE_ENTRYID]; |
||
2444 | $result = $sendMeetingRequestResult; |
||
2445 | } |
||
2446 | } |
||
2447 | else { |
||
2448 | mapi_savechanges($message); |
||
2449 | |||
2450 | if (isset($isExceptionAllowed)) { |
||
2451 | if ($isExceptionAllowed === false) { |
||
2452 | $messageProps['isexceptionallowed'] = false; |
||
2453 | } |
||
2454 | } |
||
2455 | |||
2456 | if (isset($isReminderTimeAllowed)) { |
||
2457 | if ($isReminderTimeAllowed === false) { |
||
2458 | $messageProps['remindertimeerror'] = false; |
||
2459 | } |
||
2460 | } |
||
2461 | // Return message properties that can be sent to the bus to notify changes |
||
2462 | $result = $messageProps; |
||
2463 | } |
||
2464 | |||
2465 | return $result; |
||
2466 | } |
||
2467 | |||
2468 | /** |
||
2469 | * Function is used to identify the local distribution list from all recipients and |
||
2470 | * convert all local distribution list members to recipients. |
||
2471 | * |
||
2472 | * @param array $recipients array of recipients either saved or add |
||
2473 | * @param array $remove array of recipients that was removed |
||
2474 | * |
||
2475 | * @return array $newRecipients a list of recipients as XML array structure |
||
2476 | */ |
||
2477 | public function convertLocalDistlistMembersToRecipients($recipients, $remove = []) { |
||
2478 | $addRecipients = []; |
||
2479 | $removeRecipients = []; |
||
2480 | |||
2481 | foreach ($recipients as $key => $recipient) { |
||
2482 | foreach ($recipient as $recipientItem) { |
||
2483 | $recipientEntryid = $recipientItem["entryid"]; |
||
2484 | $isExistInRemove = $this->isExistInRemove($recipientEntryid, $remove); |
||
2485 | |||
2486 | /* |
||
2487 | * Condition is only gets true, if recipient is distribution list and it`s belongs |
||
2488 | * to shared/internal(belongs in contact folder) folder. |
||
2489 | */ |
||
2490 | if ($recipientItem['object_type'] == MAPI_DISTLIST && $recipientItem['address_type'] != 'EX') { |
||
2491 | if (!$isExistInRemove) { |
||
2492 | $recipientItems = $GLOBALS["operations"]->expandDistList($recipientEntryid, true); |
||
2493 | foreach ($recipientItems as $recipient) { |
||
2494 | // set recipient type of each members as per the distribution list recipient type |
||
2495 | $recipient['recipient_type'] = $recipientItem['recipient_type']; |
||
2496 | array_push($addRecipients, $recipient); |
||
2497 | } |
||
2498 | |||
2499 | if ($key === "saved") { |
||
2500 | array_push($removeRecipients, $recipientItem); |
||
2501 | } |
||
2502 | } |
||
2503 | } |
||
2504 | else { |
||
2505 | /* |
||
2506 | * Only Add those recipients which are not saved earlier in message and |
||
2507 | * not present in remove array. |
||
2508 | */ |
||
2509 | if (!$isExistInRemove && $key === "unsaved") { |
||
2510 | array_push($addRecipients, $recipientItem); |
||
2511 | } |
||
2512 | } |
||
2513 | } |
||
2514 | } |
||
2515 | $newRecipients["add"] = $addRecipients; |
||
2516 | $newRecipients["remove"] = $removeRecipients; |
||
2517 | |||
2518 | return $newRecipients; |
||
2519 | } |
||
2520 | |||
2521 | /** |
||
2522 | * Function used to identify given recipient was already available in remove array of recipients array or not. |
||
2523 | * which was sent from client side. If it is found the entry in the $remove array will be deleted, since |
||
2524 | * we do not want to find it again for other recipients. (if a user removes and adds an user again it |
||
2525 | * should be added once!). |
||
2526 | * |
||
2527 | * @param string $recipientEntryid recipient entryid |
||
2528 | * @param array $remove removed recipients array |
||
2529 | * |
||
2530 | * @return bool return false if recipient not exist in remove array else return true |
||
2531 | */ |
||
2532 | public function isExistInRemove($recipientEntryid, &$remove) { |
||
2533 | if (!empty($remove)) { |
||
2534 | foreach ($remove as $index => $removeItem) { |
||
2535 | if (array_search($recipientEntryid, $removeItem, true)) { |
||
2536 | unset($remove[$index]); |
||
2537 | |||
2538 | return true; |
||
2539 | } |
||
2540 | } |
||
2541 | } |
||
2542 | |||
2543 | return false; |
||
2544 | } |
||
2545 | |||
2546 | /** |
||
2547 | * Function is used to identify the local distribution list from all recipients and |
||
2548 | * Add distribution list to recipient history. |
||
2549 | * |
||
2550 | * @param array $savedUnsavedRecipients array of recipients either saved or add |
||
2551 | * @param array $remove array of recipients that was removed |
||
2552 | */ |
||
2553 | public function parseDistListAndAddToRecipientHistory($savedUnsavedRecipients, $remove) { |
||
2554 | $distLists = []; |
||
2555 | foreach ($savedUnsavedRecipients as $key => $recipient) { |
||
2556 | foreach ($recipient as $recipientItem) { |
||
2557 | if ($recipientItem['address_type'] == 'MAPIPDL') { |
||
2558 | $isExistInRemove = $this->isExistInRemove($recipientItem['entryid'], $remove); |
||
2559 | if (!$isExistInRemove) { |
||
2560 | array_push($distLists, ["props" => $recipientItem]); |
||
2561 | } |
||
2562 | } |
||
2563 | } |
||
2564 | } |
||
2565 | |||
2566 | $this->addRecipientsToRecipientHistory($distLists); |
||
2567 | } |
||
2568 | |||
2569 | /** |
||
2570 | * Set sent_representing_email_address property of Appointment. |
||
2571 | * |
||
2572 | * Before saving any new appointment, sent_representing_email_address property of appointment |
||
2573 | * should contain email_address of user, who is the owner of store(in which the appointment |
||
2574 | * is created). |
||
2575 | * |
||
2576 | * @param mapistore $store MAPI store of the message |
||
2577 | * @param array $action reference to action array containing XML request |
||
2578 | */ |
||
2579 | public function setSenderAddress($store, &$action) { |
||
2580 | $storeProps = mapi_getprops($store, [PR_MAILBOX_OWNER_ENTRYID]); |
||
2581 | // check for public store |
||
2582 | if (!isset($storeProps[PR_MAILBOX_OWNER_ENTRYID])) { |
||
2583 | $store = $GLOBALS["mapisession"]->getDefaultMessageStore(); |
||
2584 | $storeProps = mapi_getprops($store, [PR_MAILBOX_OWNER_ENTRYID]); |
||
2585 | } |
||
2586 | $mailuser = mapi_ab_openentry($GLOBALS["mapisession"]->getAddressbook(), $storeProps[PR_MAILBOX_OWNER_ENTRYID]); |
||
2587 | if ($mailuser) { |
||
2588 | $userprops = mapi_getprops($mailuser, [PR_ADDRTYPE, PR_DISPLAY_NAME, PR_EMAIL_ADDRESS, PR_SMTP_ADDRESS]); |
||
2589 | $action["props"]["sent_representing_entryid"] = bin2hex((string) $storeProps[PR_MAILBOX_OWNER_ENTRYID]); |
||
2590 | // we do conversion here, because before passing props to saveMessage() props are converted from utf8-to-w |
||
2591 | $action["props"]["sent_representing_name"] = $userprops[PR_DISPLAY_NAME]; |
||
2592 | $action["props"]["sent_representing_address_type"] = $userprops[PR_ADDRTYPE]; |
||
2593 | if ($userprops[PR_ADDRTYPE] == 'SMTP') { |
||
2594 | $emailAddress = $userprops[PR_SMTP_ADDRESS]; |
||
2595 | } |
||
2596 | else { |
||
2597 | $emailAddress = $userprops[PR_EMAIL_ADDRESS]; |
||
2598 | } |
||
2599 | $action["props"]["sent_representing_email_address"] = $emailAddress; |
||
2600 | $action["props"]["sent_representing_search_key"] = bin2hex(strtoupper($userprops[PR_ADDRTYPE] . ':' . $emailAddress)) . '00'; |
||
2601 | } |
||
2602 | } |
||
2603 | |||
2604 | /** |
||
2605 | * Submit a message for sending. |
||
2606 | * |
||
2607 | * This function is an extension of the saveMessage() function, with the extra functionality |
||
2608 | * that the item is actually sent and queued for moving to 'Sent Items'. Also, the e-mail addresses |
||
2609 | * used in the message are processed for later auto-suggestion. |
||
2610 | * |
||
2611 | * @see Operations::saveMessage() for more information on the parameters, which are identical. |
||
2612 | * |
||
2613 | * @param mapistore $store MAPI Message Store Object |
||
2614 | * @param binary $entryid Entryid of the message |
||
2615 | * @param array $props The properties to be saved |
||
2616 | * @param array $messageProps reference to an array which will be filled with PR_ENTRYID, PR_PARENT_ENTRYID and PR_STORE_ENTRYID |
||
2617 | * @param array $recipients XML array structure of recipients for the recipient table |
||
2618 | * @param array $attachments array of attachments consisting unique ID of attachments for this message |
||
2619 | * @param MAPIMessage $copyFromMessage resource of the message from which we should |
||
2620 | * copy attachments and/or recipients to the current message |
||
2621 | * @param bool $copyAttachments if set we copy all attachments from the $copyFromMessage |
||
2622 | * @param bool $copyRecipients if set we copy all recipients from the $copyFromMessage |
||
2623 | * @param bool $copyInlineAttachmentsOnly if true then copy only inline attachments |
||
2624 | * @param bool $isPlainText if true then message body will be generated using PR_BODY otherwise PR_HTML will be used in saveMessage() function |
||
2625 | * |
||
2626 | * @return bool false if action succeeded, anything else indicates an error (e.g. a string) |
||
2627 | */ |
||
2628 | public function submitMessage($store, $entryid, $props, &$messageProps, $recipients = [], $attachments = [], $copyFromMessage = false, $copyAttachments = false, $copyRecipients = false, $copyInlineAttachmentsOnly = false, $isPlainText = false) { |
||
2629 | $message = false; |
||
2630 | $origStore = $store; |
||
2631 | $reprMessage = false; |
||
2632 | $delegateSentItemsStyle = $GLOBALS['settings']->get('zarafa/v1/contexts/mail/delegate_sent_items_style'); |
||
2633 | $saveBoth = strcasecmp((string) $delegateSentItemsStyle, 'both') == 0; |
||
2634 | $saveRepresentee = strcasecmp((string) $delegateSentItemsStyle, 'representee') == 0; |
||
2635 | $sendingAsDelegate = false; |
||
2636 | |||
2637 | // Get the outbox and sent mail entryid, ignore the given $store, use the default store for submitting messages |
||
2638 | $store = $GLOBALS["mapisession"]->getDefaultMessageStore(); |
||
2639 | $storeprops = mapi_getprops($store, [PR_IPM_OUTBOX_ENTRYID, PR_IPM_SENTMAIL_ENTRYID, PR_ENTRYID]); |
||
2640 | $origStoreprops = mapi_getprops($origStore, [PR_ENTRYID, PR_IPM_SENTMAIL_ENTRYID]); |
||
2641 | |||
2642 | if (!isset($storeprops[PR_IPM_OUTBOX_ENTRYID])) { |
||
2643 | return false; |
||
2644 | } |
||
2645 | if (isset($storeprops[PR_IPM_SENTMAIL_ENTRYID])) { |
||
2646 | $props[PR_SENTMAIL_ENTRYID] = $storeprops[PR_IPM_SENTMAIL_ENTRYID]; |
||
2647 | } |
||
2648 | |||
2649 | // Check if replying then set PR_INTERNET_REFERENCES and PR_IN_REPLY_TO_ID properties in props. |
||
2650 | // flag is probably used wrong here but the same flag indicates if this is reply or replyall |
||
2651 | if ($copyInlineAttachmentsOnly) { |
||
2652 | $origMsgProps = mapi_getprops($copyFromMessage, [PR_INTERNET_MESSAGE_ID, PR_INTERNET_REFERENCES]); |
||
2653 | if (isset($origMsgProps[PR_INTERNET_MESSAGE_ID])) { |
||
2654 | // The references header should indicate the message-id of the original |
||
2655 | // header plus any of the references which were set on the previous mail. |
||
2656 | $props[PR_INTERNET_REFERENCES] = $origMsgProps[PR_INTERNET_MESSAGE_ID]; |
||
2657 | if (isset($origMsgProps[PR_INTERNET_REFERENCES])) { |
||
2658 | $props[PR_INTERNET_REFERENCES] = $origMsgProps[PR_INTERNET_REFERENCES] . ' ' . $props[PR_INTERNET_REFERENCES]; |
||
2659 | } |
||
2660 | $props[PR_IN_REPLY_TO_ID] = $origMsgProps[PR_INTERNET_MESSAGE_ID]; |
||
2661 | } |
||
2662 | } |
||
2663 | |||
2664 | if (!$GLOBALS["entryid"]->compareEntryIds(bin2hex((string) $origStoreprops[PR_ENTRYID]), bin2hex((string) $storeprops[PR_ENTRYID]))) { |
||
2665 | // set properties for "on behalf of" mails |
||
2666 | $origStoreProps = mapi_getprops($origStore, [PR_MAILBOX_OWNER_ENTRYID, PR_MDB_PROVIDER, PR_IPM_SENTMAIL_ENTRYID]); |
||
2667 | |||
2668 | // set PR_SENDER_* properties, which contains currently logged user's data |
||
2669 | $ab = $GLOBALS['mapisession']->getAddressbook(); |
||
2670 | $abitem = mapi_ab_openentry($ab, $GLOBALS["mapisession"]->getUserEntryID()); |
||
2671 | $abitemprops = mapi_getprops($abitem, [PR_DISPLAY_NAME, PR_EMAIL_ADDRESS, PR_SEARCH_KEY]); |
||
2672 | |||
2673 | $props[PR_SENDER_ENTRYID] = $GLOBALS["mapisession"]->getUserEntryID(); |
||
2674 | $props[PR_SENDER_NAME] = $abitemprops[PR_DISPLAY_NAME]; |
||
2675 | $props[PR_SENDER_EMAIL_ADDRESS] = $abitemprops[PR_EMAIL_ADDRESS]; |
||
2676 | $props[PR_SENDER_ADDRTYPE] = "EX"; |
||
2677 | $props[PR_SENDER_SEARCH_KEY] = $abitemprops[PR_SEARCH_KEY]; |
||
2678 | |||
2679 | // Use the PR_SENT_REPRESENTING_* properties sent by the client or set to the currently logged user's data |
||
2680 | $props[PR_SENT_REPRESENTING_ENTRYID] = $props[PR_SENT_REPRESENTING_ENTRYID] ?? $props[PR_SENDER_ENTRYID]; |
||
2681 | $props[PR_SENT_REPRESENTING_NAME] = $props[PR_SENT_REPRESENTING_NAME] ?? $props[PR_SENDER_NAME]; |
||
2682 | $props[PR_SENT_REPRESENTING_EMAIL_ADDRESS] = $props[PR_SENT_REPRESENTING_EMAIL_ADDRESS] ?? $props[PR_SENDER_EMAIL_ADDRESS]; |
||
2683 | $props[PR_SENT_REPRESENTING_ADDRTYPE] = $props[PR_SENT_REPRESENTING_ADDRTYPE] ?? $props[PR_SENDER_ADDRTYPE]; |
||
2684 | $props[PR_SENT_REPRESENTING_SEARCH_KEY] = $props[PR_SENT_REPRESENTING_SEARCH_KEY] ?? $props[PR_SENDER_SEARCH_KEY]; |
||
2685 | /** |
||
2686 | * we are sending mail from delegate's account, so we can't use delegate's outbox and sent items folder |
||
2687 | * so we have to copy the mail from delegate's store to logged user's store and in outbox folder and then |
||
2688 | * we can send mail from logged user's outbox folder. |
||
2689 | * |
||
2690 | * if we set $entryid to false before passing it to saveMessage function then it will assume |
||
2691 | * that item doesn't exist and it will create a new item (in outbox of logged in user) |
||
2692 | */ |
||
2693 | if ($entryid) { |
||
2694 | $oldEntryId = $entryid; |
||
2695 | $entryid = false; |
||
2696 | |||
2697 | // if we are sending mail from drafts folder then we have to copy |
||
2698 | // its recipients and attachments also. $origStore and $oldEntryId points to mail |
||
2699 | // saved in delegators draft folder |
||
2700 | if ($copyFromMessage === false) { |
||
2701 | $copyFromMessage = mapi_msgstore_openentry($origStore, $oldEntryId); |
||
2702 | $copyRecipients = true; |
||
2703 | |||
2704 | // Decode smime signed messages on this message |
||
2705 | parse_smime($origStore, $copyFromMessage); |
||
2706 | } |
||
2707 | } |
||
2708 | |||
2709 | if ($copyFromMessage) { |
||
2710 | // Get properties of original message, to copy recipients and attachments in new message |
||
2711 | $copyMessageProps = mapi_getprops($copyFromMessage); |
||
2712 | $oldParentEntryId = $copyMessageProps[PR_PARENT_ENTRYID]; |
||
2713 | |||
2714 | // unset id properties before merging the props, so we will be creating new item instead of sending same item |
||
2715 | unset($copyMessageProps[PR_ENTRYID], $copyMessageProps[PR_PARENT_ENTRYID], $copyMessageProps[PR_STORE_ENTRYID], $copyMessageProps[PR_SEARCH_KEY]); |
||
2716 | |||
2717 | // grommunio generates PR_HTML on the fly, but it's necessary to unset it |
||
2718 | // if the original message didn't have PR_HTML property. |
||
2719 | if (!isset($props[PR_HTML]) && isset($copyMessageProps[PR_HTML])) { |
||
2720 | unset($copyMessageProps[PR_HTML]); |
||
2721 | } |
||
2722 | /* New EMAIL_ADDRESSes were set (various cases above), kill off old SMTP_ADDRESS. */ |
||
2723 | unset($copyMessageProps[PR_SENDER_SMTP_ADDRESS], $copyMessageProps[PR_SENT_REPRESENTING_SMTP_ADDRESS]); |
||
2724 | |||
2725 | // Merge original message props with props sent by client |
||
2726 | $props = $props + $copyMessageProps; |
||
2727 | } |
||
2728 | |||
2729 | // Save the new message properties |
||
2730 | $message = $this->saveMessage($store, $entryid, $storeprops[PR_IPM_OUTBOX_ENTRYID], $props, $messageProps, $recipients, $attachments, [], $copyFromMessage, $copyAttachments, $copyRecipients, $copyInlineAttachmentsOnly, true, true, $isPlainText); |
||
2731 | |||
2732 | // FIXME: currently message is deleted from original store and new message is created |
||
2733 | // in current user's store, but message should be moved |
||
2734 | |||
2735 | // delete message from it's original location |
||
2736 | if (!empty($oldEntryId) && !empty($oldParentEntryId)) { |
||
2737 | $folder = mapi_msgstore_openentry($origStore, $oldParentEntryId); |
||
2738 | mapi_folder_deletemessages($folder, [$oldEntryId], DELETE_HARD_DELETE); |
||
2739 | } |
||
2740 | if ($saveBoth || $saveRepresentee) { |
||
2741 | if ($origStoreProps[PR_MDB_PROVIDER] === ZARAFA_STORE_PUBLIC_GUID) { |
||
2742 | $userEntryid = $GLOBALS["mapisession"]->getStoreEntryIdOfUser(strtolower($props[PR_SENT_REPRESENTING_EMAIL_ADDRESS])); |
||
2743 | $origStore = $GLOBALS["mapisession"]->openMessageStore($userEntryid); |
||
2744 | $origStoreprops = mapi_getprops($origStore, [PR_IPM_SENTMAIL_ENTRYID]); |
||
2745 | } |
||
2746 | $destfolder = mapi_msgstore_openentry($origStore, $origStoreprops[PR_IPM_SENTMAIL_ENTRYID]); |
||
2747 | $reprMessage = mapi_folder_createmessage($destfolder); |
||
2748 | mapi_copyto($message, [], [], $reprMessage, 0); |
||
2749 | } |
||
2750 | } |
||
2751 | else { |
||
2752 | // When the message is in your own store, just move it to your outbox. We move it manually so we know the new entryid after it has been moved. |
||
2753 | $outbox = mapi_msgstore_openentry($store, $storeprops[PR_IPM_OUTBOX_ENTRYID]); |
||
2754 | |||
2755 | // Open the old and the new message |
||
2756 | $newmessage = mapi_folder_createmessage($outbox); |
||
2757 | $oldEntryId = $entryid; |
||
2758 | |||
2759 | // Remember the new entryid |
||
2760 | $newprops = mapi_getprops($newmessage, [PR_ENTRYID]); |
||
2761 | $entryid = $newprops[PR_ENTRYID]; |
||
2762 | |||
2763 | if (!empty($oldEntryId)) { |
||
2764 | $message = mapi_msgstore_openentry($store, $oldEntryId); |
||
2765 | // Copy the entire message |
||
2766 | mapi_copyto($message, [], [], $newmessage); |
||
2767 | $tmpProps = mapi_getprops($message); |
||
2768 | $oldParentEntryId = $tmpProps[PR_PARENT_ENTRYID]; |
||
2769 | if ($storeprops[PR_IPM_OUTBOX_ENTRYID] == $oldParentEntryId) { |
||
2770 | $folder = $outbox; |
||
2771 | } |
||
2772 | else { |
||
2773 | $folder = mapi_msgstore_openentry($store, $oldParentEntryId); |
||
2774 | } |
||
2775 | |||
2776 | // Copy message_class for S/MIME plugin |
||
2777 | if (isset($tmpProps[PR_MESSAGE_CLASS])) { |
||
2778 | $props[PR_MESSAGE_CLASS] = $tmpProps[PR_MESSAGE_CLASS]; |
||
2779 | } |
||
2780 | // Delete the old message |
||
2781 | mapi_folder_deletemessages($folder, [$oldEntryId]); |
||
2782 | } |
||
2783 | |||
2784 | // save changes to new message created in outbox |
||
2785 | mapi_savechanges($newmessage); |
||
2786 | |||
2787 | $reprProps = mapi_getprops($newmessage, [PR_SENT_REPRESENTING_EMAIL_ADDRESS, PR_SENDER_EMAIL_ADDRESS, PR_SENT_REPRESENTING_ENTRYID]); |
||
2788 | if (isset($reprProps[PR_SENT_REPRESENTING_EMAIL_ADDRESS], $reprProps[PR_SENDER_EMAIL_ADDRESS], $reprProps[PR_SENT_REPRESENTING_ENTRYID]) && |
||
2789 | strcasecmp((string) $reprProps[PR_SENT_REPRESENTING_EMAIL_ADDRESS], (string) $reprProps[PR_SENDER_EMAIL_ADDRESS]) != 0) { |
||
2790 | $ab = $GLOBALS['mapisession']->getAddressbook(); |
||
2791 | $abitem = mapi_ab_openentry($ab, $reprProps[PR_SENT_REPRESENTING_ENTRYID]); |
||
2792 | $abitemprops = mapi_getprops($abitem, [PR_DISPLAY_NAME, PR_EMAIL_ADDRESS, PR_SEARCH_KEY]); |
||
2793 | |||
2794 | $props[PR_SENT_REPRESENTING_NAME] = $abitemprops[PR_DISPLAY_NAME]; |
||
2795 | $props[PR_SENT_REPRESENTING_EMAIL_ADDRESS] = $abitemprops[PR_EMAIL_ADDRESS]; |
||
2796 | $props[PR_SENT_REPRESENTING_ADDRTYPE] = "EX"; |
||
2797 | $props[PR_SENT_REPRESENTING_SEARCH_KEY] = $abitemprops[PR_SEARCH_KEY]; |
||
2798 | $sendingAsDelegate = true; |
||
2799 | } |
||
2800 | // Save the new message properties |
||
2801 | $message = $this->saveMessage($store, $entryid, $storeprops[PR_IPM_OUTBOX_ENTRYID], $props, $messageProps, $recipients, $attachments, [], $copyFromMessage, $copyAttachments, $copyRecipients, $copyInlineAttachmentsOnly, true, true, $isPlainText); |
||
2802 | // Sending as delegate from drafts folder |
||
2803 | if ($sendingAsDelegate && ($saveBoth || $saveRepresentee)) { |
||
2804 | $userEntryid = $GLOBALS["mapisession"]->getStoreEntryIdOfUser(strtolower($props[PR_SENT_REPRESENTING_EMAIL_ADDRESS])); |
||
2805 | $origStore = $GLOBALS["mapisession"]->openMessageStore($userEntryid); |
||
2806 | if ($origStore) { |
||
2807 | $origStoreprops = mapi_getprops($origStore, [PR_ENTRYID, PR_IPM_SENTMAIL_ENTRYID]); |
||
2808 | $destfolder = mapi_msgstore_openentry($origStore, $origStoreprops[PR_IPM_SENTMAIL_ENTRYID]); |
||
2809 | $reprMessage = mapi_folder_createmessage($destfolder); |
||
2810 | mapi_copyto($message, [], [], $reprMessage, 0); |
||
2811 | } |
||
2812 | } |
||
2813 | } |
||
2814 | |||
2815 | if (!$message) { |
||
2816 | return false; |
||
2817 | } |
||
2818 | // Allowing to hook in just before the data sent away to be sent to the client |
||
2819 | $GLOBALS['PluginManager']->triggerHook('server.core.operations.submitmessage', [ |
||
2820 | 'moduleObject' => $this, |
||
2821 | 'store' => $store, |
||
2822 | 'entryid' => $entryid, |
||
2823 | 'message' => &$message, |
||
2824 | ]); |
||
2825 | |||
2826 | // Submit the message (send) |
||
2827 | try { |
||
2828 | mapi_message_submitmessage($message); |
||
2829 | } |
||
2830 | catch (MAPIException $e) { |
||
2831 | $username = $GLOBALS["mapisession"]->getUserName(); |
||
2832 | $errorName = get_mapi_error_name($e->getCode()); |
||
2833 | error_log(sprintf( |
||
2834 | 'Unable to submit message for %s, MAPI error: %s. ' . |
||
2835 | 'SMTP server may be down or it refused the message or the message' . |
||
2836 | ' is too large to submit or user does not have the permission ...', |
||
2837 | $username, |
||
2838 | $errorName |
||
2839 | )); |
||
2840 | |||
2841 | return $errorName; |
||
2842 | } |
||
2843 | |||
2844 | $tmp_props = mapi_getprops($message, [PR_PARENT_ENTRYID, PR_MESSAGE_DELIVERY_TIME, PR_CLIENT_SUBMIT_TIME, PR_SEARCH_KEY, PR_MESSAGE_FLAGS]); |
||
2845 | $messageProps[PR_PARENT_ENTRYID] = $tmp_props[PR_PARENT_ENTRYID]; |
||
2846 | if ($reprMessage !== false) { |
||
2847 | mapi_setprops($reprMessage, [ |
||
2848 | PR_CLIENT_SUBMIT_TIME => $tmp_props[PR_CLIENT_SUBMIT_TIME] ?? time(), |
||
2849 | PR_MESSAGE_DELIVERY_TIME => $tmp_props[PR_MESSAGE_DELIVERY_TIME] ?? time(), |
||
2850 | PR_MESSAGE_FLAGS => $tmp_props[PR_MESSAGE_FLAGS] | MSGFLAG_READ, |
||
2851 | ]); |
||
2852 | mapi_savechanges($reprMessage); |
||
2853 | if ($saveRepresentee) { |
||
2854 | // delete the message in the delegate's Sent Items folder |
||
2855 | $sentFolder = mapi_msgstore_openentry($store, $storeprops[PR_IPM_SENTMAIL_ENTRYID]); |
||
2856 | $sentTable = mapi_folder_getcontentstable($sentFolder, MAPI_DEFERRED_ERRORS); |
||
2857 | $restriction = [RES_PROPERTY, [ |
||
2858 | RELOP => RELOP_EQ, |
||
2859 | ULPROPTAG => PR_SEARCH_KEY, |
||
2860 | VALUE => $tmp_props[PR_SEARCH_KEY], |
||
2861 | ]]; |
||
2862 | mapi_table_restrict($sentTable, $restriction); |
||
2863 | $sentMessageProps = mapi_table_queryallrows($sentTable, [PR_ENTRYID, PR_SEARCH_KEY]); |
||
2864 | if (mapi_table_getrowcount($sentTable) == 1) { |
||
2865 | mapi_folder_deletemessages($sentFolder, [$sentMessageProps[0][PR_ENTRYID]], DELETE_HARD_DELETE); |
||
2866 | } |
||
2867 | else { |
||
2868 | error_log(sprintf( |
||
2869 | "Found multiple entries in Sent Items with the same PR_SEARCH_KEY (%d)." . |
||
2870 | " Impossible to delete email from the delegate's Sent Items folder.", |
||
2871 | mapi_table_getrowcount($sentTable) |
||
2872 | )); |
||
2873 | } |
||
2874 | } |
||
2875 | } |
||
2876 | |||
2877 | $this->addRecipientsToRecipientHistory($this->getRecipientsInfo($message)); |
||
2878 | |||
2879 | return false; |
||
2880 | } |
||
2881 | |||
2882 | /** |
||
2883 | * Delete messages. |
||
2884 | * |
||
2885 | * This function does what is needed when a user presses 'delete' on a MAPI message. This means that: |
||
2886 | * |
||
2887 | * - Items in the own store are moved to the wastebasket |
||
2888 | * - Items in the wastebasket are deleted |
||
2889 | * - Items in other users stores are moved to our own wastebasket |
||
2890 | * - Items in the public store are deleted |
||
2891 | * |
||
2892 | * @param mapistore $store MAPI Message Store Object |
||
2893 | * @param string $parententryid parent entryid of the messages to be deleted |
||
2894 | * @param array $entryids a list of entryids which will be deleted |
||
2895 | * @param bool $softDelete flag for soft-deleteing (when user presses Shift+Del) |
||
2896 | * @param bool $unread message is unread |
||
2897 | * |
||
2898 | * @return bool true if action succeeded, false if not |
||
2899 | */ |
||
2900 | public function deleteMessages($store, $parententryid, $entryids, $softDelete = false, $unread = false) { |
||
2901 | $result = false; |
||
2902 | if (!is_array($entryids)) { |
||
2903 | $entryids = [$entryids]; |
||
2904 | } |
||
2905 | |||
2906 | $folder = mapi_msgstore_openentry($store, $parententryid); |
||
2907 | $flags = $unread ? GX_DELMSG_NOTIFY_UNREAD : 0; |
||
2908 | $msgprops = mapi_getprops($store, [PR_IPM_WASTEBASKET_ENTRYID, PR_MDB_PROVIDER, PR_IPM_OUTBOX_ENTRYID]); |
||
2909 | |||
2910 | switch ($msgprops[PR_MDB_PROVIDER]) { |
||
2911 | case ZARAFA_STORE_DELEGATE_GUID: |
||
2912 | $softDelete = $softDelete || defined('ENABLE_DEFAULT_SOFT_DELETE') ? ENABLE_DEFAULT_SOFT_DELETE : false; |
||
2913 | // with a store from an other user we need our own waste basket... |
||
2914 | if (isset($msgprops[PR_IPM_WASTEBASKET_ENTRYID]) && $msgprops[PR_IPM_WASTEBASKET_ENTRYID] == $parententryid || $softDelete) { |
||
2915 | // except when it is the waste basket itself |
||
2916 | $result = mapi_folder_deletemessages($folder, $entryids, $flags); |
||
2917 | break; |
||
2918 | } |
||
2919 | $defaultstore = $GLOBALS["mapisession"]->getDefaultMessageStore(); |
||
2920 | $msgprops = mapi_getprops($defaultstore, [PR_IPM_WASTEBASKET_ENTRYID, PR_MDB_PROVIDER]); |
||
2921 | |||
2922 | if (!isset($msgprops[PR_IPM_WASTEBASKET_ENTRYID]) || |
||
2923 | $msgprops[PR_IPM_WASTEBASKET_ENTRYID] == $parententryid) { |
||
2924 | $result = mapi_folder_deletemessages($folder, $entryids, $flags); |
||
2925 | break; |
||
2926 | } |
||
2927 | |||
2928 | try { |
||
2929 | $result = $this->copyMessages($store, $parententryid, $defaultstore, $msgprops[PR_IPM_WASTEBASKET_ENTRYID], $entryids, [], true); |
||
2930 | } |
||
2931 | catch (MAPIException $e) { |
||
2932 | $e->setHandled(); |
||
2933 | // if moving fails, try normal delete |
||
2934 | $result = mapi_folder_deletemessages($folder, $entryids, $flags); |
||
2935 | } |
||
2936 | break; |
||
2937 | |||
2938 | case ZARAFA_STORE_ARCHIVER_GUID: |
||
2939 | case ZARAFA_STORE_PUBLIC_GUID: |
||
2940 | // always delete in public store and archive store |
||
2941 | $result = mapi_folder_deletemessages($folder, $entryids, $flags); |
||
2942 | break; |
||
2943 | |||
2944 | case ZARAFA_SERVICE_GUID: |
||
2945 | // delete message when in your own waste basket, else move it to the waste basket |
||
2946 | if (isset($msgprops[PR_IPM_WASTEBASKET_ENTRYID]) && $msgprops[PR_IPM_WASTEBASKET_ENTRYID] == $parententryid || $softDelete === true) { |
||
2947 | $result = mapi_folder_deletemessages($folder, $entryids, $flags); |
||
2948 | break; |
||
2949 | } |
||
2950 | |||
2951 | try { |
||
2952 | // if the message is deleting from outbox then first delete the |
||
2953 | // message from an outgoing queue. |
||
2954 | if (function_exists("mapi_msgstore_abortsubmit") && isset($msgprops[PR_IPM_OUTBOX_ENTRYID]) && $msgprops[PR_IPM_OUTBOX_ENTRYID] === $parententryid) { |
||
2955 | foreach ($entryids as $entryid) { |
||
2956 | $message = mapi_msgstore_openentry($store, $entryid); |
||
2957 | $messageProps = mapi_getprops($message, [PR_DEFERRED_SEND_TIME]); |
||
2958 | if (isset($messageProps[PR_DEFERRED_SEND_TIME])) { |
||
2959 | mapi_msgstore_abortsubmit($store, $entryid); |
||
2960 | } |
||
2961 | } |
||
2962 | } |
||
2963 | $result = $this->copyMessages($store, $parententryid, $store, $msgprops[PR_IPM_WASTEBASKET_ENTRYID], $entryids, [], true); |
||
2964 | } |
||
2965 | catch (MAPIException $e) { |
||
2966 | if ($e->getCode() === MAPI_E_NOT_IN_QUEUE || $e->getCode() === MAPI_E_UNABLE_TO_ABORT) { |
||
2967 | throw $e; |
||
2968 | } |
||
2969 | |||
2970 | $e->setHandled(); |
||
2971 | // if moving fails, try normal delete |
||
2972 | $result = mapi_folder_deletemessages($folder, $entryids, $flags); |
||
2973 | } |
||
2974 | break; |
||
2975 | } |
||
2976 | |||
2977 | return $result; |
||
2978 | } |
||
2979 | |||
2980 | /** |
||
2981 | * Copy or move messages. |
||
2982 | * |
||
2983 | * @param object $store MAPI Message Store Object |
||
2984 | * @param string $parententryid parent entryid of the messages |
||
2985 | * @param string $destentryid destination folder |
||
2986 | * @param array $entryids a list of entryids which will be copied or moved |
||
2987 | * @param array $ignoreProps a list of proptags which should not be copied over |
||
2988 | * to the new message |
||
2989 | * @param bool $moveMessages true - move messages, false - copy messages |
||
2990 | * @param array $props a list of proptags which should set in new messages |
||
2991 | * @param mixed $destStore |
||
2992 | * |
||
2993 | * @return bool true if action succeeded, false if not |
||
2994 | */ |
||
2995 | public function copyMessages($store, $parententryid, $destStore, $destentryid, $entryids, $ignoreProps, $moveMessages, $props = []) { |
||
2996 | $sourcefolder = mapi_msgstore_openentry($store, $parententryid); |
||
2997 | $destfolder = mapi_msgstore_openentry($destStore, $destentryid); |
||
2998 | |||
2999 | if (!$sourcefolder || !$destfolder) { |
||
3000 | error_log("Could not open source or destination folder. Aborting."); |
||
3001 | |||
3002 | return false; |
||
3003 | } |
||
3004 | |||
3005 | if (!is_array($entryids)) { |
||
3006 | $entryids = [$entryids]; |
||
3007 | } |
||
3008 | |||
3009 | /* |
||
3010 | * If there are no properties to ignore as well as set then we can use mapi_folder_copymessages instead |
||
3011 | * of mapi_copyto. mapi_folder_copymessages is much faster then copyto since it executes |
||
3012 | * the copying on the server instead of in client. |
||
3013 | */ |
||
3014 | if (empty($ignoreProps) && empty($props)) { |
||
3015 | try { |
||
3016 | mapi_folder_copymessages($sourcefolder, $entryids, $destfolder, $moveMessages ? MESSAGE_MOVE : 0); |
||
3017 | } |
||
3018 | catch (MAPIException) { |
||
3019 | error_log(sprintf("mapi_folder_copymessages failed with code: 0x%08X. Wait 250ms and try again", mapi_last_hresult())); |
||
3020 | // wait 250ms before trying again |
||
3021 | usleep(250000); |
||
3022 | |||
3023 | try { |
||
3024 | mapi_folder_copymessages($sourcefolder, $entryids, $destfolder, $moveMessages ? MESSAGE_MOVE : 0); |
||
3025 | } |
||
3026 | catch (MAPIException) { |
||
3027 | error_log(sprintf("2nd attempt of mapi_folder_copymessages also failed with code: 0x%08X. Abort.", mapi_last_hresult())); |
||
3028 | |||
3029 | return false; |
||
3030 | } |
||
3031 | } |
||
3032 | } |
||
3033 | else { |
||
3034 | foreach ($entryids as $entryid) { |
||
3035 | $oldmessage = mapi_msgstore_openentry($store, $entryid); |
||
3036 | $newmessage = mapi_folder_createmessage($destfolder); |
||
3037 | |||
3038 | mapi_copyto($oldmessage, [], $ignoreProps, $newmessage, 0); |
||
3039 | if (!empty($props)) { |
||
3040 | mapi_setprops($newmessage, $props); |
||
3041 | } |
||
3042 | mapi_savechanges($newmessage); |
||
3043 | } |
||
3044 | if ($moveMessages) { |
||
3045 | // while moving message we actually copy that particular message into |
||
3046 | // destination folder, and remove it from source folder. so we must have |
||
3047 | // to hard delete the message. |
||
3048 | mapi_folder_deletemessages($sourcefolder, $entryids, DELETE_HARD_DELETE); |
||
3049 | } |
||
3050 | } |
||
3051 | |||
3052 | return true; |
||
3053 | } |
||
3054 | |||
3055 | /** |
||
3056 | * Set message read flag. |
||
3057 | * |
||
3058 | * @param object $store MAPI Message Store Object |
||
3059 | * @param string $entryid entryid of the message |
||
3060 | * @param int $flags Bitmask of values (read, has attachment etc.) |
||
3061 | * @param array $props properties of the message |
||
3062 | * @param mixed $msg_action |
||
3063 | * |
||
3064 | * @return bool true if action succeeded, false if not |
||
3065 | */ |
||
3066 | public function setMessageFlag($store, $entryid, $flags, $msg_action = false, &$props = false) { |
||
3067 | $message = $this->openMessage($store, $entryid); |
||
3068 | |||
3069 | if ($message) { |
||
3070 | /** |
||
3071 | * convert flags of PR_MESSAGE_FLAGS property to flags that is |
||
3072 | * used in mapi_message_setreadflag. |
||
3073 | */ |
||
3074 | $flag = MAPI_DEFERRED_ERRORS; // set unread flag, read receipt will be sent |
||
3075 | |||
3076 | if (($flags & MSGFLAG_RN_PENDING) && isset($msg_action['send_read_receipt']) && $msg_action['send_read_receipt'] == false) { |
||
3077 | $flag |= SUPPRESS_RECEIPT; |
||
3078 | } |
||
3079 | else { |
||
3080 | if (!($flags & MSGFLAG_READ)) { |
||
3081 | $flag |= CLEAR_READ_FLAG; |
||
3082 | } |
||
3083 | } |
||
3084 | |||
3085 | mapi_message_setreadflag($message, $flag); |
||
3086 | |||
3087 | if (is_array($props)) { |
||
3088 | $props = mapi_getprops($message, [PR_ENTRYID, PR_STORE_ENTRYID, PR_PARENT_ENTRYID]); |
||
3089 | } |
||
3090 | } |
||
3091 | |||
3092 | return true; |
||
3093 | } |
||
3094 | |||
3095 | /** |
||
3096 | * Create a unique folder name based on a provided new folder name. |
||
3097 | * |
||
3098 | * checkFolderNameConflict() checks if a folder name conflict is caused by the given $foldername. |
||
3099 | * This function is used for copying of moving a folder to another folder. It returns |
||
3100 | * a unique foldername. |
||
3101 | * |
||
3102 | * @param object $store MAPI Message Store Object |
||
3103 | * @param object $folder MAPI Folder Object |
||
3104 | * @param string $foldername the folder name |
||
3105 | * |
||
3106 | * @return string correct foldername |
||
3107 | */ |
||
3108 | public function checkFolderNameConflict($store, $folder, $foldername) { |
||
3109 | $folderNames = []; |
||
3110 | |||
3111 | $hierarchyTable = mapi_folder_gethierarchytable($folder, MAPI_DEFERRED_ERRORS); |
||
3112 | mapi_table_sort($hierarchyTable, [PR_DISPLAY_NAME => TABLE_SORT_ASCEND], TBL_BATCH); |
||
3113 | |||
3114 | $subfolders = mapi_table_queryallrows($hierarchyTable, [PR_ENTRYID]); |
||
3115 | |||
3116 | if (is_array($subfolders)) { |
||
3117 | foreach ($subfolders as $subfolder) { |
||
3118 | $folderObject = mapi_msgstore_openentry($store, $subfolder[PR_ENTRYID]); |
||
3119 | $folderProps = mapi_getprops($folderObject, [PR_DISPLAY_NAME]); |
||
3120 | |||
3121 | array_push($folderNames, strtolower((string) $folderProps[PR_DISPLAY_NAME])); |
||
3122 | } |
||
3123 | } |
||
3124 | |||
3125 | if (array_search(strtolower($foldername), $folderNames) !== false) { |
||
3126 | $i = 2; |
||
3127 | while (array_search(strtolower($foldername) . " ({$i})", $folderNames) !== false) { |
||
3128 | ++$i; |
||
3129 | } |
||
3130 | $foldername .= " ({$i})"; |
||
3131 | } |
||
3132 | |||
3133 | return $foldername; |
||
3134 | } |
||
3135 | |||
3136 | /** |
||
3137 | * Set the recipients of a MAPI message. |
||
3138 | * |
||
3139 | * @param object $message MAPI Message Object |
||
3140 | * @param array $recipients XML array structure of recipients |
||
3141 | * @param bool $send true if we are going to send this message else false |
||
3142 | */ |
||
3143 | public function setRecipients($message, $recipients, $send = false) { |
||
3144 | if (empty($recipients)) { |
||
3145 | // no recipients are sent from client |
||
3146 | return; |
||
3147 | } |
||
3148 | |||
3149 | $newRecipients = []; |
||
3150 | $removeRecipients = []; |
||
3151 | $modifyRecipients = []; |
||
3152 | |||
3153 | if (isset($recipients['add']) && !empty($recipients['add'])) { |
||
3154 | $newRecipients = $this->createRecipientList($recipients['add'], 'add', false, $send); |
||
3155 | } |
||
3156 | |||
3157 | if (isset($recipients['remove']) && !empty($recipients['remove'])) { |
||
3158 | $removeRecipients = $this->createRecipientList($recipients['remove'], 'remove'); |
||
3159 | } |
||
3160 | |||
3161 | if (isset($recipients['modify']) && !empty($recipients['modify'])) { |
||
3162 | $modifyRecipients = $this->createRecipientList($recipients['modify'], 'modify', false, $send); |
||
3163 | } |
||
3164 | |||
3165 | if (!empty($removeRecipients)) { |
||
3166 | mapi_message_modifyrecipients($message, MODRECIP_REMOVE, $removeRecipients); |
||
3167 | } |
||
3168 | |||
3169 | if (!empty($modifyRecipients)) { |
||
3170 | mapi_message_modifyrecipients($message, MODRECIP_MODIFY, $modifyRecipients); |
||
3171 | } |
||
3172 | |||
3173 | if (!empty($newRecipients)) { |
||
3174 | mapi_message_modifyrecipients($message, MODRECIP_ADD, $newRecipients); |
||
3175 | } |
||
3176 | } |
||
3177 | |||
3178 | /** |
||
3179 | * Copy recipients from original message. |
||
3180 | * |
||
3181 | * If we are sending mail from a delegator's folder, we need to copy all recipients from the original message |
||
3182 | * |
||
3183 | * @param object $message MAPI Message Object |
||
3184 | * @param MAPIMessage $copyFromMessage If set we copy all recipients from this message |
||
3185 | */ |
||
3186 | public function copyRecipients($message, $copyFromMessage = false) { |
||
3187 | $recipienttable = mapi_message_getrecipienttable($copyFromMessage); |
||
3188 | $messageRecipients = mapi_table_queryallrows($recipienttable, $GLOBALS["properties"]->getRecipientProperties()); |
||
3189 | if (!empty($messageRecipients)) { |
||
3190 | mapi_message_modifyrecipients($message, MODRECIP_ADD, $messageRecipients); |
||
3191 | } |
||
3192 | } |
||
3193 | |||
3194 | /** |
||
3195 | * Set attachments in a MAPI message. |
||
3196 | * |
||
3197 | * This function reads any attachments that have been previously uploaded and copies them into |
||
3198 | * the passed MAPI message resource. For a description of the dialog_attachments variable and |
||
3199 | * generally how attachments work when uploading, see Operations::saveMessage() |
||
3200 | * |
||
3201 | * @see Operations::saveMessage() |
||
3202 | * |
||
3203 | * @param object $message MAPI Message Object |
||
3204 | * @param array $attachments XML array structure of attachments |
||
3205 | * @param AttachmentState $attachment_state the state object in which the attachments are saved |
||
3206 | * between different requests |
||
3207 | */ |
||
3208 | public function setAttachments($message, $attachments, $attachment_state) { |
||
3209 | // Check if attachments should be deleted. This is set in the "upload_attachment.php" file |
||
3210 | if (isset($attachments['dialog_attachments'])) { |
||
3211 | $deleted = $attachment_state->getDeletedAttachments($attachments['dialog_attachments']); |
||
3212 | if ($deleted) { |
||
3213 | foreach ($deleted as $attach_num) { |
||
3214 | try { |
||
3215 | mapi_message_deleteattach($message, (int) $attach_num); |
||
3216 | } |
||
3217 | catch (Exception) { |
||
3218 | continue; |
||
3219 | } |
||
3220 | } |
||
3221 | $attachment_state->clearDeletedAttachments($attachments['dialog_attachments']); |
||
3222 | } |
||
3223 | } |
||
3224 | |||
3225 | $addedInlineAttachmentCidMapping = []; |
||
3226 | if (is_array($attachments) && !empty($attachments)) { |
||
3227 | // Set contentId to saved attachments. |
||
3228 | if (isset($attachments['add']) && is_array($attachments['add']) && !empty($attachments['add'])) { |
||
3229 | foreach ($attachments['add'] as $key => $attach) { |
||
3230 | if ($attach && isset($attach['inline']) && $attach['inline']) { |
||
3231 | $addedInlineAttachmentCidMapping[$attach['attach_num']] = $attach['cid']; |
||
3232 | $msgattachment = mapi_message_openattach($message, $attach['attach_num']); |
||
3233 | if ($msgattachment) { |
||
3234 | $props = [PR_ATTACH_CONTENT_ID => $attach['cid'], PR_ATTACHMENT_HIDDEN => true]; |
||
3235 | mapi_setprops($msgattachment, $props); |
||
3236 | mapi_savechanges($msgattachment); |
||
3237 | } |
||
3238 | } |
||
3239 | } |
||
3240 | } |
||
3241 | |||
3242 | // Delete saved inline images if removed from body. |
||
3243 | if (isset($attachments['remove']) && is_array($attachments['remove']) && !empty($attachments['remove'])) { |
||
3244 | foreach ($attachments['remove'] as $key => $attach) { |
||
3245 | if ($attach && isset($attach['inline']) && $attach['inline']) { |
||
3246 | $msgattachment = mapi_message_openattach($message, $attach['attach_num']); |
||
3247 | if ($msgattachment) { |
||
3248 | mapi_message_deleteattach($message, $attach['attach_num']); |
||
3249 | mapi_savechanges($message); |
||
3250 | } |
||
3251 | } |
||
3252 | } |
||
3253 | } |
||
3254 | } |
||
3255 | |||
3256 | if ($attachments['dialog_attachments']) { |
||
3257 | $dialog_attachments = $attachments['dialog_attachments']; |
||
3258 | } |
||
3259 | else { |
||
3260 | return; |
||
3261 | } |
||
3262 | |||
3263 | $files = $attachment_state->getAttachmentFiles($dialog_attachments); |
||
3264 | if ($files) { |
||
3265 | // Loop through the uploaded attachments |
||
3266 | foreach ($files as $tmpname => $fileinfo) { |
||
3267 | if ($fileinfo['sourcetype'] === 'embedded') { |
||
3268 | // open message which needs to be embedded |
||
3269 | $copyFromStore = $GLOBALS['mapisession']->openMessageStore(hex2bin((string) $fileinfo['store_entryid'])); |
||
3270 | $copyFrom = mapi_msgstore_openentry($copyFromStore, hex2bin((string) $fileinfo['entryid'])); |
||
3271 | |||
3272 | $msgProps = mapi_getprops($copyFrom, [PR_SUBJECT]); |
||
3273 | |||
3274 | // get message and copy it to attachment table as embedded attachment |
||
3275 | $props = []; |
||
3276 | $props[PR_EC_WA_ATTACHMENT_ID] = $fileinfo['attach_id']; |
||
3277 | $props[PR_ATTACH_METHOD] = ATTACH_EMBEDDED_MSG; |
||
3278 | $props[PR_DISPLAY_NAME] = !empty($msgProps[PR_SUBJECT]) ? $msgProps[PR_SUBJECT] : _('Untitled'); |
||
3279 | |||
3280 | // Create new attachment. |
||
3281 | $attachment = mapi_message_createattach($message); |
||
3282 | mapi_setprops($attachment, $props); |
||
3283 | |||
3284 | $imessage = mapi_attach_openobj($attachment, MAPI_CREATE | MAPI_MODIFY); |
||
3285 | |||
3286 | // Copy the properties from the source message to the attachment |
||
3287 | mapi_copyto($copyFrom, [], [], $imessage, 0); // includes attachments and recipients |
||
3288 | |||
3289 | // save changes in the embedded message and the final attachment |
||
3290 | mapi_savechanges($imessage); |
||
3291 | mapi_savechanges($attachment); |
||
3292 | } |
||
3293 | elseif ($fileinfo['sourcetype'] === 'icsfile') { |
||
3294 | $messageStore = $GLOBALS['mapisession']->openMessageStore(hex2bin((string) $fileinfo['store_entryid'])); |
||
3295 | $copyFrom = mapi_msgstore_openentry($messageStore, hex2bin((string) $fileinfo['entryid'])); |
||
3296 | |||
3297 | // Get addressbook for current session |
||
3298 | $addrBook = $GLOBALS['mapisession']->getAddressbook(); |
||
3299 | |||
3300 | // get message properties. |
||
3301 | $messageProps = mapi_getprops($copyFrom, [PR_SUBJECT]); |
||
3302 | |||
3303 | // Read the appointment as RFC2445-formatted ics stream. |
||
3304 | $appointmentStream = mapi_mapitoical($GLOBALS['mapisession']->getSession(), $addrBook, $copyFrom, []); |
||
3305 | |||
3306 | $filename = (!empty($messageProps[PR_SUBJECT])) ? $messageProps[PR_SUBJECT] : _('Untitled'); |
||
3307 | $filename .= '.ics'; |
||
3308 | |||
3309 | $props = [ |
||
3310 | PR_ATTACH_LONG_FILENAME => $filename, |
||
3311 | PR_DISPLAY_NAME => $filename, |
||
3312 | PR_ATTACH_METHOD => ATTACH_BY_VALUE, |
||
3313 | PR_ATTACH_DATA_BIN => "", |
||
3314 | PR_ATTACH_MIME_TAG => "application/octet-stream", |
||
3315 | PR_ATTACHMENT_HIDDEN => false, |
||
3316 | PR_EC_WA_ATTACHMENT_ID => isset($fileinfo["attach_id"]) && !empty($fileinfo["attach_id"]) ? $fileinfo["attach_id"] : uniqid(), |
||
3317 | PR_ATTACH_EXTENSION => pathinfo($filename, PATHINFO_EXTENSION), |
||
3318 | ]; |
||
3319 | |||
3320 | $attachment = mapi_message_createattach($message); |
||
3321 | mapi_setprops($attachment, $props); |
||
3322 | |||
3323 | // Stream the file to the PR_ATTACH_DATA_BIN property |
||
3324 | $stream = mapi_openproperty($attachment, PR_ATTACH_DATA_BIN, IID_IStream, 0, MAPI_CREATE | MAPI_MODIFY); |
||
3325 | mapi_stream_write($stream, $appointmentStream); |
||
3326 | |||
3327 | // Commit the stream and save changes |
||
3328 | mapi_stream_commit($stream); |
||
3329 | mapi_savechanges($attachment); |
||
3330 | } |
||
3331 | else { |
||
3332 | $filepath = $attachment_state->getAttachmentPath($tmpname); |
||
3333 | if (is_file($filepath)) { |
||
3334 | // Set contentId if attachment is inline |
||
3335 | $cid = ''; |
||
3336 | if (isset($addedInlineAttachmentCidMapping[$tmpname])) { |
||
3337 | $cid = $addedInlineAttachmentCidMapping[$tmpname]; |
||
3338 | } |
||
3339 | |||
3340 | // If a .p7m file was manually uploaded by the user, we must change the mime type because |
||
3341 | // otherwise mail applications will think the containing email is an encrypted email. |
||
3342 | // That will make Outlook crash, and it will make grommunio Web show the original mail as encrypted |
||
3343 | // without showing the attachment |
||
3344 | $mimeType = $fileinfo["type"]; |
||
3345 | $smimeTags = ['multipart/signed', 'application/pkcs7-mime', 'application/x-pkcs7-mime']; |
||
3346 | if (in_array($mimeType, $smimeTags)) { |
||
3347 | $mimeType = "application/octet-stream"; |
||
3348 | } |
||
3349 | |||
3350 | // Set attachment properties |
||
3351 | $props = [ |
||
3352 | PR_ATTACH_LONG_FILENAME => $fileinfo["name"], |
||
3353 | PR_DISPLAY_NAME => $fileinfo["name"], |
||
3354 | PR_ATTACH_METHOD => ATTACH_BY_VALUE, |
||
3355 | PR_ATTACH_DATA_BIN => "", |
||
3356 | PR_ATTACH_MIME_TAG => $mimeType, |
||
3357 | PR_ATTACHMENT_HIDDEN => !empty($cid) ? true : false, |
||
3358 | PR_EC_WA_ATTACHMENT_ID => isset($fileinfo["attach_id"]) && !empty($fileinfo["attach_id"]) ? $fileinfo["attach_id"] : uniqid(), |
||
3359 | PR_ATTACH_EXTENSION => pathinfo((string) $fileinfo["name"], PATHINFO_EXTENSION), |
||
3360 | ]; |
||
3361 | |||
3362 | if (isset($fileinfo['sourcetype']) && $fileinfo['sourcetype'] === 'contactphoto') { |
||
3363 | $props[PR_ATTACHMENT_HIDDEN] = true; |
||
3364 | $props[PR_ATTACHMENT_CONTACTPHOTO] = true; |
||
3365 | } |
||
3366 | |||
3367 | if (!empty($cid)) { |
||
3368 | $props[PR_ATTACH_CONTENT_ID] = $cid; |
||
3369 | } |
||
3370 | |||
3371 | // Create attachment and set props |
||
3372 | $attachment = mapi_message_createattach($message); |
||
3373 | mapi_setprops($attachment, $props); |
||
3374 | |||
3375 | // Stream the file to the PR_ATTACH_DATA_BIN property |
||
3376 | $stream = mapi_openproperty($attachment, PR_ATTACH_DATA_BIN, IID_IStream, 0, MAPI_CREATE | MAPI_MODIFY); |
||
3377 | $handle = fopen($filepath, "r"); |
||
3378 | while (!feof($handle)) { |
||
3379 | $contents = fread($handle, BLOCK_SIZE); |
||
3380 | mapi_stream_write($stream, $contents); |
||
3381 | } |
||
3382 | |||
3383 | // Commit the stream and save changes |
||
3384 | mapi_stream_commit($stream); |
||
3385 | mapi_savechanges($attachment); |
||
3386 | fclose($handle); |
||
3387 | unlink($filepath); |
||
3388 | } |
||
3389 | } |
||
3390 | } |
||
3391 | |||
3392 | // Delete all the files in the state. |
||
3393 | $attachment_state->clearAttachmentFiles($dialog_attachments); |
||
3394 | } |
||
3395 | } |
||
3396 | |||
3397 | /** |
||
3398 | * Copy attachments from original message. |
||
3399 | * |
||
3400 | * @see Operations::saveMessage() |
||
3401 | * |
||
3402 | * @param object $message MAPI Message Object |
||
3403 | * @param string $attachments |
||
3404 | * @param MAPIMessage $copyFromMessage if set, copy the attachments from this message in addition to the uploaded attachments |
||
3405 | * @param bool $copyInlineAttachmentsOnly if true then copy only inline attachments |
||
3406 | * @param AttachmentState $attachment_state the state object in which the attachments are saved |
||
3407 | * between different requests |
||
3408 | */ |
||
3409 | public function copyAttachments($message, $attachments, $copyFromMessage, $copyInlineAttachmentsOnly, $attachment_state) { |
||
3410 | $attachmentTable = mapi_message_getattachmenttable($copyFromMessage); |
||
3411 | if ($attachmentTable && isset($attachments['dialog_attachments'])) { |
||
3412 | $existingAttachments = mapi_table_queryallrows($attachmentTable, [PR_ATTACH_NUM, PR_ATTACH_SIZE, PR_ATTACH_LONG_FILENAME, PR_ATTACHMENT_HIDDEN, PR_DISPLAY_NAME, PR_ATTACH_METHOD, PR_ATTACH_CONTENT_ID]); |
||
3413 | $deletedAttachments = $attachment_state->getDeletedAttachments($attachments['dialog_attachments']); |
||
3414 | |||
3415 | $plainText = $this->isPlainText($message); |
||
3416 | |||
3417 | $properties = $GLOBALS['properties']->getMailProperties(); |
||
3418 | $blockStatus = mapi_getprops($copyFromMessage, [PR_BLOCK_STATUS]); |
||
3419 | $blockStatus = Conversion::mapMAPI2XML($properties, $blockStatus); |
||
3420 | $isSafeSender = false; |
||
3421 | |||
3422 | // Here if message is HTML and block status is empty then and then call isSafeSender function |
||
3423 | // to check that sender or sender's domain of original message was part of safe sender list. |
||
3424 | if (!$plainText && empty($blockStatus)) { |
||
3425 | $isSafeSender = $this->isSafeSender($copyFromMessage); |
||
3426 | } |
||
3427 | |||
3428 | $body = false; |
||
3429 | foreach ($existingAttachments as $props) { |
||
3430 | // check if this attachment is "deleted" |
||
3431 | |||
3432 | if ($deletedAttachments && in_array($props[PR_ATTACH_NUM], $deletedAttachments)) { |
||
3433 | // skip attachment, remove reference from state as it no longer applies. |
||
3434 | $attachment_state->removeDeletedAttachment($attachments['dialog_attachments'], $props[PR_ATTACH_NUM]); |
||
3435 | |||
3436 | continue; |
||
3437 | } |
||
3438 | |||
3439 | $old = mapi_message_openattach($copyFromMessage, $props[PR_ATTACH_NUM]); |
||
3440 | $isInlineAttachment = $attachment_state->isInlineAttachment($old); |
||
3441 | |||
3442 | /* |
||
3443 | * If reply/reply all message, then copy only inline attachments. |
||
3444 | */ |
||
3445 | if ($copyInlineAttachmentsOnly) { |
||
3446 | /* |
||
3447 | * if message is reply/reply all and format is plain text than ignore inline attachments |
||
3448 | * and normal attachments to copy from original mail. |
||
3449 | */ |
||
3450 | if ($plainText || !$isInlineAttachment) { |
||
3451 | continue; |
||
3452 | } |
||
3453 | } |
||
3454 | elseif ($plainText && $isInlineAttachment) { |
||
3455 | /* |
||
3456 | * If message is forward and format of message is plain text then ignore only inline attachments from the |
||
3457 | * original mail. |
||
3458 | */ |
||
3459 | continue; |
||
3460 | } |
||
3461 | |||
3462 | /* |
||
3463 | * If the inline attachment is referenced with an content-id, |
||
3464 | * manually check if it's still referenced in the body otherwise remove it |
||
3465 | */ |
||
3466 | if ($isInlineAttachment) { |
||
3467 | // Cache body, so we stream it once |
||
3468 | if ($body === false) { |
||
3469 | $body = streamProperty($message, PR_HTML); |
||
3470 | } |
||
3471 | |||
3472 | $contentID = $props[PR_ATTACH_CONTENT_ID]; |
||
3473 | if (!str_contains($body, (string) $contentID)) { |
||
3474 | continue; |
||
3475 | } |
||
3476 | } |
||
3477 | |||
3478 | /* |
||
3479 | * if message is reply/reply all or forward and format of message is HTML but |
||
3480 | * - inline attachments are not downloaded from external source |
||
3481 | * - sender of original message is not safe sender |
||
3482 | * - domain of sender is not part of safe sender list |
||
3483 | * then ignore inline attachments from original message. |
||
3484 | * |
||
3485 | * NOTE : blockStatus is only generated when user has download inline image from external source. |
||
3486 | * it should remains empty if user add the sender in to safe sender list. |
||
3487 | */ |
||
3488 | if (!$plainText && $isInlineAttachment && empty($blockStatus) && !$isSafeSender) { |
||
3489 | continue; |
||
3490 | } |
||
3491 | |||
3492 | $new = mapi_message_createattach($message); |
||
3493 | |||
3494 | try { |
||
3495 | mapi_copyto($old, [], [], $new, 0); |
||
3496 | mapi_savechanges($new); |
||
3497 | } |
||
3498 | catch (MAPIException $e) { |
||
3499 | // This is a workaround for the grommunio-web issue 75 |
||
3500 | // Remove it after gromox issue 253 is resolved |
||
3501 | if ($e->getCode() == ecMsgCycle) { |
||
3502 | $oldstream = mapi_openproperty($old, PR_ATTACH_DATA_BIN, IID_IStream, 0, 0); |
||
3503 | $stat = mapi_stream_stat($oldstream); |
||
3504 | $props = mapi_attach_getprops($old, [PR_ATTACH_LONG_FILENAME, PR_ATTACH_MIME_TAG, PR_DISPLAY_NAME, PR_ATTACH_METHOD, PR_ATTACH_FILENAME, PR_ATTACHMENT_HIDDEN, PR_ATTACH_EXTENSION, PR_ATTACH_FLAGS]); |
||
3505 | |||
3506 | mapi_setprops($new, [ |
||
3507 | PR_ATTACH_LONG_FILENAME => $props[PR_ATTACH_LONG_FILENAME] ?? '', |
||
3508 | PR_ATTACH_MIME_TAG => $props[PR_ATTACH_MIME_TAG] ?? "application/octet-stream", |
||
3509 | PR_DISPLAY_NAME => $props[PR_DISPLAY_NAME] ?? '', |
||
3510 | PR_ATTACH_METHOD => $props[PR_ATTACH_METHOD] ?? ATTACH_BY_VALUE, |
||
3511 | PR_ATTACH_FILENAME => $props[PR_ATTACH_FILENAME] ?? '', |
||
3512 | PR_ATTACH_DATA_BIN => "", |
||
3513 | PR_ATTACHMENT_HIDDEN => $props[PR_ATTACHMENT_HIDDEN] ?? false, |
||
3514 | PR_ATTACH_EXTENSION => $props[PR_ATTACH_EXTENSION] ?? '', |
||
3515 | PR_ATTACH_FLAGS => $props[PR_ATTACH_FLAGS] ?? 0, |
||
3516 | ]); |
||
3517 | $newstream = mapi_openproperty($new, PR_ATTACH_DATA_BIN, IID_IStream, 0, MAPI_CREATE | MAPI_MODIFY); |
||
3518 | mapi_stream_setsize($newstream, $stat['cb']); |
||
3519 | for ($i = 0; $i < $stat['cb']; $i += BLOCK_SIZE) { |
||
3520 | mapi_stream_write($newstream, mapi_stream_read($oldstream, BLOCK_SIZE)); |
||
3521 | } |
||
3522 | mapi_stream_commit($newstream); |
||
3523 | mapi_savechanges($new); |
||
3524 | } |
||
3525 | } |
||
3526 | } |
||
3527 | } |
||
3528 | } |
||
3529 | |||
3530 | /** |
||
3531 | * Function was used to identify the sender or domain of original mail in safe sender list. |
||
3532 | * |
||
3533 | * @param MAPIMessage $copyFromMessage resource of the message from which we should get |
||
3534 | * the sender of message |
||
3535 | * |
||
3536 | * @return bool true if sender of original mail was safe sender else false |
||
3537 | */ |
||
3538 | public function isSafeSender($copyFromMessage) { |
||
3539 | $safeSenderList = $GLOBALS['settings']->get('zarafa/v1/contexts/mail/safe_senders_list'); |
||
3540 | $senderEntryid = mapi_getprops($copyFromMessage, [PR_SENT_REPRESENTING_ENTRYID]); |
||
3541 | $senderEntryid = $senderEntryid[PR_SENT_REPRESENTING_ENTRYID]; |
||
3542 | |||
3543 | // If sender is user himself (which happens in case of "Send as New message") consider sender as safe |
||
3544 | if ($GLOBALS['entryid']->compareEntryIds($senderEntryid, $GLOBALS["mapisession"]->getUserEntryID())) { |
||
3545 | return true; |
||
3546 | } |
||
3547 | |||
3548 | try { |
||
3549 | $mailuser = mapi_ab_openentry($GLOBALS["mapisession"]->getAddressbook(), $senderEntryid); |
||
3550 | } |
||
3551 | catch (MAPIException) { |
||
3552 | // The user might have a new uidNumber, which makes the user not resolve, see WA-7673 |
||
3553 | // FIXME: Lookup the user by PR_SENDER_NAME or another attribute if PR_SENDER_ADDRTYPE is "EX" |
||
3554 | return false; |
||
3555 | } |
||
3556 | |||
3557 | $addressType = mapi_getprops($mailuser, [PR_ADDRTYPE]); |
||
3558 | |||
3559 | // Here it will check that sender of original mail was address book user. |
||
3560 | // If PR_ADDRTYPE is ZARAFA, it means sender of original mail was address book contact. |
||
3561 | if ($addressType[PR_ADDRTYPE] === 'EX') { |
||
3562 | $address = mapi_getprops($mailuser, [PR_SMTP_ADDRESS]); |
||
3563 | $address = $address[PR_SMTP_ADDRESS]; |
||
3564 | } |
||
3565 | elseif ($addressType[PR_ADDRTYPE] === 'SMTP') { |
||
3566 | // If PR_ADDRTYPE is SMTP, it means sender of original mail was external sender. |
||
3567 | $address = mapi_getprops($mailuser, [PR_EMAIL_ADDRESS]); |
||
3568 | $address = $address[PR_EMAIL_ADDRESS]; |
||
3569 | } |
||
3570 | |||
3571 | // Obtain the Domain address from smtp/email address. |
||
3572 | $domain = substr((string) $address, strpos((string) $address, "@") + 1); |
||
3573 | |||
3574 | if (!empty($safeSenderList)) { |
||
3575 | foreach ($safeSenderList as $safeSender) { |
||
3576 | if ($safeSender === $address || $safeSender === $domain) { |
||
3577 | return true; |
||
3578 | } |
||
3579 | } |
||
3580 | } |
||
3581 | |||
3582 | return false; |
||
3583 | } |
||
3584 | |||
3585 | /** |
||
3586 | * get attachments information of a particular message. |
||
3587 | * |
||
3588 | * @param MapiMessage $message MAPI Message Object |
||
3589 | * @param bool $excludeHidden exclude hidden attachments |
||
3590 | */ |
||
3591 | public function getAttachmentsInfo($message, $excludeHidden = false) { |
||
3592 | $attachmentsInfo = []; |
||
3593 | |||
3594 | $hasattachProp = mapi_getprops($message, [PR_HASATTACH]); |
||
3595 | if (isset($hasattachProp[PR_HASATTACH]) && $hasattachProp[PR_HASATTACH]) { |
||
3596 | $attachmentTable = mapi_message_getattachmenttable($message); |
||
3597 | |||
3598 | $attachments = mapi_table_queryallrows($attachmentTable, [PR_ATTACH_NUM, PR_ATTACH_SIZE, PR_ATTACH_LONG_FILENAME, |
||
3599 | PR_ATTACH_FILENAME, PR_ATTACHMENT_HIDDEN, PR_DISPLAY_NAME, PR_ATTACH_METHOD, |
||
3600 | PR_ATTACH_CONTENT_ID, PR_ATTACH_MIME_TAG, |
||
3601 | PR_ATTACHMENT_CONTACTPHOTO, PR_RECORD_KEY, PR_EC_WA_ATTACHMENT_ID, PR_OBJECT_TYPE, PR_ATTACH_EXTENSION, ]); |
||
3602 | foreach ($attachments as $attachmentRow) { |
||
3603 | $props = []; |
||
3604 | |||
3605 | if (isset($attachmentRow[PR_ATTACH_MIME_TAG])) { |
||
3606 | if ($attachmentRow[PR_ATTACH_MIME_TAG]) { |
||
3607 | $props["filetype"] = $attachmentRow[PR_ATTACH_MIME_TAG]; |
||
3608 | } |
||
3609 | |||
3610 | $smimeTags = ['multipart/signed', 'application/pkcs7-mime', 'application/x-pkcs7-mime']; |
||
3611 | if (in_array($attachmentRow[PR_ATTACH_MIME_TAG], $smimeTags)) { |
||
3612 | // Ignore the message with attachment types set as smime as they are for smime |
||
3613 | continue; |
||
3614 | } |
||
3615 | } |
||
3616 | |||
3617 | $attach_id = ''; |
||
3618 | if (isset($attachmentRow[PR_EC_WA_ATTACHMENT_ID])) { |
||
3619 | $attach_id = $attachmentRow[PR_EC_WA_ATTACHMENT_ID]; |
||
3620 | } |
||
3621 | elseif (isset($attachmentRow[PR_RECORD_KEY])) { |
||
3622 | $attach_id = bin2hex((string) $attachmentRow[PR_RECORD_KEY]); |
||
3623 | } |
||
3624 | else { |
||
3625 | $attach_id = uniqid(); |
||
3626 | } |
||
3627 | |||
3628 | $props["object_type"] = $attachmentRow[PR_OBJECT_TYPE]; |
||
3629 | $props["attach_id"] = $attach_id; |
||
3630 | $props["attach_num"] = $attachmentRow[PR_ATTACH_NUM]; |
||
3631 | $props["attach_method"] = $attachmentRow[PR_ATTACH_METHOD]; |
||
3632 | $props["size"] = $attachmentRow[PR_ATTACH_SIZE]; |
||
3633 | |||
3634 | if (isset($attachmentRow[PR_ATTACH_CONTENT_ID]) && $attachmentRow[PR_ATTACH_CONTENT_ID]) { |
||
3635 | $props["cid"] = $attachmentRow[PR_ATTACH_CONTENT_ID]; |
||
3636 | } |
||
3637 | |||
3638 | $props["hidden"] = $attachmentRow[PR_ATTACHMENT_HIDDEN] ?? false; |
||
3639 | if ($excludeHidden && $props["hidden"]) { |
||
3640 | continue; |
||
3641 | } |
||
3642 | |||
3643 | if (isset($attachmentRow[PR_ATTACH_LONG_FILENAME])) { |
||
3644 | $props["name"] = $attachmentRow[PR_ATTACH_LONG_FILENAME]; |
||
3645 | } |
||
3646 | elseif (isset($attachmentRow[PR_ATTACH_FILENAME])) { |
||
3647 | $props["name"] = $attachmentRow[PR_ATTACH_FILENAME]; |
||
3648 | } |
||
3649 | elseif (isset($attachmentRow[PR_DISPLAY_NAME])) { |
||
3650 | $props["name"] = $attachmentRow[PR_DISPLAY_NAME]; |
||
3651 | } |
||
3652 | else { |
||
3653 | $props["name"] = "untitled"; |
||
3654 | } |
||
3655 | |||
3656 | if (isset($attachmentRow[PR_ATTACH_EXTENSION]) && $attachmentRow[PR_ATTACH_EXTENSION]) { |
||
3657 | $props["extension"] = $attachmentRow[PR_ATTACH_EXTENSION]; |
||
3658 | } |
||
3659 | else { |
||
3660 | // For backward compatibility where attachments doesn't have the extension property |
||
3661 | $props["extension"] = pathinfo((string) $props["name"], PATHINFO_EXTENSION); |
||
3662 | } |
||
3663 | |||
3664 | if (isset($attachmentRow[PR_ATTACHMENT_CONTACTPHOTO]) && $attachmentRow[PR_ATTACHMENT_CONTACTPHOTO]) { |
||
3665 | $props["attachment_contactphoto"] = $attachmentRow[PR_ATTACHMENT_CONTACTPHOTO]; |
||
3666 | $props["hidden"] = true; |
||
3667 | |||
3668 | // Open contact photo attachment in binary format. |
||
3669 | $attach = mapi_message_openattach($message, $props["attach_num"]); |
||
3670 | } |
||
3671 | |||
3672 | if ($props["attach_method"] == ATTACH_EMBEDDED_MSG) { |
||
3673 | // open attachment to get the message class |
||
3674 | $attach = mapi_message_openattach($message, $props["attach_num"]); |
||
3675 | $embMessage = mapi_attach_openobj($attach); |
||
3676 | $embProps = mapi_getprops($embMessage, [PR_MESSAGE_CLASS]); |
||
3677 | if (isset($embProps[PR_MESSAGE_CLASS])) { |
||
3678 | $props["attach_message_class"] = $embProps[PR_MESSAGE_CLASS]; |
||
3679 | } |
||
3680 | } |
||
3681 | |||
3682 | array_push($attachmentsInfo, ["props" => $props]); |
||
3683 | } |
||
3684 | } |
||
3685 | |||
3686 | return $attachmentsInfo; |
||
3687 | } |
||
3688 | |||
3689 | /** |
||
3690 | * get recipients information of a particular message. |
||
3691 | * |
||
3692 | * @param MapiMessage $message MAPI Message Object |
||
3693 | * @param bool $excludeDeleted exclude deleted recipients |
||
3694 | */ |
||
3695 | public function getRecipientsInfo($message, $excludeDeleted = true) { |
||
3696 | $recipientsInfo = []; |
||
3697 | |||
3698 | $recipientTable = mapi_message_getrecipienttable($message); |
||
3699 | if ($recipientTable) { |
||
3700 | $recipients = mapi_table_queryallrows($recipientTable, $GLOBALS['properties']->getRecipientProperties()); |
||
3701 | |||
3702 | foreach ($recipients as $recipientRow) { |
||
3703 | if ($excludeDeleted && isset($recipientRow[PR_RECIPIENT_FLAGS]) && (($recipientRow[PR_RECIPIENT_FLAGS] & recipExceptionalDeleted) == recipExceptionalDeleted)) { |
||
3704 | continue; |
||
3705 | } |
||
3706 | |||
3707 | $props = []; |
||
3708 | $props['rowid'] = $recipientRow[PR_ROWID]; |
||
3709 | $props['search_key'] = isset($recipientRow[PR_SEARCH_KEY]) ? bin2hex((string) $recipientRow[PR_SEARCH_KEY]) : ''; |
||
3710 | $props['display_name'] = $recipientRow[PR_DISPLAY_NAME] ?? ''; |
||
3711 | $props['email_address'] = $recipientRow[PR_EMAIL_ADDRESS] ?? ''; |
||
3712 | $props['smtp_address'] = $recipientRow[PR_SMTP_ADDRESS] ?? ''; |
||
3713 | $props['address_type'] = $recipientRow[PR_ADDRTYPE] ?? ''; |
||
3714 | $props['object_type'] = $recipientRow[PR_OBJECT_TYPE] ?? MAPI_MAILUSER; |
||
3715 | $props['recipient_type'] = $recipientRow[PR_RECIPIENT_TYPE]; |
||
3716 | $props['display_type'] = $recipientRow[PR_DISPLAY_TYPE] ?? DT_MAILUSER; |
||
3717 | $props['display_type_ex'] = $recipientRow[PR_DISPLAY_TYPE_EX] ?? DT_MAILUSER; |
||
3718 | |||
3719 | if (isset($recipientRow[PR_RECIPIENT_FLAGS])) { |
||
3720 | $props['recipient_flags'] = $recipientRow[PR_RECIPIENT_FLAGS]; |
||
3721 | } |
||
3722 | |||
3723 | if (isset($recipientRow[PR_ENTRYID])) { |
||
3724 | $props['entryid'] = bin2hex((string) $recipientRow[PR_ENTRYID]); |
||
3725 | |||
3726 | // Get the SMTP address from the addressbook if no address is found |
||
3727 | if (empty($props['smtp_address']) && ($recipientRow[PR_ADDRTYPE] == 'EX' || $props['address_type'] === 'ZARAFA')) { |
||
3728 | $recipientSearchKey = $recipientRow[PR_SEARCH_KEY] ?? false; |
||
3729 | $props['smtp_address'] = $this->getEmailAddress($recipientRow[PR_ENTRYID], $recipientSearchKey); |
||
3730 | } |
||
3731 | } |
||
3732 | |||
3733 | // smtp address is still empty(in case of external email address) than |
||
3734 | // value of email address is copied into smtp address. |
||
3735 | if ($props['address_type'] == 'SMTP' && empty($props['smtp_address'])) { |
||
3736 | $props['smtp_address'] = $props['email_address']; |
||
3737 | } |
||
3738 | |||
3739 | // PST importer imports items without an entryid and as SMTP recipient, this causes issues for |
||
3740 | // opening meeting requests with removed users as recipient. |
||
3741 | // gromox-kdb2mt might import items without an entryid and |
||
3742 | // PR_ADDRTYPE 'ZARAFA' which causes issues when opening such messages. |
||
3743 | if (empty($props['entryid']) && ($props['address_type'] === 'SMTP' || $props['address_type'] === 'ZARAFA')) { |
||
3744 | $props['entryid'] = bin2hex(mapi_createoneoff($props['display_name'], $props['address_type'], $props['smtp_address'], MAPI_UNICODE)); |
||
3745 | } |
||
3746 | |||
3747 | // Set propose new time properties |
||
3748 | if (isset($recipientRow[PR_RECIPIENT_PROPOSED], $recipientRow[PR_RECIPIENT_PROPOSEDSTARTTIME], $recipientRow[PR_RECIPIENT_PROPOSEDENDTIME])) { |
||
3749 | $props['proposednewtime_start'] = $recipientRow[PR_RECIPIENT_PROPOSEDSTARTTIME]; |
||
3750 | $props['proposednewtime_end'] = $recipientRow[PR_RECIPIENT_PROPOSEDENDTIME]; |
||
3751 | $props['proposednewtime'] = $recipientRow[PR_RECIPIENT_PROPOSED]; |
||
3752 | } |
||
3753 | else { |
||
3754 | $props['proposednewtime'] = false; |
||
3755 | } |
||
3756 | |||
3757 | $props['recipient_trackstatus'] = $recipientRow[PR_RECIPIENT_TRACKSTATUS] ?? olRecipientTrackStatusNone; |
||
3758 | $props['recipient_trackstatus_time'] = $recipientRow[PR_RECIPIENT_TRACKSTATUS_TIME] ?? null; |
||
3759 | |||
3760 | array_push($recipientsInfo, ["props" => $props]); |
||
3761 | } |
||
3762 | } |
||
3763 | |||
3764 | return $recipientsInfo; |
||
3765 | } |
||
3766 | |||
3767 | /** |
||
3768 | * Extracts email address from PR_SEARCH_KEY property if possible. |
||
3769 | * |
||
3770 | * @param string $searchKey The PR_SEARCH_KEY property |
||
3771 | * |
||
3772 | * @return string email address if possible else return empty string |
||
3773 | */ |
||
3774 | public function getEmailAddressFromSearchKey($searchKey) { |
||
3775 | if (str_contains($searchKey, ':') && str_contains($searchKey, '@')) { |
||
3776 | return trim(strtolower(explode(':', $searchKey)[1])); |
||
3777 | } |
||
3778 | |||
3779 | return ""; |
||
3780 | } |
||
3781 | |||
3782 | /** |
||
3783 | * Create a MAPI recipient list from an XML array structure. |
||
3784 | * |
||
3785 | * This functions is used for setting the recipient table of a message. |
||
3786 | * |
||
3787 | * @param array $recipientList a list of recipients as XML array structure |
||
3788 | * @param string $opType the type of operation that will be performed on this recipient list (add, remove, modify) |
||
3789 | * @param bool $send true if we are going to send this message else false |
||
3790 | * @param mixed $isException |
||
3791 | * |
||
3792 | * @return array list of recipients with the correct MAPI properties ready for mapi_message_modifyrecipients() |
||
3793 | */ |
||
3794 | public function createRecipientList($recipientList, $opType = 'add', $isException = false, $send = false) { |
||
3795 | $recipients = []; |
||
3796 | $addrbook = $GLOBALS["mapisession"]->getAddressbook(); |
||
3797 | |||
3798 | foreach ($recipientList as $recipientItem) { |
||
3799 | if ($isException) { |
||
3800 | // We do not add organizer to exception msg in organizer's calendar. |
||
3801 | if (isset($recipientItem[PR_RECIPIENT_FLAGS]) && $recipientItem[PR_RECIPIENT_FLAGS] == (recipSendable | recipOrganizer)) { |
||
3802 | continue; |
||
3803 | } |
||
3804 | |||
3805 | $recipient[PR_RECIPIENT_FLAGS] = (recipSendable | recipExceptionalResponse | recipReserved); |
||
3806 | } |
||
3807 | |||
3808 | if (!empty($recipientItem["smtp_address"]) && empty($recipientItem["email_address"])) { |
||
3809 | $recipientItem["email_address"] = $recipientItem["smtp_address"]; |
||
3810 | } |
||
3811 | |||
3812 | // When saving a mail we can allow an empty email address or entryid, but not when sending it |
||
3813 | if ($send && empty($recipientItem["email_address"]) && empty($recipientItem['entryid'])) { |
||
3814 | return; |
||
3815 | } |
||
3816 | |||
3817 | // to modify or remove recipients we need PR_ROWID property |
||
3818 | if ($opType !== 'add' && (!isset($recipientItem['rowid']) || !is_numeric($recipientItem['rowid']))) { |
||
3819 | continue; |
||
3820 | } |
||
3821 | |||
3822 | if (isset($recipientItem['search_key']) && !empty($recipientItem['search_key'])) { |
||
3823 | // search keys sent from client are in hex format so convert it to binary format |
||
3824 | $recipientItem['search_key'] = hex2bin((string) $recipientItem['search_key']); |
||
3825 | } |
||
3826 | |||
3827 | if (isset($recipientItem["entryid"]) && !empty($recipientItem["entryid"])) { |
||
3828 | // entryids sent from client are in hex format so convert it to binary format |
||
3829 | $recipientItem["entryid"] = hex2bin((string) $recipientItem["entryid"]); |
||
3830 | |||
3831 | // Only resolve the recipient when no entryid is set |
||
3832 | } |
||
3833 | else { |
||
3834 | /** |
||
3835 | * For external contacts (DT_REMOTE_MAILUSER) email_address contains display name of contact |
||
3836 | * which is obviously not unique so for that we need to resolve address based on smtp_address |
||
3837 | * if provided. |
||
3838 | */ |
||
3839 | $addressToResolve = $recipientItem["email_address"]; |
||
3840 | if (!empty($recipientItem["smtp_address"])) { |
||
3841 | $addressToResolve = $recipientItem["smtp_address"]; |
||
3842 | } |
||
3843 | |||
3844 | // Resolve the recipient |
||
3845 | $user = [[PR_DISPLAY_NAME => $addressToResolve]]; |
||
3846 | |||
3847 | try { |
||
3848 | // resolve users based on email address with strict matching |
||
3849 | $user = mapi_ab_resolvename($addrbook, $user, EMS_AB_ADDRESS_LOOKUP); |
||
3850 | $recipientItem["display_name"] = $user[0][PR_DISPLAY_NAME]; |
||
3851 | $recipientItem["entryid"] = $user[0][PR_ENTRYID]; |
||
3852 | $recipientItem["search_key"] = $user[0][PR_SEARCH_KEY]; |
||
3853 | $recipientItem["email_address"] = $user[0][PR_EMAIL_ADDRESS]; |
||
3854 | $recipientItem["address_type"] = $user[0][PR_ADDRTYPE]; |
||
3855 | } |
||
3856 | catch (MAPIException $e) { |
||
3857 | // recipient is not resolved or it got multiple matches, |
||
3858 | // so ignore this error and continue with normal processing |
||
3859 | $e->setHandled(); |
||
3860 | } |
||
3861 | } |
||
3862 | |||
3863 | $recipient = []; |
||
3864 | $recipient[PR_DISPLAY_NAME] = $recipientItem["display_name"]; |
||
3865 | $recipient[PR_DISPLAY_TYPE] = $recipientItem["display_type"]; |
||
3866 | $recipient[PR_DISPLAY_TYPE_EX] = $recipientItem["display_type_ex"]; |
||
3867 | $recipient[PR_EMAIL_ADDRESS] = $recipientItem["email_address"]; |
||
3868 | $recipient[PR_SMTP_ADDRESS] = $recipientItem["smtp_address"]; |
||
3869 | if (isset($recipientItem["search_key"])) { |
||
3870 | $recipient[PR_SEARCH_KEY] = $recipientItem["search_key"]; |
||
3871 | } |
||
3872 | $recipient[PR_ADDRTYPE] = $recipientItem["address_type"]; |
||
3873 | $recipient[PR_OBJECT_TYPE] = $recipientItem["object_type"]; |
||
3874 | $recipient[PR_RECIPIENT_TYPE] = $recipientItem["recipient_type"]; |
||
3875 | if ($opType != 'add') { |
||
3876 | $recipient[PR_ROWID] = $recipientItem["rowid"]; |
||
3877 | } |
||
3878 | |||
3879 | if (isset($recipientItem["recipient_status"]) && !empty($recipientItem["recipient_status"])) { |
||
3880 | $recipient[PR_RECIPIENT_TRACKSTATUS] = $recipientItem["recipient_status"]; |
||
3881 | } |
||
3882 | |||
3883 | if (isset($recipientItem["recipient_flags"]) && !empty($recipient["recipient_flags"])) { |
||
3884 | $recipient[PR_RECIPIENT_FLAGS] = $recipientItem["recipient_flags"]; |
||
3885 | } |
||
3886 | else { |
||
3887 | $recipient[PR_RECIPIENT_FLAGS] = recipSendable; |
||
3888 | } |
||
3889 | |||
3890 | if (isset($recipientItem["proposednewtime"]) && !empty($recipientItem["proposednewtime"]) && isset($recipientItem["proposednewtime_start"], $recipientItem["proposednewtime_end"])) { |
||
3891 | $recipient[PR_RECIPIENT_PROPOSED] = $recipientItem["proposednewtime"]; |
||
3892 | $recipient[PR_RECIPIENT_PROPOSEDSTARTTIME] = $recipientItem["proposednewtime_start"]; |
||
3893 | $recipient[PR_RECIPIENT_PROPOSEDENDTIME] = $recipientItem["proposednewtime_end"]; |
||
3894 | } |
||
3895 | else { |
||
3896 | $recipient[PR_RECIPIENT_PROPOSED] = false; |
||
3897 | } |
||
3898 | |||
3899 | // Use given entryid if possible, otherwise create a one-off entryid |
||
3900 | if (isset($recipientItem["entryid"]) && !empty($recipientItem["entryid"])) { |
||
3901 | $recipient[PR_ENTRYID] = $recipientItem["entryid"]; |
||
3902 | } |
||
3903 | elseif ($send) { |
||
3904 | // only create one-off entryid when we are actually sending the message not saving it |
||
3905 | $recipient[PR_ENTRYID] = mapi_createoneoff($recipient[PR_DISPLAY_NAME], $recipient[PR_ADDRTYPE], $recipient[PR_EMAIL_ADDRESS]); |
||
3906 | } |
||
3907 | |||
3908 | array_push($recipients, $recipient); |
||
3909 | } |
||
3910 | |||
3911 | return $recipients; |
||
3912 | } |
||
3913 | |||
3914 | /** |
||
3915 | * Function which is get store of external resource from entryid. |
||
3916 | * |
||
3917 | * @param string $entryid entryid of the shared folder record |
||
3918 | * |
||
3919 | * @return object/boolean $store store of shared folder if found otherwise false |
||
3920 | * |
||
3921 | * FIXME: this function is pretty inefficient, since it opens the store for every |
||
3922 | * shared user in the worst case. Might be that we could extract the guid from |
||
3923 | * the $entryid and compare it and fetch the guid from the userentryid. |
||
3924 | * C++ has a GetStoreGuidFromEntryId() function. |
||
3925 | */ |
||
3926 | public function getOtherStoreFromEntryid($entryid) { |
||
3927 | // Get all external user from settings |
||
3928 | $otherUsers = $GLOBALS['mapisession']->retrieveOtherUsersFromSettings(); |
||
3929 | |||
3930 | // Fetch the store of each external user and |
||
3931 | // find the record with given entryid |
||
3932 | foreach ($otherUsers as $sharedUser => $values) { |
||
3933 | $userEntryid = mapi_msgstore_createentryid($GLOBALS['mapisession']->getDefaultMessageStore(), $sharedUser); |
||
3934 | $store = $GLOBALS['mapisession']->openMessageStore($userEntryid); |
||
3935 | if ($GLOBALS['entryid']->hasContactProviderGUID($entryid)) { |
||
3936 | $entryid = $GLOBALS["entryid"]->unwrapABEntryIdObj($entryid); |
||
3937 | } |
||
3938 | |||
3939 | try { |
||
3940 | $record = mapi_msgstore_openentry($store, hex2bin((string) $entryid)); |
||
3941 | if ($record) { |
||
3942 | return $store; |
||
3943 | } |
||
3944 | } |
||
3945 | catch (MAPIException) { |
||
3946 | } |
||
3947 | } |
||
3948 | |||
3949 | return false; |
||
3950 | } |
||
3951 | |||
3952 | /** |
||
3953 | * Function which is use to check the contact item (distribution list / contact) |
||
3954 | * belongs to any external folder or not. |
||
3955 | * |
||
3956 | * @param string $entryid entryid of contact item |
||
3957 | * |
||
3958 | * @return bool true if contact item from external folder otherwise false. |
||
3959 | * |
||
3960 | * FIXME: this function is broken and returns true if the user is a contact in a shared store. |
||
3961 | * Also research if we cannot just extract the GUID and compare it with our own GUID. |
||
3962 | * FIXME This function should be renamed, because it's also meant for normal shared folder contacts. |
||
3963 | */ |
||
3964 | public function isExternalContactItem($entryid) { |
||
3965 | try { |
||
3966 | if (!$GLOBALS['entryid']->hasContactProviderGUID(bin2hex($entryid))) { |
||
3967 | $entryid = hex2bin((string) $GLOBALS['entryid']->wrapABEntryIdObj(bin2hex($entryid), MAPI_DISTLIST)); |
||
3968 | } |
||
3969 | mapi_ab_openentry($GLOBALS["mapisession"]->getAddressbook(), $entryid); |
||
3970 | } |
||
3971 | catch (MAPIException) { |
||
3972 | return true; |
||
3973 | } |
||
3974 | |||
3975 | return false; |
||
3976 | } |
||
3977 | |||
3978 | /** |
||
3979 | * Get object type from distlist type of member of distribution list. |
||
3980 | * |
||
3981 | * @param int $distlistType distlist type of distribution list |
||
3982 | * |
||
3983 | * @return int object type of distribution list |
||
3984 | */ |
||
3985 | public function getObjectTypeFromDistlistType($distlistType) { |
||
3986 | return match ($distlistType) { |
||
3987 | DL_DIST, DL_DIST_AB => MAPI_DISTLIST, |
||
3988 | default => MAPI_MAILUSER, |
||
3989 | }; |
||
3990 | } |
||
3991 | |||
3992 | /** |
||
3993 | * Function which fetches all members of shared/internal(Local Contact Folder) |
||
3994 | * folder's distribution list. |
||
3995 | * |
||
3996 | * @param string $distlistEntryid entryid of distribution list |
||
3997 | * @param bool $isRecursive if there is/are distribution list(s) inside the distlist |
||
3998 | * to expand all the members, pass true to expand distlist recursively, false to not expand |
||
3999 | * |
||
4000 | * @return array $members all members of a distribution list |
||
4001 | */ |
||
4002 | public function expandDistList($distlistEntryid, $isRecursive = false) { |
||
4003 | $properties = $GLOBALS['properties']->getDistListProperties(); |
||
4004 | $eidObj = $GLOBALS['entryid']->createABEntryIdObj($distlistEntryid); |
||
4005 | $isMuidGuid = !$GLOBALS['entryid']->hasNoMuid('', $eidObj); |
||
4006 | $extidObj = $isMuidGuid ? |
||
4007 | $GLOBALS['entryid']->createMessageEntryIdObj($eidObj['extid']) : |
||
4008 | $GLOBALS['entryid']->createMessageEntryIdObj($GLOBALS['entryid']->createMessageEntryId($eidObj)); |
||
4009 | |||
4010 | $store = $GLOBALS["mapisession"]->getDefaultMessageStore(); |
||
4011 | $contactFolderId = $this->getPropertiesFromStoreRoot($store, [PR_IPM_CONTACT_ENTRYID]); |
||
4012 | $contactFolderidObj = $GLOBALS['entryid']->createFolderEntryIdObj(bin2hex((string) $contactFolderId[PR_IPM_CONTACT_ENTRYID])); |
||
4013 | |||
4014 | if ($contactFolderidObj['providerguid'] != $extidObj['providerguid'] && $contactFolderidObj['folderdbguid'] != $extidObj['folderdbguid']) { |
||
4015 | $storelist = $GLOBALS["mapisession"]->getAllMessageStores(); |
||
4016 | foreach ($storelist as $storeObj) { |
||
4017 | $contactFolderId = $this->getPropertiesFromStoreRoot($storeObj, [PR_IPM_CONTACT_ENTRYID]); |
||
4018 | if (isset($contactFolderId[PR_IPM_CONTACT_ENTRYID])) { |
||
4019 | $contactFolderidObj = $GLOBALS['entryid']->createFolderEntryIdObj(bin2hex((string) $contactFolderId[PR_IPM_CONTACT_ENTRYID])); |
||
4020 | if ($contactFolderidObj['providerguid'] == $extidObj['providerguid'] && $contactFolderidObj['folderdbguid'] == $extidObj['folderdbguid']) { |
||
4021 | $store = $storeObj; |
||
4022 | break; |
||
4023 | } |
||
4024 | } |
||
4025 | } |
||
4026 | } |
||
4027 | |||
4028 | if ($isMuidGuid) { |
||
4029 | $distlistEntryid = $GLOBALS["entryid"]->unwrapABEntryIdObj($distlistEntryid); |
||
4030 | } |
||
4031 | |||
4032 | try { |
||
4033 | $distlist = $this->openMessage($store, hex2bin((string) $distlistEntryid)); |
||
4034 | } |
||
4035 | catch (Exception) { |
||
4036 | // the distribution list is in a public folder |
||
4037 | $distlist = $this->openMessage($GLOBALS["mapisession"]->getPublicMessageStore(), hex2bin((string) $distlistEntryid)); |
||
4038 | } |
||
4039 | |||
4040 | // Retrieve the members from distribution list. |
||
4041 | $distlistMembers = $this->getMembersFromDistributionList($store, $distlist, $properties, $isRecursive); |
||
4042 | $recipients = []; |
||
4043 | |||
4044 | foreach ($distlistMembers as $member) { |
||
4045 | $props = $this->convertDistlistMemberToRecipient($store, $member); |
||
4046 | array_push($recipients, $props); |
||
4047 | } |
||
4048 | |||
4049 | return $recipients; |
||
4050 | } |
||
4051 | |||
4052 | /** |
||
4053 | * Function Which convert the shared/internal(local contact folder distlist) |
||
4054 | * folder's distlist members to recipient type. |
||
4055 | * |
||
4056 | * @param mapistore $store MAPI store of the message |
||
4057 | * @param array $member of distribution list contacts |
||
4058 | * |
||
4059 | * @return array members properties converted in to recipient |
||
4060 | */ |
||
4061 | public function convertDistlistMemberToRecipient($store, $member) { |
||
4149 | } |
||
4150 | |||
4151 | /** |
||
4152 | * Parse reply-to value from PR_REPLY_RECIPIENT_ENTRIES property. |
||
4153 | * |
||
4154 | * @param string $flatEntryList the PR_REPLY_RECIPIENT_ENTRIES value |
||
4155 | * |
||
4156 | * @return array list of recipients in array structure |
||
4157 | */ |
||
4158 | public function readReplyRecipientEntry($flatEntryList) { |
||
4159 | $addressbook = $GLOBALS["mapisession"]->getAddressbook(); |
||
4160 | $entryids = []; |
||
4161 | |||
4162 | // Unpack number of entries, the byte count and the entries |
||
4163 | $unpacked = unpack('V1cEntries/V1cbEntries/a*', $flatEntryList); |
||
4164 | |||
4165 | // $unpacked consists now of the following fields: |
||
4166 | // 'cEntries' => The number of entryids in our list |
||
4167 | // 'cbEntries' => The total number of bytes inside 'abEntries' |
||
4168 | // 'abEntries' => The list of Entryids |
||
4169 | // |
||
4170 | // Each 'abEntries' can be broken down into groups of 2 fields |
||
4171 | // 'cb' => The length of the entryid |
||
4172 | // 'entryid' => The entryid |
||
4173 | |||
4174 | $position = 8; // sizeof(cEntries) + sizeof(cbEntries); |
||
4175 | |||
4176 | for ($i = 0, $len = $unpacked['cEntries']; $i < $len; ++$i) { |
||
4177 | // Obtain the size for the current entry |
||
4178 | $size = unpack('a' . $position . '/V1cb/a*', $flatEntryList); |
||
4179 | |||
4180 | // We have the size, now can obtain the bytes |
||
4181 | $entryid = unpack('a' . $position . '/V1cb/a' . $size['cb'] . 'entryid/a*', $flatEntryList); |
||
4182 | |||
4183 | // unpack() will remove the NULL characters, re-add |
||
4184 | // them until we match the 'cb' length. |
||
4185 | while ($entryid['cb'] > strlen((string) $entryid['entryid'])) { |
||
4186 | $entryid['entryid'] .= chr(0x00); |
||
4187 | } |
||
4188 | |||
4189 | $entryids[] = $entryid['entryid']; |
||
4190 | |||
4191 | // sizeof(cb) + strlen(entryid) |
||
4192 | $position += 4 + $entryid['cb']; |
||
4193 | } |
||
4194 | |||
4195 | $recipients = []; |
||
4196 | foreach ($entryids as $entryid) { |
||
4197 | // Check if entryid extracted, since unpack errors can not be caught. |
||
4198 | if (!$entryid) { |
||
4199 | continue; |
||
4200 | } |
||
4201 | |||
4202 | // Handle malformed entryids |
||
4203 | try { |
||
4204 | $entry = mapi_ab_openentry($addressbook, $entryid); |
||
4205 | $props = mapi_getprops($entry, [PR_ENTRYID, PR_SEARCH_KEY, PR_OBJECT_TYPE, PR_DISPLAY_NAME, PR_ADDRTYPE, PR_EMAIL_ADDRESS]); |
||
4206 | |||
4207 | // Put data in recipient array |
||
4208 | $recipients[] = $this->composeRecipient(count($recipients), $props); |
||
4209 | } |
||
4210 | catch (MAPIException $e) { |
||
4211 | try { |
||
4212 | $oneoff = mapi_parseoneoff($entryid); |
||
4213 | } |
||
4214 | catch (MAPIException $ex) { |
||
4215 | error_log(sprintf( |
||
4216 | "readReplyRecipientEntry unable to open AB entry and mapi_parseoneoff failed: %s - %s", |
||
4217 | get_mapi_error_name($ex->getCode()), |
||
4218 | $ex->getDisplayMessage() |
||
4219 | )); |
||
4220 | |||
4221 | continue; |
||
4222 | } |
||
4223 | if (!isset($oneoff['address'])) { |
||
4224 | error_log(sprintf( |
||
4225 | "readReplyRecipientEntry unable to open AB entry and oneoff address is not available: %s - %s ", |
||
4226 | get_mapi_error_name($e->getCode()), |
||
4227 | $e->getDisplayMessage() |
||
4228 | )); |
||
4229 | |||
4230 | continue; |
||
4231 | } |
||
4232 | |||
4233 | $entryid = mapi_createoneoff($oneoff['name'] ?? '', $oneoff['type'] ?? 'SMTP', $oneoff['address']); |
||
4234 | $props = [ |
||
4235 | PR_ENTRYID => $entryid, |
||
4236 | PR_DISPLAY_NAME => !empty($oneoff['name']) ? $oneoff['name'] : $oneoff['address'], |
||
4237 | PR_ADDRTYPE => $oneoff['type'] ?? 'SMTP', |
||
4238 | PR_EMAIL_ADDRESS => $oneoff['address'], |
||
4239 | ]; |
||
4240 | $recipients[] = $this->composeRecipient(count($recipients), $props); |
||
4241 | } |
||
4242 | } |
||
4243 | |||
4244 | return $recipients; |
||
4245 | } |
||
4246 | |||
4247 | private function composeRecipient($rowid, $props) { |
||
4248 | return [ |
||
4249 | 'rowid' => $rowid, |
||
4250 | 'props' => [ |
||
4251 | 'entryid' => !empty($props[PR_ENTRYID]) ? bin2hex((string) $props[PR_ENTRYID]) : '', |
||
4252 | 'object_type' => $props[PR_OBJECT_TYPE] ?? MAPI_MAILUSER, |
||
4253 | 'search_key' => $props[PR_SEARCH_KEY] ?? '', |
||
4254 | 'display_name' => !empty($props[PR_DISPLAY_NAME]) ? $props[PR_DISPLAY_NAME] : $props[PR_EMAIL_ADDRESS], |
||
4255 | 'address_type' => $props[PR_ADDRTYPE] ?? 'SMTP', |
||
4256 | 'email_address' => $props[PR_EMAIL_ADDRESS] ?? '', |
||
4257 | 'smtp_address' => $props[PR_EMAIL_ADDRESS] ?? '', |
||
4258 | ], |
||
4259 | ]; |
||
4260 | } |
||
4261 | |||
4262 | /** |
||
4263 | * Build full-page HTML from the TinyMCE HTML. |
||
4264 | * |
||
4265 | * This function basically takes the generated HTML from TinyMCE and embeds it in |
||
4266 | * a standalone HTML page (including header and CSS) to form. |
||
4267 | * |
||
4268 | * @param string $body This is the HTML created by the TinyMCE |
||
4269 | * @param string $title Optional, this string is placed in the <title> |
||
4270 | * |
||
4271 | * @return string full HTML message |
||
4272 | */ |
||
4273 | public function generateBodyHTML($body, $title = "grommunio-web") { |
||
4274 | $html = "<!DOCTYPE html>" . |
||
4275 | "<html>\n" . |
||
4276 | "<head>\n" . |
||
4277 | " <meta name=\"Generator\" content=\"grommunio-web v" . trim(file_get_contents('version')) . "\">\n" . |
||
4278 | " <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\n" . |
||
4279 | " <title>" . htmlspecialchars($title) . "</title>\n"; |
||
4280 | |||
4281 | $html .= "</head>\n" . |
||
4282 | "<body>\n" . |
||
4283 | $body . "\n" . |
||
4284 | "</body>\n" . |
||
4285 | "</html>"; |
||
4286 | |||
4287 | return $html; |
||
4288 | } |
||
4289 | |||
4290 | /** |
||
4291 | * Calculate the total size for all items in the given folder. |
||
4292 | * |
||
4293 | * @param mapifolder $folder The folder for which the size must be calculated |
||
4294 | * |
||
4295 | * @return number The folder size |
||
4296 | */ |
||
4297 | public function calcFolderMessageSize($folder) { |
||
4298 | $folderProps = mapi_getprops($folder, [PR_MESSAGE_SIZE_EXTENDED]); |
||
4299 | |||
4300 | return $folderProps[PR_MESSAGE_SIZE_EXTENDED] ?? 0; |
||
4301 | } |
||
4302 | |||
4303 | /** |
||
4304 | * Detect plaintext body type of message. |
||
4305 | * |
||
4306 | * @param mapimessage $message MAPI message resource to check |
||
4307 | * |
||
4308 | * @return bool TRUE if the message is a plaintext message, FALSE if otherwise |
||
4309 | */ |
||
4310 | public function isPlainText($message) { |
||
4311 | $props = mapi_getprops($message, [PR_NATIVE_BODY_INFO]); |
||
4312 | if (isset($props[PR_NATIVE_BODY_INFO]) && $props[PR_NATIVE_BODY_INFO] == 1) { |
||
4313 | return true; |
||
4314 | } |
||
4315 | |||
4316 | return false; |
||
4317 | } |
||
4318 | |||
4319 | /** |
||
4320 | * Parse email recipient list and add all e-mail addresses to the recipient history. |
||
4321 | * |
||
4322 | * The recipient history is used for auto-suggestion when writing e-mails. This function |
||
4323 | * opens the recipient history property (PR_EC_RECIPIENT_HISTORY_JSON) and updates or appends |
||
4324 | * it with the passed email addresses. |
||
4325 | * |
||
4326 | * @param array $recipients list of recipients |
||
4327 | */ |
||
4328 | public function addRecipientsToRecipientHistory($recipients) { |
||
4329 | $emailAddress = []; |
||
4330 | foreach ($recipients as $key => $value) { |
||
4331 | $emailAddresses[] = $value['props']; |
||
4332 | } |
||
4333 | |||
4334 | if (empty($emailAddresses)) { |
||
4335 | return; |
||
4336 | } |
||
4337 | |||
4338 | // Retrieve the recipient history |
||
4339 | $store = $GLOBALS["mapisession"]->getDefaultMessageStore(); |
||
4340 | $storeProps = mapi_getprops($store, [PR_EC_RECIPIENT_HISTORY_JSON]); |
||
4341 | $recipient_history = []; |
||
4342 | |||
4343 | if (isset($storeProps[PR_EC_RECIPIENT_HISTORY_JSON]) || propIsError(PR_EC_RECIPIENT_HISTORY_JSON, $storeProps) == MAPI_E_NOT_ENOUGH_MEMORY) { |
||
4344 | $datastring = streamProperty($store, PR_EC_RECIPIENT_HISTORY_JSON); |
||
4345 | |||
4346 | if (!empty($datastring)) { |
||
4347 | $recipient_history = json_decode_data($datastring, true); |
||
4348 | } |
||
4349 | } |
||
4350 | |||
4351 | $l_aNewHistoryItems = []; |
||
4352 | // Loop through all new recipients |
||
4353 | for ($i = 0, $len = count($emailAddresses); $i < $len; ++$i) { |
||
4354 | if ($emailAddresses[$i]['address_type'] == 'SMTP') { |
||
4355 | $emailAddress = $emailAddresses[$i]['smtp_address']; |
||
4356 | if (empty($emailAddress)) { |
||
4357 | $emailAddress = $emailAddresses[$i]['email_address']; |
||
4358 | } |
||
4359 | } |
||
4360 | else { // address_type == 'EX' || address_type == 'MAPIPDL' |
||
4361 | $emailAddress = $emailAddresses[$i]['email_address']; |
||
4362 | if (empty($emailAddress)) { |
||
4363 | $emailAddress = $emailAddresses[$i]['smtp_address']; |
||
4364 | } |
||
4365 | } |
||
4366 | |||
4367 | // If no email address property is found, then we can't |
||
4368 | // generate a valid suggestion. |
||
4369 | if (empty($emailAddress)) { |
||
4370 | continue; |
||
4371 | } |
||
4372 | |||
4373 | $l_bFoundInHistory = false; |
||
4374 | // Loop through all the recipients in history |
||
4375 | if (is_array($recipient_history) && !empty($recipient_history['recipients'])) { |
||
4376 | for ($j = 0, $lenJ = count($recipient_history['recipients']); $j < $lenJ; ++$j) { |
||
4377 | // Email address already found in history |
||
4378 | $l_bFoundInHistory = false; |
||
4379 | |||
4380 | // The address_type property must exactly match, |
||
4381 | // when it does, a recipient matches the suggestion |
||
4382 | // if it matches to either the email_address or smtp_address. |
||
4383 | if ($emailAddresses[$i]['address_type'] === $recipient_history['recipients'][$j]['address_type']) { |
||
4384 | if ($emailAddress == $recipient_history['recipients'][$j]['email_address'] || |
||
4385 | $emailAddress == $recipient_history['recipients'][$j]['smtp_address']) { |
||
4386 | $l_bFoundInHistory = true; |
||
4387 | } |
||
4388 | } |
||
4389 | |||
4390 | if ($l_bFoundInHistory === true) { |
||
4391 | // Check if a name has been supplied. |
||
4392 | $newDisplayName = trim((string) $emailAddresses[$i]['display_name']); |
||
4393 | if (!empty($newDisplayName)) { |
||
4394 | $oldDisplayName = trim((string) $recipient_history['recipients'][$j]['display_name']); |
||
4395 | |||
4396 | // Check if the name is not the same as the email address |
||
4397 | if ($newDisplayName != $emailAddresses[$i]['smtp_address']) { |
||
4398 | $recipient_history['recipients'][$j]['display_name'] = $newDisplayName; |
||
4399 | // Check if the recipient history has no name for this email |
||
4400 | } |
||
4401 | elseif (empty($oldDisplayName)) { |
||
4402 | $recipient_history['recipients'][$j]['display_name'] = $newDisplayName; |
||
4403 | } |
||
4404 | } |
||
4405 | ++$recipient_history['recipients'][$j]['count']; |
||
4406 | $recipient_history['recipients'][$j]['last_used'] = time(); |
||
4407 | break; |
||
4408 | } |
||
4409 | } |
||
4410 | } |
||
4411 | if (!$l_bFoundInHistory && !isset($l_aNewHistoryItems[$emailAddress])) { |
||
4412 | $l_aNewHistoryItems[$emailAddress] = [ |
||
4413 | 'display_name' => $emailAddresses[$i]['display_name'], |
||
4414 | 'smtp_address' => $emailAddresses[$i]['smtp_address'], |
||
4415 | 'email_address' => $emailAddresses[$i]['email_address'], |
||
4416 | 'address_type' => $emailAddresses[$i]['address_type'], |
||
4417 | 'count' => 1, |
||
4418 | 'last_used' => time(), |
||
4419 | 'object_type' => $emailAddresses[$i]['object_type'], |
||
4420 | ]; |
||
4421 | } |
||
4422 | } |
||
4423 | if (!empty($l_aNewHistoryItems)) { |
||
4424 | foreach ($l_aNewHistoryItems as $l_aValue) { |
||
4425 | $recipient_history['recipients'][] = $l_aValue; |
||
4426 | } |
||
4427 | } |
||
4428 | |||
4429 | $l_sNewRecipientHistoryJSON = json_encode($recipient_history); |
||
4430 | |||
4431 | $stream = mapi_openproperty($store, PR_EC_RECIPIENT_HISTORY_JSON, IID_IStream, 0, MAPI_CREATE | MAPI_MODIFY); |
||
4432 | mapi_stream_setsize($stream, strlen($l_sNewRecipientHistoryJSON)); |
||
4433 | mapi_stream_write($stream, $l_sNewRecipientHistoryJSON); |
||
4434 | mapi_stream_commit($stream); |
||
4435 | mapi_savechanges($store); |
||
4436 | } |
||
4437 | |||
4438 | /** |
||
4439 | * Get the SMTP e-mail of an addressbook entry. |
||
4440 | * |
||
4441 | * @param string $entryid Addressbook entryid of object |
||
4442 | * |
||
4443 | * @return string SMTP e-mail address of that entry or FALSE on error |
||
4444 | */ |
||
4445 | public function getEmailAddressFromEntryID($entryid) { |
||
4446 | try { |
||
4447 | $mailuser = mapi_ab_openentry($GLOBALS["mapisession"]->getAddressbook(), $entryid); |
||
4448 | } |
||
4449 | catch (MAPIException $e) { |
||
4450 | // if any invalid entryid is passed in this function then it should silently ignore it |
||
4451 | // and continue with execution |
||
4452 | if ($e->getCode() == MAPI_E_UNKNOWN_ENTRYID) { |
||
4453 | $e->setHandled(); |
||
4454 | |||
4455 | return ""; |
||
4456 | } |
||
4457 | } |
||
4458 | |||
4459 | if (!isset($mailuser)) { |
||
4460 | return ""; |
||
4461 | } |
||
4462 | |||
4463 | $abprops = mapi_getprops($mailuser, [PR_SMTP_ADDRESS, PR_EMAIL_ADDRESS]); |
||
4464 | |||
4465 | return $abprops[PR_SMTP_ADDRESS] ?? $abprops[PR_EMAIL_ADDRESS] ?? ""; |
||
4466 | } |
||
4467 | |||
4468 | /** |
||
4469 | * Function which fetches all members of a distribution list recursively. |
||
4470 | * |
||
4471 | * @param resource $store MAPI Message Store Object |
||
4472 | * @param resource $message the distribution list message |
||
4473 | * @param array $properties array of properties to get properties of distlist |
||
4474 | * @param bool $isRecursive function will be called recursively if there is/are |
||
4475 | * distribution list inside the distlist to expand all the members, |
||
4476 | * pass true to expand distlist recursively, false to not expand |
||
4477 | * @param array $listEntryIDs list of already expanded Distribution list from contacts folder, |
||
4478 | * This parameter is used for recursive call of the function |
||
4479 | * |
||
4480 | * @return object $items all members of a distlist |
||
4481 | */ |
||
4482 | public function getMembersFromDistributionList($store, $message, $properties, $isRecursive = false, $listEntryIDs = []) { |
||
4483 | $items = []; |
||
4484 | |||
4485 | $props = mapi_getprops($message, [$properties['oneoff_members'], $properties['members'], PR_ENTRYID]); |
||
4486 | |||
4487 | // only continue when we have something to expand |
||
4488 | if (!isset($props[$properties['oneoff_members']]) || !isset($props[$properties['members']])) { |
||
4489 | return []; |
||
4490 | } |
||
4491 | |||
4492 | if ($isRecursive) { |
||
4493 | // when opening sub message we will not have entryid, so use entryid only when we have it |
||
4494 | if (isset($props[PR_ENTRYID])) { |
||
4495 | // for preventing recursion we need to store entryids, and check if the same distlist is going to be expanded again |
||
4496 | if (in_array($props[PR_ENTRYID], $listEntryIDs)) { |
||
4497 | // don't expand a distlist that is already expanded |
||
4498 | return []; |
||
4499 | } |
||
4500 | |||
4501 | $listEntryIDs[] = $props[PR_ENTRYID]; |
||
4502 | } |
||
4503 | } |
||
4504 | |||
4505 | $members = $props[$properties['members']]; |
||
4506 | |||
4507 | // parse oneoff members |
||
4508 | $oneoffmembers = []; |
||
4509 | foreach ($props[$properties['oneoff_members']] as $key => $item) { |
||
4510 | $oneoffmembers[$key] = mapi_parseoneoff($item); |
||
4511 | } |
||
4512 | |||
4513 | foreach ($members as $key => $item) { |
||
4514 | /* |
||
4515 | * PHP 5.5.0 and greater has made the unpack function incompatible with previous versions by changing: |
||
4516 | * - a = code now retains trailing NULL bytes. |
||
4517 | * - A = code now strips all trailing ASCII whitespace (spaces, tabs, newlines, carriage |
||
4518 | * returns, and NULL bytes). |
||
4519 | * for more http://php.net/manual/en/function.unpack.php |
||
4520 | */ |
||
4521 | if (version_compare(PHP_VERSION, '5.5.0', '>=')) { |
||
4522 | $parts = unpack('Vnull/A16guid/Ctype/a*entryid', (string) $item); |
||
4523 | } |
||
4524 | else { |
||
4525 | $parts = unpack('Vnull/A16guid/Ctype/A*entryid', (string) $item); |
||
4526 | } |
||
4527 | |||
4528 | $memberItem = []; |
||
4529 | $memberItem['props'] = []; |
||
4530 | $memberItem['props']['distlist_type'] = $parts['type']; |
||
4531 | |||
4532 | if ($parts['guid'] === hex2bin('812b1fa4bea310199d6e00dd010f5402')) { |
||
4533 | // custom e-mail address (no user or contact) |
||
4534 | $oneoff = mapi_parseoneoff($item); |
||
4535 | |||
4536 | $memberItem['props']['display_name'] = $oneoff['name']; |
||
4537 | $memberItem['props']['address_type'] = $oneoff['type']; |
||
4538 | $memberItem['props']['email_address'] = $oneoff['address']; |
||
4539 | $memberItem['props']['smtp_address'] = $oneoff['address']; |
||
4540 | $memberItem['props']['entryid'] = bin2hex((string) $members[$key]); |
||
4541 | |||
4542 | $items[] = $memberItem; |
||
4543 | } |
||
4544 | else { |
||
4545 | if ($parts['type'] === DL_DIST && $isRecursive) { |
||
4546 | // Expand distribution list to get distlist members inside the distributionlist. |
||
4547 | $distlist = mapi_msgstore_openentry($store, $parts['entryid']); |
||
4548 | $items = array_merge($items, $this->getMembersFromDistributionList($store, $distlist, $properties, true, $listEntryIDs)); |
||
4549 | } |
||
4550 | else { |
||
4551 | $memberItem['props']['entryid'] = bin2hex((string) $parts['entryid']); |
||
4552 | $memberItem['props']['display_name'] = $oneoffmembers[$key]['name']; |
||
4553 | $memberItem['props']['address_type'] = $oneoffmembers[$key]['type']; |
||
4554 | // distribution lists don't have valid email address so ignore that property |
||
4555 | |||
4556 | if ($parts['type'] !== DL_DIST) { |
||
4557 | $memberItem['props']['email_address'] = $oneoffmembers[$key]['address']; |
||
4558 | |||
4559 | // internal members in distribution list don't have smtp address so add add that property |
||
4560 | $memberProps = $this->convertDistlistMemberToRecipient($store, $memberItem); |
||
4561 | $memberItem['props']['smtp_address'] = $memberProps["smtp_address"] ?? $memberProps["email_address"]; |
||
4562 | } |
||
4563 | |||
4564 | $items[] = $memberItem; |
||
4565 | } |
||
4566 | } |
||
4567 | } |
||
4568 | |||
4569 | return $items; |
||
4570 | } |
||
4571 | |||
4572 | /** |
||
4573 | * Convert inline image <img src="data:image/mimetype;.date> links in HTML email |
||
4574 | * to CID embedded images. Which are supported in major mail clients or |
||
4575 | * providers such as outlook.com or gmail.com. |
||
4576 | * |
||
4577 | * grommunio Web now extracts the base64 image, saves it as hidden attachment, |
||
4578 | * replace the img src tag with the 'cid' which corresponds with the attachments |
||
4579 | * cid. |
||
4580 | * |
||
4581 | * @param MAPIMessage $message the distribution list message |
||
4582 | */ |
||
4583 | public function convertInlineImage($message) { |
||
4584 | $body = streamProperty($message, PR_HTML); |
||
4585 | $imageIDs = []; |
||
4586 | |||
4587 | // Only load the DOM if the HTML contains a img or data:text/plain due to a bug |
||
4588 | // in Chrome on Windows in combination with TinyMCE. |
||
4589 | if (str_contains($body, "img") || str_contains($body, "data:text/plain")) { |
||
4590 | $doc = new DOMDocument(); |
||
4591 | $cpprops = mapi_message_getprops($message, [PR_INTERNET_CPID]); |
||
4592 | $codepage = $cpprops[PR_INTERNET_CPID] ?? 1252; |
||
4593 | $hackEncoding = '<meta http-equiv="Content-Type" content="text/html; charset=' . Conversion::getCodepageCharset($codepage) . '">'; |
||
4594 | // TinyMCE does not generate valid HTML, so we must suppress warnings. |
||
4595 | @$doc->loadHTML($hackEncoding . $body); |
||
4596 | $images = $doc->getElementsByTagName('img'); |
||
4597 | $saveChanges = false; |
||
4598 | |||
4599 | foreach ($images as $image) { |
||
4600 | $src = $image->getAttribute('src'); |
||
4601 | |||
4602 | if (!str_contains($src, "cid:") && (str_contains($src, "data:image") || |
||
4603 | str_contains($body, "data:text/plain"))) { |
||
4604 | $saveChanges = true; |
||
4605 | |||
4606 | // Extract mime type data:image/jpeg; |
||
4607 | $firstOffset = strpos($src, '/') + 1; |
||
4608 | $endOffset = strpos($src, ';'); |
||
4609 | $mimeType = substr($src, $firstOffset, $endOffset - $firstOffset); |
||
4610 | |||
4611 | $dataPosition = strpos($src, ","); |
||
4612 | // Extract encoded data |
||
4613 | $rawImage = base64_decode(substr($src, $dataPosition + 1, strlen($src))); |
||
4614 | |||
4615 | $uniqueId = uniqid(); |
||
4616 | $image->setAttribute('src', 'cid:' . $uniqueId); |
||
4617 | // TinyMCE adds an extra inline image for some reason, remove it. |
||
4618 | $image->setAttribute('data-mce-src', ''); |
||
4619 | |||
4620 | array_push($imageIDs, $uniqueId); |
||
4621 | |||
4622 | // Create hidden attachment with CID |
||
4623 | $inlineImage = mapi_message_createattach($message); |
||
4624 | $props = [ |
||
4625 | PR_ATTACH_METHOD => ATTACH_BY_VALUE, |
||
4626 | PR_ATTACH_CONTENT_ID => $uniqueId, |
||
4627 | PR_ATTACHMENT_HIDDEN => true, |
||
4628 | PR_ATTACH_FLAGS => 4, |
||
4629 | PR_ATTACH_MIME_TAG => $mimeType !== 'plain' ? 'image/' . $mimeType : 'image/png', |
||
4630 | ]; |
||
4631 | mapi_setprops($inlineImage, $props); |
||
4632 | |||
4633 | $stream = mapi_openproperty($inlineImage, PR_ATTACH_DATA_BIN, IID_IStream, 0, MAPI_CREATE | MAPI_MODIFY); |
||
4634 | mapi_stream_setsize($stream, strlen($rawImage)); |
||
4635 | mapi_stream_write($stream, $rawImage); |
||
4636 | mapi_stream_commit($stream); |
||
4637 | mapi_savechanges($inlineImage); |
||
4638 | } |
||
4639 | elseif (str_contains($src, "cid:")) { |
||
4640 | // Check for the cid(there may be http: ) is in the image src. push the cid |
||
4641 | // to $imageIDs array. which further used in clearDeletedInlineAttachments function. |
||
4642 | |||
4643 | $firstOffset = strpos($src, ":") + 1; |
||
4644 | $cid = substr($src, $firstOffset); |
||
4645 | array_push($imageIDs, $cid); |
||
4646 | } |
||
4647 | } |
||
4648 | |||
4649 | if ($saveChanges) { |
||
4650 | // Write the <img src="cid:data"> changes to the HTML property |
||
4651 | $body = $doc->saveHTML(); |
||
4652 | $stream = mapi_openproperty($message, PR_HTML, IID_IStream, 0, MAPI_MODIFY); |
||
4653 | mapi_stream_setsize($stream, strlen($body)); |
||
4654 | mapi_stream_write($stream, $body); |
||
4655 | mapi_stream_commit($stream); |
||
4656 | mapi_savechanges($message); |
||
4657 | } |
||
4658 | } |
||
4659 | $this->clearDeletedInlineAttachments($message, $imageIDs); |
||
4660 | } |
||
4661 | |||
4662 | /** |
||
4663 | * Delete the deleted inline image attachment from attachment store. |
||
4664 | * |
||
4665 | * @param MAPIMessage $message the distribution list message |
||
4666 | * @param array $imageIDs Array of existing inline image PR_ATTACH_CONTENT_ID |
||
4667 | */ |
||
4668 | public function clearDeletedInlineAttachments($message, $imageIDs = []) { |
||
4669 | $attachmentTable = mapi_message_getattachmenttable($message); |
||
4670 | |||
4671 | $restriction = [RES_AND, [ |
||
4672 | [RES_PROPERTY, |
||
4673 | [ |
||
4674 | RELOP => RELOP_EQ, |
||
4675 | ULPROPTAG => PR_ATTACHMENT_HIDDEN, |
||
4676 | VALUE => [PR_ATTACHMENT_HIDDEN => true], |
||
4677 | ], |
||
4678 | ], |
||
4679 | [RES_EXIST, |
||
4680 | [ |
||
4681 | ULPROPTAG => PR_ATTACH_CONTENT_ID, |
||
4682 | ], |
||
4683 | ], |
||
4684 | ]]; |
||
4685 | |||
4686 | $attachments = mapi_table_queryallrows($attachmentTable, [PR_ATTACH_CONTENT_ID, PR_ATTACH_NUM], $restriction); |
||
4687 | foreach ($attachments as $attachment) { |
||
4688 | $clearDeletedInlineAttach = array_search($attachment[PR_ATTACH_CONTENT_ID], $imageIDs) === false; |
||
4689 | if ($clearDeletedInlineAttach) { |
||
4690 | mapi_message_deleteattach($message, $attachment[PR_ATTACH_NUM]); |
||
4691 | } |
||
4692 | } |
||
4693 | } |
||
4694 | |||
4695 | /** |
||
4696 | * This function will fetch the user from mapi session and retrieve its LDAP image. |
||
4697 | * It will return the compressed image using php's GD library. |
||
4698 | * |
||
4699 | * @param string $userEntryId The user entryid which is going to open |
||
4700 | * @param int $compressedQuality The compression factor ranges from 0 (high) to 100 (low) |
||
4701 | * Default value is set to 10 which is nearly |
||
4702 | * extreme compressed image |
||
4703 | * |
||
4704 | * @return string A base64 encoded string (data url) |
||
4705 | */ |
||
4706 | public function getCompressedUserImage($userEntryId, $compressedQuality = 10) { |
||
4707 | try { |
||
4708 | $user = $GLOBALS['mapisession']->getUser($userEntryId); |
||
4709 | } |
||
4710 | catch (Exception $e) { |
||
4711 | $msg = "Problem while getting a user from the addressbook. Error %s : %s."; |
||
4712 | $formattedMsg = sprintf($msg, $e->getCode(), $e->getMessage()); |
||
4713 | error_log($formattedMsg); |
||
4714 | Log::Write(LOGLEVEL_ERROR, "Operations:getCompressedUserImage() " . $formattedMsg); |
||
4715 | |||
4716 | return ""; |
||
4717 | } |
||
4718 | |||
4719 | $userImageProp = mapi_getprops($user, [PR_EMS_AB_THUMBNAIL_PHOTO]); |
||
4720 | if (isset($userImageProp[PR_EMS_AB_THUMBNAIL_PHOTO])) { |
||
4721 | return $this->compressedImage($userImageProp[PR_EMS_AB_THUMBNAIL_PHOTO], $compressedQuality); |
||
4722 | } |
||
4723 | |||
4724 | return ""; |
||
4725 | } |
||
4726 | |||
4727 | /** |
||
4728 | * Function used to compressed the image. |
||
4729 | * |
||
4730 | * @param string $image the image which is going to compress |
||
4731 | * @param int compressedQuality The compression factor range from 0 (high) to 100 (low) |
||
4732 | * Default value is set to 10 which is nearly extreme compressed image |
||
4733 | * @param mixed $compressedQuality |
||
4734 | * |
||
4735 | * @return string A base64 encoded string (data url) |
||
4736 | */ |
||
4737 | public function compressedImage($image, $compressedQuality = 10) { |
||
4765 | } |
||
4766 | |||
4767 | public function getPropertiesFromStoreRoot($store, $props) { |
||
4768 | $root = mapi_msgstore_openentry($store); |
||
4769 | |||
4770 | return mapi_getprops($root, $props); |
||
4771 | } |
||
4772 | |||
4773 | /** |
||
4774 | * Returns the encryption key for sodium functions. |
||
4775 | * |
||
4776 | * It will generate a new one if the user doesn't have an encryption key yet. |
||
4777 | * It will also save the key into EncryptionStore for this session if the key |
||
4778 | * wasn't there yet. |
||
4779 | * |
||
4780 | * @return string |
||
4781 | */ |
||
4782 | public function getFilesEncryptionKey() { |
||
4804 | } |
||
4805 | } |
||
4806 |