Total Complexity | 754 |
Total Lines | 4817 |
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] ?? 0, |
||
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) { |
||
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) { |
||
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) { |
||
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 | |||
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"] = mapi_table_getrowcount($table); |
||
1479 | |||
1480 | return $data; |
||
1481 | } |
||
1482 | |||
1483 | /** |
||
1484 | * Returns TRUE of the MAPI message only has inline attachments. |
||
1485 | * |
||
1486 | * @param mapimessage $message The MAPI message object to check |
||
1487 | * |
||
1488 | * @return bool TRUE if the item contains only inline attachments, FALSE otherwise |
||
1489 | * |
||
1490 | * @deprecated This function is not used, because it is much too slow to run on all messages in your inbox |
||
1491 | */ |
||
1492 | public function hasOnlyInlineAttachments($message) { |
||
1493 | $attachmentTable = @mapi_message_getattachmenttable($message); |
||
1494 | if (!$attachmentTable) { |
||
1495 | return false; |
||
1496 | } |
||
1497 | |||
1498 | $attachments = @mapi_table_queryallrows($attachmentTable, [PR_ATTACHMENT_HIDDEN]); |
||
1499 | if (empty($attachments)) { |
||
1500 | return false; |
||
1501 | } |
||
1502 | |||
1503 | foreach ($attachments as $attachmentRow) { |
||
1504 | if (!isset($attachmentRow[PR_ATTACHMENT_HIDDEN]) || !$attachmentRow[PR_ATTACHMENT_HIDDEN]) { |
||
1505 | return false; |
||
1506 | } |
||
1507 | } |
||
1508 | |||
1509 | return true; |
||
1510 | } |
||
1511 | |||
1512 | /** |
||
1513 | * Retrieve and convert body content of a message. |
||
1514 | * |
||
1515 | * This function performs the heavy lifting of decompressing RTF, converting |
||
1516 | * code pages and extracting both the HTML and plain text bodies. It can be |
||
1517 | * called independently to lazily fetch body data when required. |
||
1518 | * |
||
1519 | * @param object $message The MAPI Message Object |
||
1520 | * @param bool $html2text true - body will be converted from html to text, |
||
1521 | * false - html body will be returned |
||
1522 | * |
||
1523 | * @return array associative array containing keys 'body', 'html_body' and 'isHTML' |
||
1524 | */ |
||
1525 | public function getMessageBody($message, $html2text = false) { |
||
1526 | $result = [ |
||
1527 | 'body' => '', |
||
1528 | 'isHTML' => false, |
||
1529 | ]; |
||
1530 | |||
1531 | if (!$message) { |
||
1532 | return $result; |
||
1533 | } |
||
1534 | |||
1535 | $plaintext = $this->isPlainText($message); |
||
1536 | $tmpProps = mapi_getprops($message, [PR_BODY, PR_HTML]); |
||
1537 | |||
1538 | if (empty($tmpProps[PR_HTML])) { |
||
1539 | $tmpProps = mapi_getprops($message, [PR_BODY, PR_RTF_COMPRESSED]); |
||
1540 | if (isset($tmpProps[PR_RTF_COMPRESSED])) { |
||
1541 | $tmpProps[PR_HTML] = mapi_decompressrtf($tmpProps[PR_RTF_COMPRESSED]); |
||
1542 | } |
||
1543 | } |
||
1544 | |||
1545 | $htmlcontent = ''; |
||
1546 | if (!$plaintext && isset($tmpProps[PR_HTML])) { |
||
1547 | $cpprops = mapi_message_getprops($message, [PR_INTERNET_CPID]); |
||
1548 | $codepage = $cpprops[PR_INTERNET_CPID] ?? 65001; |
||
1549 | $htmlcontent = Conversion::convertCodepageStringToUtf8($codepage, $tmpProps[PR_HTML]); |
||
1550 | if (!empty($htmlcontent)) { |
||
1551 | if ($html2text) { |
||
1552 | $htmlcontent = ''; |
||
1553 | } |
||
1554 | else { |
||
1555 | $result['isHTML'] = true; |
||
1556 | } |
||
1557 | } |
||
1558 | |||
1559 | $htmlcontent = trim($htmlcontent, "\0"); |
||
1560 | } |
||
1561 | |||
1562 | if (isset($tmpProps[PR_BODY])) { |
||
1563 | // only open property if it exists |
||
1564 | $result['body'] = trim((string) mapi_message_openproperty($message, PR_BODY), "\0"); |
||
1565 | } |
||
1566 | elseif ($html2text && isset($tmpProps[PR_HTML])) { |
||
1567 | $result['body'] = strip_tags((string) $tmpProps[PR_HTML]); |
||
1568 | } |
||
1569 | |||
1570 | if (!empty($htmlcontent)) { |
||
1571 | $result['html_body'] = $htmlcontent; |
||
1572 | } |
||
1573 | |||
1574 | return $result; |
||
1575 | } |
||
1576 | |||
1577 | /** |
||
1578 | * Read message properties. |
||
1579 | * |
||
1580 | * Reads a message and returns the data as an XML array structure with all data from the message that is needed |
||
1581 | * to show a message (for example in the preview pane) |
||
1582 | * |
||
1583 | * @param object $store MAPI Message Store Object |
||
1584 | * @param object $message The MAPI Message Object |
||
1585 | * @param array $properties Mapping of properties that should be read |
||
1586 | * @param bool $html2text true - body will be converted from html to text, false - html body will be returned |
||
1587 | * @param bool $loadBody true - fetch body content, false - skip body retrieval |
||
1588 | * |
||
1589 | * @return array item properties |
||
1590 | * |
||
1591 | * @todo Function name is misleading as it doesn't just get message properties |
||
1592 | */ |
||
1593 | public function getMessageProps($store, $message, $properties, $html2text = false, $loadBody = false) { |
||
1594 | $props = []; |
||
1595 | |||
1596 | if ($message) { |
||
1597 | $itemprops = mapi_getprops($message, $properties); |
||
1598 | |||
1599 | /* If necessary stream the property, if it's > 8KB */ |
||
1600 | if (isset($itemprops[PR_TRANSPORT_MESSAGE_HEADERS]) || propIsError(PR_TRANSPORT_MESSAGE_HEADERS, $itemprops) == MAPI_E_NOT_ENOUGH_MEMORY) { |
||
1601 | $itemprops[PR_TRANSPORT_MESSAGE_HEADERS] = mapi_openproperty($message, PR_TRANSPORT_MESSAGE_HEADERS); |
||
1602 | } |
||
1603 | |||
1604 | $props = Conversion::mapMAPI2XML($properties, $itemprops); |
||
1605 | |||
1606 | // Get actual SMTP address for sent_representing_email_address and received_by_email_address |
||
1607 | $smtpprops = mapi_getprops($message, [PR_SENT_REPRESENTING_ENTRYID, PR_RECEIVED_BY_ENTRYID, PR_SENDER_ENTRYID]); |
||
1608 | |||
1609 | if (isset($smtpprops[PR_SENT_REPRESENTING_ENTRYID])) { |
||
1610 | try { |
||
1611 | $user = mapi_ab_openentry($GLOBALS['mapisession']->getAddressbook(true), $smtpprops[PR_SENT_REPRESENTING_ENTRYID]); |
||
1612 | if (isset($user)) { |
||
1613 | $user_props = mapi_getprops($user, [PR_EMS_AB_THUMBNAIL_PHOTO]); |
||
1614 | if (isset($user_props[PR_EMS_AB_THUMBNAIL_PHOTO])) { |
||
1615 | $props["props"]['thumbnail_photo'] = "data:image/jpeg;base64," . base64_encode((string) $user_props[PR_EMS_AB_THUMBNAIL_PHOTO]); |
||
1616 | } |
||
1617 | } |
||
1618 | } |
||
1619 | catch (MAPIException) { |
||
1620 | // do nothing |
||
1621 | } |
||
1622 | } |
||
1623 | |||
1624 | /* |
||
1625 | * Check that we have PR_SENT_REPRESENTING_ENTRYID for the item, and also |
||
1626 | * Check that we have sent_representing_email_address property there in the message, |
||
1627 | * but for contacts we are not using sent_representing_* properties so we are not |
||
1628 | * getting it from the message. So basically this will be used for mail items only |
||
1629 | */ |
||
1630 | if (isset($smtpprops[PR_SENT_REPRESENTING_ENTRYID], $props["props"]["sent_representing_email_address"])) { |
||
1631 | $props["props"]["sent_representing_username"] = $props["props"]["sent_representing_email_address"]; |
||
1632 | $sentRepresentingSearchKey = isset($props['props']['sent_representing_search_key']) ? hex2bin($props['props']['sent_representing_search_key']) : false; |
||
1633 | $props["props"]["sent_representing_email_address"] = $this->getEmailAddress($smtpprops[PR_SENT_REPRESENTING_ENTRYID], $sentRepresentingSearchKey); |
||
1634 | } |
||
1635 | |||
1636 | if (isset($smtpprops[PR_SENDER_ENTRYID], $props["props"]["sender_email_address"])) { |
||
1637 | $props["props"]["sender_username"] = $props["props"]["sender_email_address"]; |
||
1638 | $senderSearchKey = isset($props['props']['sender_search_key']) ? hex2bin($props['props']['sender_search_key']) : false; |
||
1639 | $props["props"]["sender_email_address"] = $this->getEmailAddress($smtpprops[PR_SENDER_ENTRYID], $senderSearchKey); |
||
1640 | } |
||
1641 | |||
1642 | if (isset($smtpprops[PR_RECEIVED_BY_ENTRYID], $props["props"]["received_by_email_address"])) { |
||
1643 | $props["props"]["received_by_username"] = $props["props"]["received_by_email_address"]; |
||
1644 | $receivedSearchKey = isset($props['props']['received_by_search_key']) ? hex2bin($props['props']['received_by_search_key']) : false; |
||
1645 | $props["props"]["received_by_email_address"] = $this->getEmailAddress($smtpprops[PR_RECEIVED_BY_ENTRYID], $receivedSearchKey); |
||
1646 | } |
||
1647 | |||
1648 | $props['props']['isHTML'] = false; |
||
1649 | $htmlcontent = null; |
||
1650 | if ($loadBody) { |
||
1651 | $body = $this->getMessageBody($message, $html2text); |
||
1652 | $props['props'] = array_merge($props['props'], $body); |
||
1653 | $htmlcontent = $body['html_body'] ?? null; |
||
1654 | } |
||
1655 | |||
1656 | // Get reply-to information, otherwise consider the sender to be the reply-to person. |
||
1657 | $props['reply-to'] = ['item' => []]; |
||
1658 | $messageprops = mapi_getprops($message, [PR_REPLY_RECIPIENT_ENTRIES]); |
||
1659 | if (isset($messageprops[PR_REPLY_RECIPIENT_ENTRIES])) { |
||
1660 | $props['reply-to']['item'] = $this->readReplyRecipientEntry($messageprops[PR_REPLY_RECIPIENT_ENTRIES]); |
||
1661 | } |
||
1662 | if (!isset($messageprops[PR_REPLY_RECIPIENT_ENTRIES]) || count($props['reply-to']['item']) === 0) { |
||
1663 | if (isset($props['props']['sent_representing_email_address']) && !empty($props['props']['sent_representing_email_address'])) { |
||
1664 | $props['reply-to']['item'][] = [ |
||
1665 | 'rowid' => 0, |
||
1666 | 'props' => [ |
||
1667 | 'entryid' => $props['props']['sent_representing_entryid'], |
||
1668 | 'display_name' => $props['props']['sent_representing_name'], |
||
1669 | 'smtp_address' => $props['props']['sent_representing_email_address'], |
||
1670 | 'address_type' => $props['props']['sent_representing_address_type'], |
||
1671 | 'object_type' => MAPI_MAILUSER, |
||
1672 | 'search_key' => $props['props']['sent_representing_search_key'] ?? '', |
||
1673 | ], |
||
1674 | ]; |
||
1675 | } |
||
1676 | elseif (!empty($props['props']['sender_email_address'])) { |
||
1677 | $props['reply-to']['item'][] = [ |
||
1678 | 'rowid' => 0, |
||
1679 | 'props' => [ |
||
1680 | 'entryid' => $props['props']['sender_entryid'], |
||
1681 | 'display_name' => $props['props']['sender_name'], |
||
1682 | 'smtp_address' => $props['props']['sender_email_address'], |
||
1683 | 'address_type' => $props['props']['sender_address_type'], |
||
1684 | 'object_type' => MAPI_MAILUSER, |
||
1685 | 'search_key' => $props['props']['sender_search_key'], |
||
1686 | ], |
||
1687 | ]; |
||
1688 | } |
||
1689 | } |
||
1690 | |||
1691 | // Get recipients |
||
1692 | $recipients = $GLOBALS["operations"]->getRecipientsInfo($message); |
||
1693 | if (!empty($recipients)) { |
||
1694 | $props["recipients"] = [ |
||
1695 | "item" => $recipients, |
||
1696 | ]; |
||
1697 | } |
||
1698 | |||
1699 | // Get attachments |
||
1700 | $attachments = $GLOBALS["operations"]->getAttachmentsInfo($message); |
||
1701 | if (!empty($attachments)) { |
||
1702 | $props["attachments"] = [ |
||
1703 | "item" => $attachments, |
||
1704 | ]; |
||
1705 | $cid_found = false; |
||
1706 | foreach ($attachments as $attachment) { |
||
1707 | if (isset($attachment["props"]["cid"])) { |
||
1708 | $cid_found = true; |
||
1709 | } |
||
1710 | } |
||
1711 | if ($loadBody && $cid_found === true && $htmlcontent !== null) { |
||
1712 | preg_match_all('/src="cid:(.*)"/Uims', $htmlcontent, $matches); |
||
1713 | if (count($matches) > 0) { |
||
1714 | $search = []; |
||
1715 | $replace = []; |
||
1716 | foreach ($matches[1] as $match) { |
||
1717 | $idx = -1; |
||
1718 | foreach ($attachments as $key => $attachment) { |
||
1719 | if (isset($attachment["props"]["cid"]) && |
||
1720 | strcasecmp($match, $attachment["props"]["cid"]) == 0) { |
||
1721 | $idx = $key; |
||
1722 | $num = $attachment["props"]["attach_num"]; |
||
1723 | } |
||
1724 | } |
||
1725 | if ($idx == -1) { |
||
1726 | continue; |
||
1727 | } |
||
1728 | $attach = mapi_message_openattach($message, $num); |
||
1729 | if (empty($attach)) { |
||
1730 | continue; |
||
1731 | } |
||
1732 | $attachprop = mapi_getprops($attach, [PR_ATTACH_DATA_BIN, PR_ATTACH_MIME_TAG]); |
||
1733 | if (empty($attachprop) || !isset($attachprop[PR_ATTACH_DATA_BIN])) { |
||
1734 | continue; |
||
1735 | } |
||
1736 | if (!isset($attachprop[PR_ATTACH_MIME_TAG])) { |
||
1737 | $mime_tag = "text/plain"; |
||
1738 | } |
||
1739 | else { |
||
1740 | $mime_tag = $attachprop[PR_ATTACH_MIME_TAG]; |
||
1741 | } |
||
1742 | $search[] = "src=\"cid:{$match}\""; |
||
1743 | $replace[] = "src=\"data:{$mime_tag};base64," . base64_encode((string) $attachprop[PR_ATTACH_DATA_BIN]) . "\""; |
||
1744 | unset($props["attachments"]["item"][$idx]); |
||
1745 | } |
||
1746 | $props["attachments"]["item"] = array_values($props["attachments"]["item"]); |
||
1747 | $htmlcontent = str_replace($search, $replace, $htmlcontent); |
||
1748 | $props["props"]["html_body"] = $htmlcontent; |
||
1749 | } |
||
1750 | } |
||
1751 | } |
||
1752 | |||
1753 | // for distlists, we need to get members data |
||
1754 | if (isset($props["props"]["oneoff_members"], $props["props"]["members"])) { |
||
1755 | // remove non-client props |
||
1756 | unset($props["props"]["members"], $props["props"]["oneoff_members"]); |
||
1757 | |||
1758 | // get members |
||
1759 | $members = $GLOBALS["operations"]->getMembersFromDistributionList($store, $message, $properties); |
||
1760 | if (!empty($members)) { |
||
1761 | $props["members"] = [ |
||
1762 | "item" => $members, |
||
1763 | ]; |
||
1764 | } |
||
1765 | } |
||
1766 | } |
||
1767 | |||
1768 | return $props; |
||
1769 | } |
||
1770 | |||
1771 | /** |
||
1772 | * Get the email address either from entryid or search key. Function is helpful |
||
1773 | * to retrieve the email address of already deleted contact which is use as a |
||
1774 | * recipient in message. |
||
1775 | * |
||
1776 | * @param string $entryId the entryId of an item/recipient |
||
1777 | * @param bool|string $searchKey then search key of an item/recipient |
||
1778 | * |
||
1779 | * @return string email address if found else return empty string |
||
1780 | */ |
||
1781 | public function getEmailAddress($entryId, $searchKey = false) { |
||
1782 | $emailAddress = $this->getEmailAddressFromEntryID($entryId); |
||
1783 | if (empty($emailAddress) && $searchKey !== false) { |
||
1784 | $emailAddress = $this->getEmailAddressFromSearchKey($searchKey); |
||
1785 | } |
||
1786 | |||
1787 | return $emailAddress; |
||
1788 | } |
||
1789 | |||
1790 | /** |
||
1791 | * Get and convert properties of a message into an XML array structure. |
||
1792 | * |
||
1793 | * @param object $item The MAPI Object |
||
1794 | * @param array $properties Mapping of properties that should be read |
||
1795 | * |
||
1796 | * @return array XML array structure |
||
1797 | * |
||
1798 | * @todo Function name is misleading, especially compared to getMessageProps() |
||
1799 | */ |
||
1800 | public function getProps($item, $properties) { |
||
1801 | $props = []; |
||
1802 | |||
1803 | if ($item) { |
||
1804 | $itemprops = mapi_getprops($item, $properties); |
||
1805 | $props = Conversion::mapMAPI2XML($properties, $itemprops); |
||
1806 | } |
||
1807 | |||
1808 | return $props; |
||
1809 | } |
||
1810 | |||
1811 | /** |
||
1812 | * Get embedded message data. |
||
1813 | * |
||
1814 | * Returns the same data as getMessageProps, but then for a specific sub/sub/sub message |
||
1815 | * of a MAPI message. |
||
1816 | * |
||
1817 | * @param object $store MAPI Message Store Object |
||
1818 | * @param object $message MAPI Message Object |
||
1819 | * @param array $properties a set of properties which will be selected |
||
1820 | * @param array $parentMessage MAPI Message Object of parent |
||
1821 | * @param array $attach_num a list of attachment numbers (aka 2,1 means 'attachment nr 1 of attachment nr 2') |
||
1822 | * |
||
1823 | * @return array item XML array structure of the embedded message |
||
1824 | */ |
||
1825 | public function getEmbeddedMessageProps($store, $message, $properties, $parentMessage, $attach_num) { |
||
1826 | $msgprops = mapi_getprops($message, [PR_MESSAGE_CLASS]); |
||
1827 | |||
1828 | $html2text = match ($msgprops[PR_MESSAGE_CLASS]) { |
||
1829 | 'IPM.Note' => false, |
||
1830 | default => true, |
||
1831 | }; |
||
1832 | |||
1833 | $props = $this->getMessageProps($store, $message, $properties, $html2text, true); |
||
1834 | |||
1835 | // sub message will not be having entryid, so use parent's entryid |
||
1836 | $parentProps = mapi_getprops($parentMessage, [PR_ENTRYID, PR_PARENT_ENTRYID, PR_STORE_ENTRYID]); |
||
1837 | $props['entryid'] = bin2hex((string) $parentProps[PR_ENTRYID]); |
||
1838 | $props['parent_entryid'] = bin2hex((string) $parentProps[PR_PARENT_ENTRYID]); |
||
1839 | $props['store_entryid'] = bin2hex((string) $parentProps[PR_STORE_ENTRYID]); |
||
1840 | $props['attach_num'] = $attach_num; |
||
1841 | |||
1842 | return $props; |
||
1843 | } |
||
1844 | |||
1845 | /** |
||
1846 | * Create a MAPI message. |
||
1847 | * |
||
1848 | * @param object $store MAPI Message Store Object |
||
1849 | * @param string $parententryid The entryid of the folder in which the new message is to be created |
||
1850 | * |
||
1851 | * @return mapimessage Created MAPI message resource |
||
1852 | */ |
||
1853 | public function createMessage($store, $parententryid) { |
||
1854 | $folder = mapi_msgstore_openentry($store, $parententryid); |
||
1855 | |||
1856 | return mapi_folder_createmessage($folder); |
||
1857 | } |
||
1858 | |||
1859 | /** |
||
1860 | * Open a MAPI message. |
||
1861 | * |
||
1862 | * @param object $store MAPI Message Store Object |
||
1863 | * @param string $entryid entryid of the message |
||
1864 | * @param array $attach_num a list of attachment numbers (aka 2,1 means 'attachment nr 1 of attachment nr 2') |
||
1865 | * @param bool $parse_smime (optional) call parse_smime on the opened message or not |
||
1866 | * |
||
1867 | * @return object MAPI Message |
||
1868 | */ |
||
1869 | public function openMessage($store, $entryid, $attach_num = false, $parse_smime = false) { |
||
1870 | $message = mapi_msgstore_openentry($store, $entryid); |
||
1871 | |||
1872 | // Needed for S/MIME messages with embedded message attachments |
||
1873 | if ($parse_smime) { |
||
1874 | parse_smime($store, $message); |
||
1875 | } |
||
1876 | |||
1877 | if ($message && $attach_num) { |
||
1878 | for ($index = 0, $count = count($attach_num); $index < $count; ++$index) { |
||
1879 | // attach_num cannot have value of -1 |
||
1880 | // if we get that then we are trying to open an embedded message which |
||
1881 | // is not present in the attachment table to parent message (because parent message is unsaved yet) |
||
1882 | // so return the message which is opened using entryid which will point to actual message which is |
||
1883 | // attached as embedded message |
||
1884 | if ($attach_num[$index] === -1) { |
||
1885 | return $message; |
||
1886 | } |
||
1887 | |||
1888 | $attachment = mapi_message_openattach($message, $attach_num[$index]); |
||
1889 | |||
1890 | if ($attachment) { |
||
1891 | $message = mapi_attach_openobj($attachment); |
||
1892 | } |
||
1893 | else { |
||
1894 | return false; |
||
1895 | } |
||
1896 | } |
||
1897 | } |
||
1898 | |||
1899 | return $message; |
||
1900 | } |
||
1901 | |||
1902 | /** |
||
1903 | * Save a MAPI message. |
||
1904 | * |
||
1905 | * The to-be-saved message can be of any type, including e-mail items, appointments, contacts, etc. The message may be pre-existing |
||
1906 | * or it may be a new message. |
||
1907 | * |
||
1908 | * The dialog_attachments parameter represents a unique ID which for the dialog in the client for which this function was called; This |
||
1909 | * 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, |
||
1910 | * the temporary server location of the attachment is saved in the session information, accompanied by the $dialog_attachments unique ID. This |
||
1911 | * way, when we save the message into MAPI, we know which attachment was previously uploaded ready for this message, because when the user saves |
||
1912 | * the message, we pass the same $dialog_attachments ID as when we uploaded the file. |
||
1913 | * |
||
1914 | * @param object $store MAPI Message Store Object |
||
1915 | * @param binary $entryid entryid of the message |
||
1916 | * @param binary $parententryid Parent entryid of the message |
||
1917 | * @param array $props The MAPI properties to be saved |
||
1918 | * @param array $messageProps reference to an array which will be filled with PR_ENTRYID and PR_STORE_ENTRYID of the saved message |
||
1919 | * @param array $recipients XML array structure of recipients for the recipient table |
||
1920 | * @param array $attachments attachments array containing unique check number which checks if attachments should be added |
||
1921 | * @param array $propertiesToDelete Properties specified in this array are deleted from the MAPI message |
||
1922 | * @param MAPIMessage $copyFromMessage resource of the message from which we should |
||
1923 | * copy attachments and/or recipients to the current message |
||
1924 | * @param bool $copyAttachments if set we copy all attachments from the $copyFromMessage |
||
1925 | * @param bool $copyRecipients if set we copy all recipients from the $copyFromMessage |
||
1926 | * @param bool $copyInlineAttachmentsOnly if true then copy only inline attachments |
||
1927 | * @param bool $saveChanges if true then save all change in mapi message |
||
1928 | * @param bool $send true if this function is called from submitMessage else false |
||
1929 | * @param bool $isPlainText if true then message body will be generated using PR_BODY otherwise PR_HTML will be used in saveMessage() function |
||
1930 | * |
||
1931 | * @return mapimessage Saved MAPI message resource |
||
1932 | */ |
||
1933 | 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) { |
||
1934 | $message = false; |
||
1935 | |||
1936 | // Check if an entryid is set, otherwise create a new message |
||
1937 | if ($entryid && !empty($entryid)) { |
||
1938 | $message = $this->openMessage($store, $entryid); |
||
1939 | } |
||
1940 | else { |
||
1941 | $message = $this->createMessage($store, $parententryid); |
||
1942 | } |
||
1943 | |||
1944 | if ($message) { |
||
1945 | $property = false; |
||
1946 | $body = ""; |
||
1947 | |||
1948 | // Check if the body is set. |
||
1949 | if (isset($props[PR_BODY])) { |
||
1950 | $body = $props[PR_BODY]; |
||
1951 | $property = PR_BODY; |
||
1952 | $bodyPropertiesToDelete = [PR_HTML, PR_RTF_COMPRESSED]; |
||
1953 | |||
1954 | if (isset($props[PR_HTML])) { |
||
1955 | $subject = ''; |
||
1956 | if (isset($props[PR_SUBJECT])) { |
||
1957 | $subject = $props[PR_SUBJECT]; |
||
1958 | // If subject is not updated we need to get it from the message |
||
1959 | } |
||
1960 | else { |
||
1961 | $subjectProp = mapi_getprops($message, [PR_SUBJECT]); |
||
1962 | if (isset($subjectProp[PR_SUBJECT])) { |
||
1963 | $subject = $subjectProp[PR_SUBJECT]; |
||
1964 | } |
||
1965 | } |
||
1966 | $body = $this->generateBodyHTML($isPlainText ? $props[PR_BODY] : $props[PR_HTML], $subject); |
||
1967 | $property = PR_HTML; |
||
1968 | $bodyPropertiesToDelete = [PR_BODY, PR_RTF_COMPRESSED]; |
||
1969 | unset($props[PR_HTML]); |
||
1970 | } |
||
1971 | unset($props[PR_BODY]); |
||
1972 | |||
1973 | $propertiesToDelete = array_unique(array_merge($propertiesToDelete, $bodyPropertiesToDelete)); |
||
1974 | } |
||
1975 | |||
1976 | if (!isset($props[PR_SENT_REPRESENTING_ENTRYID]) && |
||
1977 | isset($props[PR_SENT_REPRESENTING_EMAIL_ADDRESS]) && !empty($props[PR_SENT_REPRESENTING_EMAIL_ADDRESS]) && |
||
1978 | isset($props[PR_SENT_REPRESENTING_ADDRTYPE]) && !empty($props[PR_SENT_REPRESENTING_ADDRTYPE]) && |
||
1979 | isset($props[PR_SENT_REPRESENTING_NAME]) && !empty($props[PR_SENT_REPRESENTING_NAME])) { |
||
1980 | // Set FROM field properties |
||
1981 | $props[PR_SENT_REPRESENTING_ENTRYID] = mapi_createoneoff($props[PR_SENT_REPRESENTING_NAME], $props[PR_SENT_REPRESENTING_ADDRTYPE], $props[PR_SENT_REPRESENTING_EMAIL_ADDRESS]); |
||
1982 | } |
||
1983 | |||
1984 | /* |
||
1985 | * Delete PR_SENT_REPRESENTING_ENTRYID and PR_SENT_REPRESENTING_SEARCH_KEY properties, if PR_SENT_REPRESENTING_* properties are configured with empty string. |
||
1986 | * Because, this is the case while user removes recipient from FROM field and send that particular draft without saving it. |
||
1987 | */ |
||
1988 | if (isset($props[PR_SENT_REPRESENTING_EMAIL_ADDRESS]) && empty($props[PR_SENT_REPRESENTING_EMAIL_ADDRESS]) && |
||
1989 | isset($props[PR_SENT_REPRESENTING_ADDRTYPE]) && empty($props[PR_SENT_REPRESENTING_ADDRTYPE]) && |
||
1990 | isset($props[PR_SENT_REPRESENTING_NAME]) && empty($props[PR_SENT_REPRESENTING_NAME])) { |
||
1991 | array_push($propertiesToDelete, PR_SENT_REPRESENTING_ENTRYID, PR_SENT_REPRESENTING_SEARCH_KEY); |
||
1992 | } |
||
1993 | |||
1994 | // remove mv properties when needed |
||
1995 | foreach ($props as $propTag => $propVal) { |
||
1996 | switch (mapi_prop_type($propTag)) { |
||
1997 | case PT_SYSTIME: |
||
1998 | // Empty PT_SYSTIME values mean they should be deleted (there is no way to set an empty PT_SYSTIME) |
||
1999 | // case PT_STRING8: // not enabled at this moment |
||
2000 | // Empty Strings |
||
2001 | case PT_MV_LONG: |
||
2002 | // Empty multivalued long |
||
2003 | if (empty($propVal)) { |
||
2004 | $propertiesToDelete[] = $propTag; |
||
2005 | } |
||
2006 | break; |
||
2007 | |||
2008 | case PT_MV_STRING8: |
||
2009 | // Empty multivalued string |
||
2010 | if (empty($propVal)) { |
||
2011 | $props[$propTag] = []; |
||
2012 | } |
||
2013 | break; |
||
2014 | } |
||
2015 | } |
||
2016 | |||
2017 | foreach ($propertiesToDelete as $prop) { |
||
2018 | unset($props[$prop]); |
||
2019 | } |
||
2020 | |||
2021 | // Set the properties |
||
2022 | mapi_setprops($message, $props); |
||
2023 | |||
2024 | // Delete the properties we don't need anymore |
||
2025 | mapi_deleteprops($message, $propertiesToDelete); |
||
2026 | |||
2027 | if ($property != false) { |
||
2028 | // Stream the body to the PR_BODY or PR_HTML property |
||
2029 | $stream = mapi_openproperty($message, $property, IID_IStream, 0, MAPI_CREATE | MAPI_MODIFY); |
||
2030 | mapi_stream_setsize($stream, strlen((string) $body)); |
||
2031 | mapi_stream_write($stream, $body); |
||
2032 | mapi_stream_commit($stream); |
||
2033 | } |
||
2034 | |||
2035 | /* |
||
2036 | * Save recipients |
||
2037 | * |
||
2038 | * If we are sending mail from delegator's folder, then we need to copy |
||
2039 | * all recipients from original message first - need to pass message |
||
2040 | * |
||
2041 | * if delegate has added or removed any recipients then they will be |
||
2042 | * added/removed using recipients array. |
||
2043 | */ |
||
2044 | if ($copyRecipients !== false && $copyFromMessage !== false) { |
||
2045 | $this->copyRecipients($message, $copyFromMessage); |
||
2046 | } |
||
2047 | |||
2048 | $this->setRecipients($message, $recipients, $send); |
||
2049 | |||
2050 | // Save the attachments with the $dialog_attachments, for attachments we have to obtain |
||
2051 | // some additional information from the state. |
||
2052 | if (!empty($attachments)) { |
||
2053 | $attachment_state = new AttachmentState(); |
||
2054 | $attachment_state->open(); |
||
2055 | |||
2056 | if ($copyFromMessage !== false) { |
||
2057 | $this->copyAttachments($message, $attachments, $copyFromMessage, $copyInlineAttachmentsOnly, $attachment_state); |
||
2058 | } |
||
2059 | |||
2060 | $this->setAttachments($message, $attachments, $attachment_state); |
||
2061 | |||
2062 | $attachment_state->close(); |
||
2063 | } |
||
2064 | |||
2065 | // Set 'hideattachments' if message has only inline attachments. |
||
2066 | $properties = $GLOBALS['properties']->getMailProperties(); |
||
2067 | if ($this->hasOnlyInlineAttachments($message)) { |
||
2068 | mapi_setprops($message, [$properties['hide_attachments'] => true]); |
||
2069 | } |
||
2070 | else { |
||
2071 | mapi_deleteprops($message, [$properties['hide_attachments']]); |
||
2072 | } |
||
2073 | |||
2074 | $this->convertInlineImage($message); |
||
2075 | // Save changes |
||
2076 | if ($saveChanges) { |
||
2077 | mapi_savechanges($message); |
||
2078 | } |
||
2079 | |||
2080 | // Get the PR_ENTRYID, PR_PARENT_ENTRYID and PR_STORE_ENTRYID properties of this message |
||
2081 | $messageProps = mapi_getprops($message, [PR_ENTRYID, PR_PARENT_ENTRYID, PR_STORE_ENTRYID]); |
||
2082 | } |
||
2083 | |||
2084 | return $message; |
||
2085 | } |
||
2086 | |||
2087 | /** |
||
2088 | * Save an appointment item. |
||
2089 | * |
||
2090 | * This is basically the same as saving any other type of message with the added complexity that |
||
2091 | * we support saving exceptions to recurrence here. This means that if the client sends a basedate |
||
2092 | * in the action, that we will attempt to open an existing exception and change that, and if that |
||
2093 | * fails, create a new exception with the specified data. |
||
2094 | * |
||
2095 | * @param mapistore $store MAPI store of the message |
||
2096 | * @param string $entryid entryid of the message |
||
2097 | * @param string $parententryid Parent entryid of the message (folder entryid, NOT message entryid) |
||
2098 | * @param array $action Action array containing XML request |
||
2099 | * @param string $actionType The action type which triggered this action |
||
2100 | * @param bool $directBookingMeetingRequest Indicates if a Meeting Request should use direct booking or not. Defaults to true. |
||
2101 | * |
||
2102 | * @return array of PR_ENTRYID, PR_PARENT_ENTRYID and PR_STORE_ENTRYID properties of modified item |
||
2103 | */ |
||
2104 | public function saveAppointment($store, $entryid, $parententryid, $action, $actionType = 'save', $directBookingMeetingRequest = true) { |
||
2105 | $messageProps = []; |
||
2106 | // It stores the values that is exception allowed or not false -> not allowed |
||
2107 | $isExceptionAllowed = true; |
||
2108 | $delete = $actionType == 'delete'; // Flag for MeetingRequest Class whether to send update or cancel mail. |
||
2109 | $basedate = false; // Flag for MeetingRequest Class whether to send an exception or not. |
||
2110 | $isReminderTimeAllowed = true; // Flag to check reminder minutes is in range of the occurrences |
||
2111 | $properties = $GLOBALS['properties']->getAppointmentProperties(); |
||
2112 | $send = false; |
||
2113 | $oldProps = []; |
||
2114 | $pasteRecord = false; |
||
2115 | |||
2116 | if (isset($action['message_action'], $action['message_action']['send'])) { |
||
2117 | $send = $action['message_action']['send']; |
||
2118 | } |
||
2119 | |||
2120 | if (isset($action['message_action'], $action['message_action']['paste'])) { |
||
2121 | $pasteRecord = true; |
||
2122 | } |
||
2123 | |||
2124 | if (!empty($action['recipients'])) { |
||
2125 | $recips = $action['recipients']; |
||
2126 | } |
||
2127 | else { |
||
2128 | $recips = false; |
||
2129 | } |
||
2130 | |||
2131 | // Set PidLidAppointmentTimeZoneDefinitionStartDisplay and |
||
2132 | // PidLidAppointmentTimeZoneDefinitionEndDisplay so that the allday |
||
2133 | // events are displayed correctly |
||
2134 | if (!empty($action['props']['timezone_iana'])) { |
||
2135 | try { |
||
2136 | $tzdef = mapi_ianatz_to_tzdef($action['props']['timezone_iana']); |
||
2137 | } |
||
2138 | catch (Exception) { |
||
2139 | } |
||
2140 | if ($tzdef !== false) { |
||
2141 | $action['props']['tzdefstart'] = $action['props']['tzdefend'] = bin2hex($tzdef); |
||
2142 | if (isset($action['props']['recurring']) && $action['props']['recurring'] == true) { |
||
2143 | $action['props']['tzdefrecur'] = $action['props']['tzdefstart']; |
||
2144 | } |
||
2145 | } |
||
2146 | } |
||
2147 | |||
2148 | if ($store && $parententryid) { |
||
2149 | // @FIXME: check for $action['props'] array |
||
2150 | if (isset($entryid) && $entryid) { |
||
2151 | // Modify existing or add/change exception |
||
2152 | $message = mapi_msgstore_openentry($store, $entryid); |
||
2153 | |||
2154 | if ($message) { |
||
2155 | $props = mapi_getprops($message, $properties); |
||
2156 | // Do not update timezone information if the appointment times haven't changed |
||
2157 | if (!isset($action['props']['commonstart']) && |
||
2158 | !isset($action['props']['commonend']) && |
||
2159 | !isset($action['props']['startdate']) && |
||
2160 | !isset($action['props']['enddate']) |
||
2161 | ) { |
||
2162 | unset($action['props']['tzdefstart'], $action['props']['tzdefend'], $action['props']['tzdefrecur']); |
||
2163 | } |
||
2164 | // Check if appointment is an exception to a recurring item |
||
2165 | if (isset($action['basedate']) && $action['basedate'] > 0) { |
||
2166 | // Create recurrence object |
||
2167 | $recurrence = new Recurrence($store, $message); |
||
2168 | |||
2169 | $basedate = $action['basedate']; |
||
2170 | $exceptionatt = $recurrence->getExceptionAttachment($basedate); |
||
2171 | if ($exceptionatt) { |
||
2172 | // get properties of existing exception. |
||
2173 | $exceptionattProps = mapi_getprops($exceptionatt, [PR_ATTACH_NUM]); |
||
2174 | $attach_num = $exceptionattProps[PR_ATTACH_NUM]; |
||
2175 | } |
||
2176 | |||
2177 | if ($delete === true) { |
||
2178 | $isExceptionAllowed = $recurrence->createException([], $basedate, true); |
||
2179 | } |
||
2180 | else { |
||
2181 | $exception_recips = []; |
||
2182 | if (isset($recips['add'])) { |
||
2183 | $savedUnsavedRecipients = []; |
||
2184 | foreach ($recips["add"] as $recip) { |
||
2185 | $savedUnsavedRecipients["unsaved"][] = $recip; |
||
2186 | } |
||
2187 | // convert all local distribution list members to ideal recipient. |
||
2188 | $members = $this->convertLocalDistlistMembersToRecipients($savedUnsavedRecipients); |
||
2189 | |||
2190 | $recips['add'] = $members['add']; |
||
2191 | $exception_recips['add'] = $this->createRecipientList($recips['add'], 'add', true, true); |
||
2192 | } |
||
2193 | if (isset($recips['remove'])) { |
||
2194 | $exception_recips['remove'] = $this->createRecipientList($recips['remove'], 'remove'); |
||
2195 | } |
||
2196 | if (isset($recips['modify'])) { |
||
2197 | $exception_recips['modify'] = $this->createRecipientList($recips['modify'], 'modify', true, true); |
||
2198 | } |
||
2199 | |||
2200 | if (isset($action['props']['reminder_minutes'], $action['props']['startdate'])) { |
||
2201 | $isReminderTimeAllowed = $recurrence->isValidReminderTime($basedate, $action['props']['reminder_minutes'], $action['props']['startdate']); |
||
2202 | } |
||
2203 | |||
2204 | // As the reminder minutes occurs before other occurrences don't modify the item. |
||
2205 | if ($isReminderTimeAllowed) { |
||
2206 | if ($recurrence->isException($basedate)) { |
||
2207 | $oldProps = $recurrence->getExceptionProperties($recurrence->getChangeException($basedate)); |
||
2208 | |||
2209 | $isExceptionAllowed = $recurrence->modifyException(Conversion::mapXML2MAPI($properties, $action['props']), $basedate, $exception_recips); |
||
2210 | } |
||
2211 | else { |
||
2212 | $oldProps[$properties['startdate']] = $recurrence->getOccurrenceStart($basedate); |
||
2213 | $oldProps[$properties['duedate']] = $recurrence->getOccurrenceEnd($basedate); |
||
2214 | |||
2215 | $isExceptionAllowed = $recurrence->createException(Conversion::mapXML2MAPI($properties, $action['props']), $basedate, false, $exception_recips); |
||
2216 | } |
||
2217 | mapi_savechanges($message); |
||
2218 | } |
||
2219 | } |
||
2220 | } |
||
2221 | else { |
||
2222 | $oldProps = mapi_getprops($message, [$properties['startdate'], $properties['duedate']]); |
||
2223 | // Modifying non-exception (the series) or normal appointment item |
||
2224 | $message = $GLOBALS['operations']->saveMessage($store, $entryid, $parententryid, Conversion::mapXML2MAPI($properties, $action['props']), $messageProps, $recips ?: [], $action['attachments'] ?? [], [], false, false, false, false, false, false, $send); |
||
2225 | |||
2226 | $recurrenceProps = mapi_getprops($message, [$properties['startdate_recurring'], $properties['enddate_recurring'], $properties["recurring"]]); |
||
2227 | // Check if the meeting is recurring |
||
2228 | if ($recips && $recurrenceProps[$properties["recurring"]] && isset($recurrenceProps[$properties['startdate_recurring']], $recurrenceProps[$properties['enddate_recurring']])) { |
||
2229 | // If recipient of meeting is modified than that modification needs to be applied |
||
2230 | // to recurring exception as well, if any. |
||
2231 | $exception_recips = []; |
||
2232 | if (isset($recips['add'])) { |
||
2233 | $exception_recips['add'] = $this->createRecipientList($recips['add'], 'add', true, true); |
||
2234 | } |
||
2235 | if (isset($recips['remove'])) { |
||
2236 | $exception_recips['remove'] = $this->createRecipientList($recips['remove'], 'remove'); |
||
2237 | } |
||
2238 | if (isset($recips['modify'])) { |
||
2239 | $exception_recips['modify'] = $this->createRecipientList($recips['modify'], 'modify', true, true); |
||
2240 | } |
||
2241 | |||
2242 | // Create recurrence object |
||
2243 | $recurrence = new Recurrence($store, $message); |
||
2244 | |||
2245 | $recurItems = $recurrence->getItems($recurrenceProps[$properties['startdate_recurring']], $recurrenceProps[$properties['enddate_recurring']]); |
||
2246 | |||
2247 | foreach ($recurItems as $recurItem) { |
||
2248 | if (isset($recurItem["exception"])) { |
||
2249 | $recurrence->modifyException([], $recurItem["basedate"], $exception_recips); |
||
2250 | } |
||
2251 | } |
||
2252 | } |
||
2253 | |||
2254 | // Only save recurrence if it has been changed by the user (because otherwise we'll reset |
||
2255 | // the exceptions) |
||
2256 | if (isset($action['props']['recurring_reset']) && $action['props']['recurring_reset'] == true) { |
||
2257 | $recur = new Recurrence($store, $message); |
||
2258 | |||
2259 | if (isset($action['props']['timezone'])) { |
||
2260 | $tzprops = ['timezone', 'timezonedst', 'dststartmonth', 'dststartweek', 'dststartday', 'dststarthour', 'dstendmonth', 'dstendweek', 'dstendday', 'dstendhour']; |
||
2261 | |||
2262 | // Get timezone info |
||
2263 | $tz = []; |
||
2264 | foreach ($tzprops as $tzprop) { |
||
2265 | $tz[$tzprop] = $action['props'][$tzprop]; |
||
2266 | } |
||
2267 | } |
||
2268 | |||
2269 | /** |
||
2270 | * Check if any recurrence property is missing, if yes then prepare |
||
2271 | * the set of properties required to update the recurrence. For more info |
||
2272 | * please refer detailed description of parseRecurrence function of |
||
2273 | * BaseRecurrence class". |
||
2274 | * |
||
2275 | * Note : this is a special case of changing the time of |
||
2276 | * recurrence meeting from scheduling tab. |
||
2277 | */ |
||
2278 | $recurrence = $recur->getRecurrence(); |
||
2279 | if (isset($recurrence)) { |
||
2280 | unset($recurrence['changed_occurrences'], $recurrence['deleted_occurrences']); |
||
2281 | |||
2282 | foreach ($recurrence as $key => $value) { |
||
2283 | if (!isset($action['props'][$key])) { |
||
2284 | $action['props'][$key] = $value; |
||
2285 | } |
||
2286 | } |
||
2287 | } |
||
2288 | // Act like the 'props' are the recurrence pattern; it has more information but that |
||
2289 | // is ignored |
||
2290 | $recur->setRecurrence($tz ?? false, $action['props']); |
||
2291 | } |
||
2292 | } |
||
2293 | |||
2294 | // Get the properties of the main object of which the exception was changed, and post |
||
2295 | // that message as being modified. This will cause the update function to update all |
||
2296 | // occurrences of the item to the client |
||
2297 | $messageProps = mapi_getprops($message, [PR_ENTRYID, PR_PARENT_ENTRYID, PR_STORE_ENTRYID]); |
||
2298 | |||
2299 | // if opened appointment is exception then it will add |
||
2300 | // the attach_num and basedate in messageProps. |
||
2301 | if (isset($attach_num)) { |
||
2302 | $messageProps[PR_ATTACH_NUM] = [$attach_num]; |
||
2303 | $messageProps[$properties["basedate"]] = $action['basedate']; |
||
2304 | } |
||
2305 | } |
||
2306 | } |
||
2307 | else { |
||
2308 | $tz = null; |
||
2309 | $hasRecipient = false; |
||
2310 | $copyAttachments = false; |
||
2311 | $sourceRecord = false; |
||
2312 | if (isset($action['message_action'], $action['message_action']['source_entryid'])) { |
||
2313 | $sourceEntryId = $action['message_action']['source_entryid']; |
||
2314 | $sourceStoreEntryId = $action['message_action']['source_store_entryid']; |
||
2315 | |||
2316 | $sourceStore = $GLOBALS['mapisession']->openMessageStore(hex2bin((string) $sourceStoreEntryId)); |
||
2317 | $sourceRecord = mapi_msgstore_openentry($sourceStore, hex2bin($sourceEntryId)); |
||
2318 | if ($pasteRecord) { |
||
2319 | $sourceRecordProps = mapi_getprops($sourceRecord, [$properties["meeting"], $properties["responsestatus"]]); |
||
2320 | // Don't copy recipient if source record is received message. |
||
2321 | if ($sourceRecordProps[$properties["meeting"]] === olMeeting && |
||
2322 | $sourceRecordProps[$properties["meeting"]] === olResponseOrganized) { |
||
2323 | $table = mapi_message_getrecipienttable($sourceRecord); |
||
2324 | $hasRecipient = mapi_table_getrowcount($table) > 0; |
||
2325 | } |
||
2326 | } |
||
2327 | else { |
||
2328 | $copyAttachments = true; |
||
2329 | // Set sender of new Appointment. |
||
2330 | $this->setSenderAddress($store, $action); |
||
2331 | } |
||
2332 | } |
||
2333 | else { |
||
2334 | // Set sender of new Appointment. |
||
2335 | $this->setSenderAddress($store, $action); |
||
2336 | } |
||
2337 | |||
2338 | $message = $this->saveMessage($store, $entryid, $parententryid, Conversion::mapXML2MAPI($properties, $action['props']), $messageProps, $recips ?: [], $action['attachments'] ?? [], [], $sourceRecord, $copyAttachments, $hasRecipient, false, false, false, $send); |
||
2339 | |||
2340 | if (isset($action['props']['timezone'])) { |
||
2341 | $tzprops = ['timezone', 'timezonedst', 'dststartmonth', 'dststartweek', 'dststartday', 'dststarthour', 'dstendmonth', 'dstendweek', 'dstendday', 'dstendhour']; |
||
2342 | |||
2343 | // Get timezone info |
||
2344 | $tz = []; |
||
2345 | foreach ($tzprops as $tzprop) { |
||
2346 | $tz[$tzprop] = $action['props'][$tzprop]; |
||
2347 | } |
||
2348 | } |
||
2349 | |||
2350 | // Set recurrence |
||
2351 | if (isset($action['props']['recurring']) && $action['props']['recurring'] == true) { |
||
2352 | $recur = new Recurrence($store, $message); |
||
2353 | $recur->setRecurrence($tz, $action['props']); |
||
2354 | } |
||
2355 | } |
||
2356 | } |
||
2357 | |||
2358 | $result = false; |
||
2359 | // Check to see if it should be sent as a meeting request |
||
2360 | if ($send === true && $isExceptionAllowed) { |
||
2361 | $savedUnsavedRecipients = []; |
||
2362 | $remove = []; |
||
2363 | if (!isset($action['basedate'])) { |
||
2364 | // retrieve recipients from saved message |
||
2365 | $savedRecipients = $GLOBALS['operations']->getRecipientsInfo($message); |
||
2366 | foreach ($savedRecipients as $recipient) { |
||
2367 | $savedUnsavedRecipients["saved"][] = $recipient['props']; |
||
2368 | } |
||
2369 | |||
2370 | // retrieve removed recipients. |
||
2371 | if (!empty($recips) && !empty($recips["remove"])) { |
||
2372 | $remove = $recips["remove"]; |
||
2373 | } |
||
2374 | |||
2375 | // convert all local distribution list members to ideal recipient. |
||
2376 | $members = $this->convertLocalDistlistMembersToRecipients($savedUnsavedRecipients, $remove); |
||
2377 | |||
2378 | // Before sending meeting request we set the recipient to message |
||
2379 | // which are converted from local distribution list members. |
||
2380 | $this->setRecipients($message, $members); |
||
2381 | } |
||
2382 | |||
2383 | $request = new Meetingrequest($store, $message, $GLOBALS['mapisession']->getSession(), $directBookingMeetingRequest); |
||
2384 | |||
2385 | /* |
||
2386 | * check write access for delegate, make sure that we will not send meeting request |
||
2387 | * if we don't have permission to save calendar item |
||
2388 | */ |
||
2389 | if ($request->checkFolderWriteAccess($parententryid, $store) !== true) { |
||
2390 | // Throw an exception that we don't have write permissions on calendar folder, |
||
2391 | // error message will be filled by module |
||
2392 | throw new MAPIException(null, MAPI_E_NO_ACCESS); |
||
2393 | } |
||
2394 | |||
2395 | $request->updateMeetingRequest($basedate); |
||
2396 | |||
2397 | $isRecurrenceChanged = isset($action['props']['recurring_reset']) && $action['props']['recurring_reset'] == true; |
||
2398 | $request->checkSignificantChanges($oldProps, $basedate, $isRecurrenceChanged); |
||
2399 | |||
2400 | // Update extra body information |
||
2401 | if (isset($action['message_action']['meetingTimeInfo']) && !empty($action['message_action']['meetingTimeInfo'])) { |
||
2402 | // Append body if the request action requires this |
||
2403 | if (isset($action['message_action'], $action['message_action']['append_body'])) { |
||
2404 | $bodyProps = mapi_getprops($message, [PR_BODY]); |
||
2405 | if (isset($bodyProps[PR_BODY]) || propIsError(PR_BODY, $bodyProps) == MAPI_E_NOT_ENOUGH_MEMORY) { |
||
2406 | $bodyProps[PR_BODY] = streamProperty($message, PR_BODY); |
||
2407 | } |
||
2408 | |||
2409 | if (isset($action['message_action']['meetingTimeInfo'], $bodyProps[PR_BODY])) { |
||
2410 | $action['message_action']['meetingTimeInfo'] .= $bodyProps[PR_BODY]; |
||
2411 | } |
||
2412 | } |
||
2413 | |||
2414 | $request->setMeetingTimeInfo($action['message_action']['meetingTimeInfo']); |
||
2415 | unset($action['message_action']['meetingTimeInfo']); |
||
2416 | } |
||
2417 | |||
2418 | $modifiedRecipients = false; |
||
2419 | $deletedRecipients = false; |
||
2420 | if ($recips) { |
||
2421 | if (isset($action['message_action']['send_update']) && $action['message_action']['send_update'] == 'modified') { |
||
2422 | if (isset($recips['add']) && !empty($recips['add'])) { |
||
2423 | $modifiedRecipients = $modifiedRecipients ?: []; |
||
2424 | $modifiedRecipients = array_merge($modifiedRecipients, $this->createRecipientList($recips['add'], 'add')); |
||
2425 | } |
||
2426 | |||
2427 | if (isset($recips['modify']) && !empty($recips['modify'])) { |
||
2428 | $modifiedRecipients = $modifiedRecipients ?: []; |
||
2429 | $modifiedRecipients = array_merge($modifiedRecipients, $this->createRecipientList($recips['modify'], 'modify')); |
||
2430 | } |
||
2431 | } |
||
2432 | |||
2433 | // lastUpdateCounter is represent that how many times this message is updated(send) |
||
2434 | $lastUpdateCounter = $request->getLastUpdateCounter(); |
||
2435 | if ($lastUpdateCounter !== false && $lastUpdateCounter > 0) { |
||
2436 | if (isset($recips['remove']) && !empty($recips['remove'])) { |
||
2437 | $deletedRecipients = $deletedRecipients ?: []; |
||
2438 | $deletedRecipients = array_merge($deletedRecipients, $this->createRecipientList($recips['remove'], 'remove')); |
||
2439 | if (isset($action['message_action']['send_update']) && $action['message_action']['send_update'] != 'all') { |
||
2440 | $modifiedRecipients = $modifiedRecipients ?: []; |
||
2441 | } |
||
2442 | } |
||
2443 | } |
||
2444 | } |
||
2445 | |||
2446 | $sendMeetingRequestResult = $request->sendMeetingRequest($delete, false, $basedate, $modifiedRecipients, $deletedRecipients); |
||
2447 | |||
2448 | $this->addRecipientsToRecipientHistory($this->getRecipientsInfo($message)); |
||
2449 | |||
2450 | if ($sendMeetingRequestResult === true) { |
||
2451 | $this->parseDistListAndAddToRecipientHistory($savedUnsavedRecipients, $remove); |
||
2452 | |||
2453 | mapi_savechanges($message); |
||
2454 | |||
2455 | // We want to sent the 'request_sent' property, to have it properly |
||
2456 | // deserialized we must also send some type properties. |
||
2457 | $props = mapi_getprops($message, [PR_MESSAGE_CLASS, PR_OBJECT_TYPE]); |
||
2458 | $messageProps[PR_MESSAGE_CLASS] = $props[PR_MESSAGE_CLASS]; |
||
2459 | $messageProps[PR_OBJECT_TYPE] = $props[PR_OBJECT_TYPE]; |
||
2460 | |||
2461 | // Indicate that the message was correctly sent |
||
2462 | $messageProps[$properties['request_sent']] = true; |
||
2463 | |||
2464 | // Return message properties that can be sent to the bus to notify changes |
||
2465 | $result = $messageProps; |
||
2466 | } |
||
2467 | else { |
||
2468 | $sendMeetingRequestResult[PR_ENTRYID] = $messageProps[PR_ENTRYID]; |
||
2469 | $sendMeetingRequestResult[PR_PARENT_ENTRYID] = $messageProps[PR_PARENT_ENTRYID]; |
||
2470 | $sendMeetingRequestResult[PR_STORE_ENTRYID] = $messageProps[PR_STORE_ENTRYID]; |
||
2471 | $result = $sendMeetingRequestResult; |
||
2472 | } |
||
2473 | } |
||
2474 | else { |
||
2475 | mapi_savechanges($message); |
||
2476 | |||
2477 | if (isset($isExceptionAllowed)) { |
||
2478 | if ($isExceptionAllowed === false) { |
||
2479 | $messageProps['isexceptionallowed'] = false; |
||
2480 | } |
||
2481 | } |
||
2482 | |||
2483 | if (isset($isReminderTimeAllowed)) { |
||
2484 | if ($isReminderTimeAllowed === false) { |
||
2485 | $messageProps['remindertimeerror'] = false; |
||
2486 | } |
||
2487 | } |
||
2488 | // Return message properties that can be sent to the bus to notify changes |
||
2489 | $result = $messageProps; |
||
2490 | } |
||
2491 | |||
2492 | return $result; |
||
2493 | } |
||
2494 | |||
2495 | /** |
||
2496 | * Function is used to identify the local distribution list from all recipients and |
||
2497 | * convert all local distribution list members to recipients. |
||
2498 | * |
||
2499 | * @param array $recipients array of recipients either saved or add |
||
2500 | * @param array $remove array of recipients that was removed |
||
2501 | * |
||
2502 | * @return array $newRecipients a list of recipients as XML array structure |
||
2503 | */ |
||
2504 | public function convertLocalDistlistMembersToRecipients($recipients, $remove = []) { |
||
2505 | $addRecipients = []; |
||
2506 | $removeRecipients = []; |
||
2507 | |||
2508 | foreach ($recipients as $key => $recipient) { |
||
2509 | foreach ($recipient as $recipientItem) { |
||
2510 | $recipientEntryid = $recipientItem["entryid"]; |
||
2511 | $isExistInRemove = $this->isExistInRemove($recipientEntryid, $remove); |
||
2512 | |||
2513 | /* |
||
2514 | * Condition is only gets true, if recipient is distribution list and it`s belongs |
||
2515 | * to shared/internal(belongs in contact folder) folder. |
||
2516 | */ |
||
2517 | if ($recipientItem['object_type'] == MAPI_DISTLIST && $recipientItem['address_type'] != 'EX') { |
||
2518 | if (!$isExistInRemove) { |
||
2519 | $recipientItems = $GLOBALS["operations"]->expandDistList($recipientEntryid, true); |
||
2520 | foreach ($recipientItems as $recipient) { |
||
2521 | // set recipient type of each members as per the distribution list recipient type |
||
2522 | $recipient['recipient_type'] = $recipientItem['recipient_type']; |
||
2523 | array_push($addRecipients, $recipient); |
||
2524 | } |
||
2525 | |||
2526 | if ($key === "saved") { |
||
2527 | array_push($removeRecipients, $recipientItem); |
||
2528 | } |
||
2529 | } |
||
2530 | } |
||
2531 | else { |
||
2532 | /* |
||
2533 | * Only Add those recipients which are not saved earlier in message and |
||
2534 | * not present in remove array. |
||
2535 | */ |
||
2536 | if (!$isExistInRemove && $key === "unsaved") { |
||
2537 | array_push($addRecipients, $recipientItem); |
||
2538 | } |
||
2539 | } |
||
2540 | } |
||
2541 | } |
||
2542 | $newRecipients["add"] = $addRecipients; |
||
2543 | $newRecipients["remove"] = $removeRecipients; |
||
2544 | |||
2545 | return $newRecipients; |
||
2546 | } |
||
2547 | |||
2548 | /** |
||
2549 | * Function used to identify given recipient was already available in remove array of recipients array or not. |
||
2550 | * which was sent from client side. If it is found the entry in the $remove array will be deleted, since |
||
2551 | * we do not want to find it again for other recipients. (if a user removes and adds an user again it |
||
2552 | * should be added once!). |
||
2553 | * |
||
2554 | * @param string $recipientEntryid recipient entryid |
||
2555 | * @param array $remove removed recipients array |
||
2556 | * |
||
2557 | * @return bool return false if recipient not exist in remove array else return true |
||
2558 | */ |
||
2559 | public function isExistInRemove($recipientEntryid, &$remove) { |
||
2560 | if (!empty($remove)) { |
||
2561 | foreach ($remove as $index => $removeItem) { |
||
2562 | if (array_search($recipientEntryid, $removeItem, true)) { |
||
2563 | unset($remove[$index]); |
||
2564 | |||
2565 | return true; |
||
2566 | } |
||
2567 | } |
||
2568 | } |
||
2569 | |||
2570 | return false; |
||
2571 | } |
||
2572 | |||
2573 | /** |
||
2574 | * Function is used to identify the local distribution list from all recipients and |
||
2575 | * Add distribution list to recipient history. |
||
2576 | * |
||
2577 | * @param array $savedUnsavedRecipients array of recipients either saved or add |
||
2578 | * @param array $remove array of recipients that was removed |
||
2579 | */ |
||
2580 | public function parseDistListAndAddToRecipientHistory($savedUnsavedRecipients, $remove) { |
||
2581 | $distLists = []; |
||
2582 | foreach ($savedUnsavedRecipients as $key => $recipient) { |
||
2583 | foreach ($recipient as $recipientItem) { |
||
2584 | if ($recipientItem['address_type'] == 'MAPIPDL') { |
||
2585 | $isExistInRemove = $this->isExistInRemove($recipientItem['entryid'], $remove); |
||
2586 | if (!$isExistInRemove) { |
||
2587 | array_push($distLists, ["props" => $recipientItem]); |
||
2588 | } |
||
2589 | } |
||
2590 | } |
||
2591 | } |
||
2592 | |||
2593 | $this->addRecipientsToRecipientHistory($distLists); |
||
2594 | } |
||
2595 | |||
2596 | /** |
||
2597 | * Set sent_representing_email_address property of Appointment. |
||
2598 | * |
||
2599 | * Before saving any new appointment, sent_representing_email_address property of appointment |
||
2600 | * should contain email_address of user, who is the owner of store(in which the appointment |
||
2601 | * is created). |
||
2602 | * |
||
2603 | * @param mapistore $store MAPI store of the message |
||
2604 | * @param array $action reference to action array containing XML request |
||
2605 | */ |
||
2606 | public function setSenderAddress($store, &$action) { |
||
2607 | $storeProps = mapi_getprops($store, [PR_MAILBOX_OWNER_ENTRYID]); |
||
2608 | // check for public store |
||
2609 | if (!isset($storeProps[PR_MAILBOX_OWNER_ENTRYID])) { |
||
2610 | $store = $GLOBALS["mapisession"]->getDefaultMessageStore(); |
||
2611 | $storeProps = mapi_getprops($store, [PR_MAILBOX_OWNER_ENTRYID]); |
||
2612 | } |
||
2613 | $mailuser = mapi_ab_openentry($GLOBALS["mapisession"]->getAddressbook(), $storeProps[PR_MAILBOX_OWNER_ENTRYID]); |
||
2614 | if ($mailuser) { |
||
2615 | $userprops = mapi_getprops($mailuser, [PR_ADDRTYPE, PR_DISPLAY_NAME, PR_EMAIL_ADDRESS, PR_SMTP_ADDRESS]); |
||
2616 | $action["props"]["sent_representing_entryid"] = bin2hex((string) $storeProps[PR_MAILBOX_OWNER_ENTRYID]); |
||
2617 | // we do conversion here, because before passing props to saveMessage() props are converted from utf8-to-w |
||
2618 | $action["props"]["sent_representing_name"] = $userprops[PR_DISPLAY_NAME]; |
||
2619 | $action["props"]["sent_representing_address_type"] = $userprops[PR_ADDRTYPE]; |
||
2620 | if ($userprops[PR_ADDRTYPE] == 'SMTP') { |
||
2621 | $emailAddress = $userprops[PR_SMTP_ADDRESS]; |
||
2622 | } |
||
2623 | else { |
||
2624 | $emailAddress = $userprops[PR_EMAIL_ADDRESS]; |
||
2625 | } |
||
2626 | $action["props"]["sent_representing_email_address"] = $emailAddress; |
||
2627 | $action["props"]["sent_representing_search_key"] = bin2hex(strtoupper($userprops[PR_ADDRTYPE] . ':' . $emailAddress)) . '00'; |
||
2628 | } |
||
2629 | } |
||
2630 | |||
2631 | /** |
||
2632 | * Submit a message for sending. |
||
2633 | * |
||
2634 | * This function is an extension of the saveMessage() function, with the extra functionality |
||
2635 | * that the item is actually sent and queued for moving to 'Sent Items'. Also, the e-mail addresses |
||
2636 | * used in the message are processed for later auto-suggestion. |
||
2637 | * |
||
2638 | * @see Operations::saveMessage() for more information on the parameters, which are identical. |
||
2639 | * |
||
2640 | * @param mapistore $store MAPI Message Store Object |
||
2641 | * @param binary $entryid Entryid of the message |
||
2642 | * @param array $props The properties to be saved |
||
2643 | * @param array $messageProps reference to an array which will be filled with PR_ENTRYID, PR_PARENT_ENTRYID and PR_STORE_ENTRYID |
||
2644 | * @param array $recipients XML array structure of recipients for the recipient table |
||
2645 | * @param array $attachments array of attachments consisting unique ID of attachments for this message |
||
2646 | * @param MAPIMessage $copyFromMessage resource of the message from which we should |
||
2647 | * copy attachments and/or recipients to the current message |
||
2648 | * @param bool $copyAttachments if set we copy all attachments from the $copyFromMessage |
||
2649 | * @param bool $copyRecipients if set we copy all recipients from the $copyFromMessage |
||
2650 | * @param bool $copyInlineAttachmentsOnly if true then copy only inline attachments |
||
2651 | * @param bool $isPlainText if true then message body will be generated using PR_BODY otherwise PR_HTML will be used in saveMessage() function |
||
2652 | * |
||
2653 | * @return bool false if action succeeded, anything else indicates an error (e.g. a string) |
||
2654 | */ |
||
2655 | public function submitMessage($store, $entryid, $props, &$messageProps, $recipients = [], $attachments = [], $copyFromMessage = false, $copyAttachments = false, $copyRecipients = false, $copyInlineAttachmentsOnly = false, $isPlainText = false) { |
||
2656 | $message = false; |
||
2657 | $origStore = $store; |
||
2658 | $reprMessage = false; |
||
2659 | $delegateSentItemsStyle = $GLOBALS['settings']->get('zarafa/v1/contexts/mail/delegate_sent_items_style'); |
||
2660 | $saveBoth = strcasecmp((string) $delegateSentItemsStyle, 'both') == 0; |
||
2661 | $saveRepresentee = strcasecmp((string) $delegateSentItemsStyle, 'representee') == 0; |
||
2662 | $sendingAsDelegate = false; |
||
2663 | |||
2664 | // Get the outbox and sent mail entryid, ignore the given $store, use the default store for submitting messages |
||
2665 | $store = $GLOBALS["mapisession"]->getDefaultMessageStore(); |
||
2666 | $storeprops = mapi_getprops($store, [PR_IPM_OUTBOX_ENTRYID, PR_IPM_SENTMAIL_ENTRYID, PR_ENTRYID]); |
||
2667 | $origStoreprops = mapi_getprops($origStore, [PR_ENTRYID, PR_IPM_SENTMAIL_ENTRYID]); |
||
2668 | |||
2669 | if (!isset($storeprops[PR_IPM_OUTBOX_ENTRYID])) { |
||
2670 | return false; |
||
2671 | } |
||
2672 | if (isset($storeprops[PR_IPM_SENTMAIL_ENTRYID])) { |
||
2673 | $props[PR_SENTMAIL_ENTRYID] = $storeprops[PR_IPM_SENTMAIL_ENTRYID]; |
||
2674 | } |
||
2675 | |||
2676 | // Check if replying then set PR_INTERNET_REFERENCES and PR_IN_REPLY_TO_ID properties in props. |
||
2677 | // flag is probably used wrong here but the same flag indicates if this is reply or replyall |
||
2678 | if ($copyInlineAttachmentsOnly) { |
||
2679 | $origMsgProps = mapi_getprops($copyFromMessage, [PR_INTERNET_MESSAGE_ID, PR_INTERNET_REFERENCES]); |
||
2680 | if (isset($origMsgProps[PR_INTERNET_MESSAGE_ID])) { |
||
2681 | // The references header should indicate the message-id of the original |
||
2682 | // header plus any of the references which were set on the previous mail. |
||
2683 | $props[PR_INTERNET_REFERENCES] = $origMsgProps[PR_INTERNET_MESSAGE_ID]; |
||
2684 | if (isset($origMsgProps[PR_INTERNET_REFERENCES])) { |
||
2685 | $props[PR_INTERNET_REFERENCES] = $origMsgProps[PR_INTERNET_REFERENCES] . ' ' . $props[PR_INTERNET_REFERENCES]; |
||
2686 | } |
||
2687 | $props[PR_IN_REPLY_TO_ID] = $origMsgProps[PR_INTERNET_MESSAGE_ID]; |
||
2688 | } |
||
2689 | } |
||
2690 | |||
2691 | if (!$GLOBALS["entryid"]->compareEntryIds(bin2hex((string) $origStoreprops[PR_ENTRYID]), bin2hex((string) $storeprops[PR_ENTRYID]))) { |
||
2692 | // set properties for "on behalf of" mails |
||
2693 | $origStoreProps = mapi_getprops($origStore, [PR_MAILBOX_OWNER_ENTRYID, PR_MDB_PROVIDER, PR_IPM_SENTMAIL_ENTRYID]); |
||
2694 | |||
2695 | // set PR_SENDER_* properties, which contains currently logged user's data |
||
2696 | $ab = $GLOBALS['mapisession']->getAddressbook(); |
||
2697 | $abitem = mapi_ab_openentry($ab, $GLOBALS["mapisession"]->getUserEntryID()); |
||
2698 | $abitemprops = mapi_getprops($abitem, [PR_DISPLAY_NAME, PR_EMAIL_ADDRESS, PR_SEARCH_KEY]); |
||
2699 | |||
2700 | $props[PR_SENDER_ENTRYID] = $GLOBALS["mapisession"]->getUserEntryID(); |
||
2701 | $props[PR_SENDER_NAME] = $abitemprops[PR_DISPLAY_NAME]; |
||
2702 | $props[PR_SENDER_EMAIL_ADDRESS] = $abitemprops[PR_EMAIL_ADDRESS]; |
||
2703 | $props[PR_SENDER_ADDRTYPE] = "EX"; |
||
2704 | $props[PR_SENDER_SEARCH_KEY] = $abitemprops[PR_SEARCH_KEY]; |
||
2705 | |||
2706 | // Use the PR_SENT_REPRESENTING_* properties sent by the client or set to the currently logged user's data |
||
2707 | $props[PR_SENT_REPRESENTING_ENTRYID] = $props[PR_SENT_REPRESENTING_ENTRYID] ?? $props[PR_SENDER_ENTRYID]; |
||
2708 | $props[PR_SENT_REPRESENTING_NAME] = $props[PR_SENT_REPRESENTING_NAME] ?? $props[PR_SENDER_NAME]; |
||
2709 | $props[PR_SENT_REPRESENTING_EMAIL_ADDRESS] = $props[PR_SENT_REPRESENTING_EMAIL_ADDRESS] ?? $props[PR_SENDER_EMAIL_ADDRESS]; |
||
2710 | $props[PR_SENT_REPRESENTING_ADDRTYPE] = $props[PR_SENT_REPRESENTING_ADDRTYPE] ?? $props[PR_SENDER_ADDRTYPE]; |
||
2711 | $props[PR_SENT_REPRESENTING_SEARCH_KEY] = $props[PR_SENT_REPRESENTING_SEARCH_KEY] ?? $props[PR_SENDER_SEARCH_KEY]; |
||
2712 | /** |
||
2713 | * we are sending mail from delegate's account, so we can't use delegate's outbox and sent items folder |
||
2714 | * so we have to copy the mail from delegate's store to logged user's store and in outbox folder and then |
||
2715 | * we can send mail from logged user's outbox folder. |
||
2716 | * |
||
2717 | * if we set $entryid to false before passing it to saveMessage function then it will assume |
||
2718 | * that item doesn't exist and it will create a new item (in outbox of logged in user) |
||
2719 | */ |
||
2720 | if ($entryid) { |
||
2721 | $oldEntryId = $entryid; |
||
2722 | $entryid = false; |
||
2723 | |||
2724 | // if we are sending mail from drafts folder then we have to copy |
||
2725 | // its recipients and attachments also. $origStore and $oldEntryId points to mail |
||
2726 | // saved in delegators draft folder |
||
2727 | if ($copyFromMessage === false) { |
||
2728 | $copyFromMessage = mapi_msgstore_openentry($origStore, $oldEntryId); |
||
2729 | $copyRecipients = true; |
||
2730 | |||
2731 | // Decode smime signed messages on this message |
||
2732 | parse_smime($origStore, $copyFromMessage); |
||
2733 | } |
||
2734 | } |
||
2735 | |||
2736 | if ($copyFromMessage) { |
||
2737 | // Get properties of original message, to copy recipients and attachments in new message |
||
2738 | $copyMessageProps = mapi_getprops($copyFromMessage); |
||
2739 | $oldParentEntryId = $copyMessageProps[PR_PARENT_ENTRYID]; |
||
2740 | |||
2741 | // unset id properties before merging the props, so we will be creating new item instead of sending same item |
||
2742 | unset($copyMessageProps[PR_ENTRYID], $copyMessageProps[PR_PARENT_ENTRYID], $copyMessageProps[PR_STORE_ENTRYID], $copyMessageProps[PR_SEARCH_KEY]); |
||
2743 | |||
2744 | // grommunio generates PR_HTML on the fly, but it's necessary to unset it |
||
2745 | // if the original message didn't have PR_HTML property. |
||
2746 | if (!isset($props[PR_HTML]) && isset($copyMessageProps[PR_HTML])) { |
||
2747 | unset($copyMessageProps[PR_HTML]); |
||
2748 | } |
||
2749 | /* New EMAIL_ADDRESSes were set (various cases above), kill off old SMTP_ADDRESS. */ |
||
2750 | unset($copyMessageProps[PR_SENDER_SMTP_ADDRESS], $copyMessageProps[PR_SENT_REPRESENTING_SMTP_ADDRESS]); |
||
2751 | |||
2752 | // Merge original message props with props sent by client |
||
2753 | $props = $props + $copyMessageProps; |
||
2754 | } |
||
2755 | |||
2756 | // Save the new message properties |
||
2757 | $message = $this->saveMessage($store, $entryid, $storeprops[PR_IPM_OUTBOX_ENTRYID], $props, $messageProps, $recipients, $attachments, [], $copyFromMessage, $copyAttachments, $copyRecipients, $copyInlineAttachmentsOnly, true, true, $isPlainText); |
||
2758 | |||
2759 | // FIXME: currently message is deleted from original store and new message is created |
||
2760 | // in current user's store, but message should be moved |
||
2761 | |||
2762 | // delete message from it's original location |
||
2763 | if (!empty($oldEntryId) && !empty($oldParentEntryId)) { |
||
2764 | $folder = mapi_msgstore_openentry($origStore, $oldParentEntryId); |
||
2765 | mapi_folder_deletemessages($folder, [$oldEntryId], DELETE_HARD_DELETE); |
||
2766 | } |
||
2767 | if ($saveBoth || $saveRepresentee) { |
||
2768 | if ($origStoreProps[PR_MDB_PROVIDER] === ZARAFA_STORE_PUBLIC_GUID) { |
||
2769 | $userEntryid = $GLOBALS["mapisession"]->getStoreEntryIdOfUser(strtolower($props[PR_SENT_REPRESENTING_EMAIL_ADDRESS])); |
||
2770 | $origStore = $GLOBALS["mapisession"]->openMessageStore($userEntryid); |
||
2771 | $origStoreprops = mapi_getprops($origStore, [PR_IPM_SENTMAIL_ENTRYID]); |
||
2772 | } |
||
2773 | $destfolder = mapi_msgstore_openentry($origStore, $origStoreprops[PR_IPM_SENTMAIL_ENTRYID]); |
||
2774 | $reprMessage = mapi_folder_createmessage($destfolder); |
||
2775 | mapi_copyto($message, [], [], $reprMessage, 0); |
||
2776 | } |
||
2777 | } |
||
2778 | else { |
||
2779 | // 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. |
||
2780 | $outbox = mapi_msgstore_openentry($store, $storeprops[PR_IPM_OUTBOX_ENTRYID]); |
||
2781 | |||
2782 | // Open the old and the new message |
||
2783 | $newmessage = mapi_folder_createmessage($outbox); |
||
2784 | $oldEntryId = $entryid; |
||
2785 | |||
2786 | // Remember the new entryid |
||
2787 | $newprops = mapi_getprops($newmessage, [PR_ENTRYID]); |
||
2788 | $entryid = $newprops[PR_ENTRYID]; |
||
2789 | |||
2790 | if (!empty($oldEntryId)) { |
||
2791 | $message = mapi_msgstore_openentry($store, $oldEntryId); |
||
2792 | // Copy the entire message |
||
2793 | mapi_copyto($message, [], [], $newmessage); |
||
2794 | $tmpProps = mapi_getprops($message); |
||
2795 | $oldParentEntryId = $tmpProps[PR_PARENT_ENTRYID]; |
||
2796 | if ($storeprops[PR_IPM_OUTBOX_ENTRYID] == $oldParentEntryId) { |
||
2797 | $folder = $outbox; |
||
2798 | } |
||
2799 | else { |
||
2800 | $folder = mapi_msgstore_openentry($store, $oldParentEntryId); |
||
2801 | } |
||
2802 | |||
2803 | // Copy message_class for S/MIME plugin |
||
2804 | if (isset($tmpProps[PR_MESSAGE_CLASS])) { |
||
2805 | $props[PR_MESSAGE_CLASS] = $tmpProps[PR_MESSAGE_CLASS]; |
||
2806 | } |
||
2807 | // Delete the old message |
||
2808 | mapi_folder_deletemessages($folder, [$oldEntryId]); |
||
2809 | } |
||
2810 | |||
2811 | // save changes to new message created in outbox |
||
2812 | mapi_savechanges($newmessage); |
||
2813 | |||
2814 | $reprProps = mapi_getprops($newmessage, [PR_SENT_REPRESENTING_EMAIL_ADDRESS, PR_SENDER_EMAIL_ADDRESS, PR_SENT_REPRESENTING_ENTRYID]); |
||
2815 | if (isset($reprProps[PR_SENT_REPRESENTING_EMAIL_ADDRESS], $reprProps[PR_SENDER_EMAIL_ADDRESS], $reprProps[PR_SENT_REPRESENTING_ENTRYID]) && |
||
2816 | strcasecmp((string) $reprProps[PR_SENT_REPRESENTING_EMAIL_ADDRESS], (string) $reprProps[PR_SENDER_EMAIL_ADDRESS]) != 0) { |
||
2817 | $ab = $GLOBALS['mapisession']->getAddressbook(); |
||
2818 | $abitem = mapi_ab_openentry($ab, $reprProps[PR_SENT_REPRESENTING_ENTRYID]); |
||
2819 | $abitemprops = mapi_getprops($abitem, [PR_DISPLAY_NAME, PR_EMAIL_ADDRESS, PR_SEARCH_KEY]); |
||
2820 | |||
2821 | $props[PR_SENT_REPRESENTING_NAME] = $abitemprops[PR_DISPLAY_NAME]; |
||
2822 | $props[PR_SENT_REPRESENTING_EMAIL_ADDRESS] = $abitemprops[PR_EMAIL_ADDRESS]; |
||
2823 | $props[PR_SENT_REPRESENTING_ADDRTYPE] = "EX"; |
||
2824 | $props[PR_SENT_REPRESENTING_SEARCH_KEY] = $abitemprops[PR_SEARCH_KEY]; |
||
2825 | $sendingAsDelegate = true; |
||
2826 | } |
||
2827 | // Save the new message properties |
||
2828 | $message = $this->saveMessage($store, $entryid, $storeprops[PR_IPM_OUTBOX_ENTRYID], $props, $messageProps, $recipients, $attachments, [], $copyFromMessage, $copyAttachments, $copyRecipients, $copyInlineAttachmentsOnly, true, true, $isPlainText); |
||
2829 | // Sending as delegate from drafts folder |
||
2830 | if ($sendingAsDelegate && ($saveBoth || $saveRepresentee)) { |
||
2831 | $userEntryid = $GLOBALS["mapisession"]->getStoreEntryIdOfUser(strtolower($props[PR_SENT_REPRESENTING_EMAIL_ADDRESS])); |
||
2832 | $origStore = $GLOBALS["mapisession"]->openMessageStore($userEntryid); |
||
2833 | if ($origStore) { |
||
2834 | $origStoreprops = mapi_getprops($origStore, [PR_ENTRYID, PR_IPM_SENTMAIL_ENTRYID]); |
||
2835 | $destfolder = mapi_msgstore_openentry($origStore, $origStoreprops[PR_IPM_SENTMAIL_ENTRYID]); |
||
2836 | $reprMessage = mapi_folder_createmessage($destfolder); |
||
2837 | mapi_copyto($message, [], [], $reprMessage, 0); |
||
2838 | } |
||
2839 | } |
||
2840 | } |
||
2841 | |||
2842 | if (!$message) { |
||
2843 | return false; |
||
2844 | } |
||
2845 | // Allowing to hook in just before the data sent away to be sent to the client |
||
2846 | $GLOBALS['PluginManager']->triggerHook('server.core.operations.submitmessage', [ |
||
2847 | 'moduleObject' => $this, |
||
2848 | 'store' => $store, |
||
2849 | 'entryid' => $entryid, |
||
2850 | 'message' => &$message, |
||
2851 | ]); |
||
2852 | |||
2853 | // Submit the message (send) |
||
2854 | try { |
||
2855 | mapi_message_submitmessage($message); |
||
2856 | } |
||
2857 | catch (MAPIException $e) { |
||
2858 | $username = $GLOBALS["mapisession"]->getUserName(); |
||
2859 | $errorName = get_mapi_error_name($e->getCode()); |
||
2860 | error_log(sprintf( |
||
2861 | 'Unable to submit message for %s, MAPI error: %s. ' . |
||
2862 | 'SMTP server may be down or it refused the message or the message' . |
||
2863 | ' is too large to submit or user does not have the permission ...', |
||
2864 | $username, |
||
2865 | $errorName |
||
2866 | )); |
||
2867 | |||
2868 | return $errorName; |
||
2869 | } |
||
2870 | |||
2871 | $tmp_props = mapi_getprops($message, [PR_PARENT_ENTRYID, PR_MESSAGE_DELIVERY_TIME, PR_CLIENT_SUBMIT_TIME, PR_SEARCH_KEY, PR_MESSAGE_FLAGS]); |
||
2872 | $messageProps[PR_PARENT_ENTRYID] = $tmp_props[PR_PARENT_ENTRYID]; |
||
2873 | if ($reprMessage !== false) { |
||
2874 | mapi_setprops($reprMessage, [ |
||
2875 | PR_CLIENT_SUBMIT_TIME => $tmp_props[PR_CLIENT_SUBMIT_TIME] ?? time(), |
||
2876 | PR_MESSAGE_DELIVERY_TIME => $tmp_props[PR_MESSAGE_DELIVERY_TIME] ?? time(), |
||
2877 | PR_MESSAGE_FLAGS => $tmp_props[PR_MESSAGE_FLAGS] | MSGFLAG_READ, |
||
2878 | ]); |
||
2879 | mapi_savechanges($reprMessage); |
||
2880 | if ($saveRepresentee) { |
||
2881 | // delete the message in the delegate's Sent Items folder |
||
2882 | $sentFolder = mapi_msgstore_openentry($store, $storeprops[PR_IPM_SENTMAIL_ENTRYID]); |
||
2883 | $sentTable = mapi_folder_getcontentstable($sentFolder, MAPI_DEFERRED_ERRORS); |
||
2884 | $restriction = [RES_PROPERTY, [ |
||
2885 | RELOP => RELOP_EQ, |
||
2886 | ULPROPTAG => PR_SEARCH_KEY, |
||
2887 | VALUE => $tmp_props[PR_SEARCH_KEY], |
||
2888 | ]]; |
||
2889 | mapi_table_restrict($sentTable, $restriction); |
||
2890 | $sentMessageProps = mapi_table_queryallrows($sentTable, [PR_ENTRYID, PR_SEARCH_KEY]); |
||
2891 | if (mapi_table_getrowcount($sentTable) == 1) { |
||
2892 | mapi_folder_deletemessages($sentFolder, [$sentMessageProps[0][PR_ENTRYID]], DELETE_HARD_DELETE); |
||
2893 | } |
||
2894 | else { |
||
2895 | error_log(sprintf( |
||
2896 | "Found multiple entries in Sent Items with the same PR_SEARCH_KEY (%d)." . |
||
2897 | " Impossible to delete email from the delegate's Sent Items folder.", |
||
2898 | mapi_table_getrowcount($sentTable) |
||
2899 | )); |
||
2900 | } |
||
2901 | } |
||
2902 | } |
||
2903 | |||
2904 | $this->addRecipientsToRecipientHistory($this->getRecipientsInfo($message)); |
||
2905 | |||
2906 | return false; |
||
2907 | } |
||
2908 | |||
2909 | /** |
||
2910 | * Delete messages. |
||
2911 | * |
||
2912 | * This function does what is needed when a user presses 'delete' on a MAPI message. This means that: |
||
2913 | * |
||
2914 | * - Items in the own store are moved to the wastebasket |
||
2915 | * - Items in the wastebasket are deleted |
||
2916 | * - Items in other users stores are moved to our own wastebasket |
||
2917 | * - Items in the public store are deleted |
||
2918 | * |
||
2919 | * @param mapistore $store MAPI Message Store Object |
||
2920 | * @param string $parententryid parent entryid of the messages to be deleted |
||
2921 | * @param array $entryids a list of entryids which will be deleted |
||
2922 | * @param bool $softDelete flag for soft-deleteing (when user presses Shift+Del) |
||
2923 | * @param bool $unread message is unread |
||
2924 | * |
||
2925 | * @return bool true if action succeeded, false if not |
||
2926 | */ |
||
2927 | public function deleteMessages($store, $parententryid, $entryids, $softDelete = false, $unread = false) { |
||
2928 | $result = false; |
||
2929 | if (!is_array($entryids)) { |
||
2930 | $entryids = [$entryids]; |
||
2931 | } |
||
2932 | |||
2933 | $folder = mapi_msgstore_openentry($store, $parententryid); |
||
2934 | $flags = $unread ? GX_DELMSG_NOTIFY_UNREAD : 0; |
||
2935 | $msgprops = mapi_getprops($store, [PR_IPM_WASTEBASKET_ENTRYID, PR_MDB_PROVIDER, PR_IPM_OUTBOX_ENTRYID]); |
||
2936 | |||
2937 | switch ($msgprops[PR_MDB_PROVIDER]) { |
||
2938 | case ZARAFA_STORE_DELEGATE_GUID: |
||
2939 | $softDelete = $softDelete || defined('ENABLE_DEFAULT_SOFT_DELETE') ? ENABLE_DEFAULT_SOFT_DELETE : false; |
||
2940 | // with a store from an other user we need our own waste basket... |
||
2941 | if (isset($msgprops[PR_IPM_WASTEBASKET_ENTRYID]) && $msgprops[PR_IPM_WASTEBASKET_ENTRYID] == $parententryid || $softDelete) { |
||
2942 | // except when it is the waste basket itself |
||
2943 | $result = mapi_folder_deletemessages($folder, $entryids, $flags); |
||
2944 | break; |
||
2945 | } |
||
2946 | $defaultstore = $GLOBALS["mapisession"]->getDefaultMessageStore(); |
||
2947 | $msgprops = mapi_getprops($defaultstore, [PR_IPM_WASTEBASKET_ENTRYID, PR_MDB_PROVIDER]); |
||
2948 | |||
2949 | if (!isset($msgprops[PR_IPM_WASTEBASKET_ENTRYID]) || |
||
2950 | $msgprops[PR_IPM_WASTEBASKET_ENTRYID] == $parententryid) { |
||
2951 | $result = mapi_folder_deletemessages($folder, $entryids, $flags); |
||
2952 | break; |
||
2953 | } |
||
2954 | |||
2955 | try { |
||
2956 | $result = $this->copyMessages($store, $parententryid, $defaultstore, $msgprops[PR_IPM_WASTEBASKET_ENTRYID], $entryids, [], true); |
||
2957 | } |
||
2958 | catch (MAPIException $e) { |
||
2959 | $e->setHandled(); |
||
2960 | // if moving fails, try normal delete |
||
2961 | $result = mapi_folder_deletemessages($folder, $entryids, $flags); |
||
2962 | } |
||
2963 | break; |
||
2964 | |||
2965 | case ZARAFA_STORE_ARCHIVER_GUID: |
||
2966 | case ZARAFA_STORE_PUBLIC_GUID: |
||
2967 | // always delete in public store and archive store |
||
2968 | $result = mapi_folder_deletemessages($folder, $entryids, $flags); |
||
2969 | break; |
||
2970 | |||
2971 | case ZARAFA_SERVICE_GUID: |
||
2972 | // delete message when in your own waste basket, else move it to the waste basket |
||
2973 | if (isset($msgprops[PR_IPM_WASTEBASKET_ENTRYID]) && $msgprops[PR_IPM_WASTEBASKET_ENTRYID] == $parententryid || $softDelete === true) { |
||
2974 | $result = mapi_folder_deletemessages($folder, $entryids, $flags); |
||
2975 | break; |
||
2976 | } |
||
2977 | |||
2978 | try { |
||
2979 | // if the message is deleting from outbox then first delete the |
||
2980 | // message from an outgoing queue. |
||
2981 | if (function_exists("mapi_msgstore_abortsubmit") && isset($msgprops[PR_IPM_OUTBOX_ENTRYID]) && $msgprops[PR_IPM_OUTBOX_ENTRYID] === $parententryid) { |
||
2982 | foreach ($entryids as $entryid) { |
||
2983 | $message = mapi_msgstore_openentry($store, $entryid); |
||
2984 | $messageProps = mapi_getprops($message, [PR_DEFERRED_SEND_TIME]); |
||
2985 | if (isset($messageProps[PR_DEFERRED_SEND_TIME])) { |
||
2986 | mapi_msgstore_abortsubmit($store, $entryid); |
||
2987 | } |
||
2988 | } |
||
2989 | } |
||
2990 | $result = $this->copyMessages($store, $parententryid, $store, $msgprops[PR_IPM_WASTEBASKET_ENTRYID], $entryids, [], true); |
||
2991 | } |
||
2992 | catch (MAPIException $e) { |
||
2993 | if ($e->getCode() === MAPI_E_NOT_IN_QUEUE || $e->getCode() === MAPI_E_UNABLE_TO_ABORT) { |
||
2994 | throw $e; |
||
2995 | } |
||
2996 | |||
2997 | $e->setHandled(); |
||
2998 | // if moving fails, try normal delete |
||
2999 | $result = mapi_folder_deletemessages($folder, $entryids, $flags); |
||
3000 | } |
||
3001 | break; |
||
3002 | } |
||
3003 | |||
3004 | return $result; |
||
3005 | } |
||
3006 | |||
3007 | /** |
||
3008 | * Copy or move messages. |
||
3009 | * |
||
3010 | * @param object $store MAPI Message Store Object |
||
3011 | * @param string $parententryid parent entryid of the messages |
||
3012 | * @param string $destentryid destination folder |
||
3013 | * @param array $entryids a list of entryids which will be copied or moved |
||
3014 | * @param array $ignoreProps a list of proptags which should not be copied over |
||
3015 | * to the new message |
||
3016 | * @param bool $moveMessages true - move messages, false - copy messages |
||
3017 | * @param array $props a list of proptags which should set in new messages |
||
3018 | * @param mixed $destStore |
||
3019 | * |
||
3020 | * @return bool true if action succeeded, false if not |
||
3021 | */ |
||
3022 | public function copyMessages($store, $parententryid, $destStore, $destentryid, $entryids, $ignoreProps, $moveMessages, $props = []) { |
||
3023 | $sourcefolder = mapi_msgstore_openentry($store, $parententryid); |
||
3024 | $destfolder = mapi_msgstore_openentry($destStore, $destentryid); |
||
3025 | |||
3026 | if (!$sourcefolder || !$destfolder) { |
||
3027 | error_log("Could not open source or destination folder. Aborting."); |
||
3028 | |||
3029 | return false; |
||
3030 | } |
||
3031 | |||
3032 | if (!is_array($entryids)) { |
||
3033 | $entryids = [$entryids]; |
||
3034 | } |
||
3035 | |||
3036 | /* |
||
3037 | * If there are no properties to ignore as well as set then we can use mapi_folder_copymessages instead |
||
3038 | * of mapi_copyto. mapi_folder_copymessages is much faster then copyto since it executes |
||
3039 | * the copying on the server instead of in client. |
||
3040 | */ |
||
3041 | if (empty($ignoreProps) && empty($props)) { |
||
3042 | try { |
||
3043 | mapi_folder_copymessages($sourcefolder, $entryids, $destfolder, $moveMessages ? MESSAGE_MOVE : 0); |
||
3044 | } |
||
3045 | catch (MAPIException) { |
||
3046 | error_log(sprintf("mapi_folder_copymessages failed with code: 0x%08X. Wait 250ms and try again", mapi_last_hresult())); |
||
3047 | // wait 250ms before trying again |
||
3048 | usleep(250000); |
||
3049 | |||
3050 | try { |
||
3051 | mapi_folder_copymessages($sourcefolder, $entryids, $destfolder, $moveMessages ? MESSAGE_MOVE : 0); |
||
3052 | } |
||
3053 | catch (MAPIException) { |
||
3054 | error_log(sprintf("2nd attempt of mapi_folder_copymessages also failed with code: 0x%08X. Abort.", mapi_last_hresult())); |
||
3055 | |||
3056 | return false; |
||
3057 | } |
||
3058 | } |
||
3059 | } |
||
3060 | else { |
||
3061 | foreach ($entryids as $entryid) { |
||
3062 | $oldmessage = mapi_msgstore_openentry($store, $entryid); |
||
3063 | $newmessage = mapi_folder_createmessage($destfolder); |
||
3064 | |||
3065 | mapi_copyto($oldmessage, [], $ignoreProps, $newmessage, 0); |
||
3066 | if (!empty($props)) { |
||
3067 | mapi_setprops($newmessage, $props); |
||
3068 | } |
||
3069 | mapi_savechanges($newmessage); |
||
3070 | } |
||
3071 | if ($moveMessages) { |
||
3072 | // while moving message we actually copy that particular message into |
||
3073 | // destination folder, and remove it from source folder. so we must have |
||
3074 | // to hard delete the message. |
||
3075 | mapi_folder_deletemessages($sourcefolder, $entryids, DELETE_HARD_DELETE); |
||
3076 | } |
||
3077 | } |
||
3078 | |||
3079 | return true; |
||
3080 | } |
||
3081 | |||
3082 | /** |
||
3083 | * Set message read flag. |
||
3084 | * |
||
3085 | * @param object $store MAPI Message Store Object |
||
3086 | * @param string $entryid entryid of the message |
||
3087 | * @param int $flags Bitmask of values (read, has attachment etc.) |
||
3088 | * @param array $props properties of the message |
||
3089 | * @param mixed $msg_action |
||
3090 | * |
||
3091 | * @return bool true if action succeeded, false if not |
||
3092 | */ |
||
3093 | public function setMessageFlag($store, $entryid, $flags, $msg_action = false, &$props = false) { |
||
3094 | $message = $this->openMessage($store, $entryid); |
||
3095 | |||
3096 | if ($message) { |
||
3097 | /** |
||
3098 | * convert flags of PR_MESSAGE_FLAGS property to flags that is |
||
3099 | * used in mapi_message_setreadflag. |
||
3100 | */ |
||
3101 | $flag = MAPI_DEFERRED_ERRORS; // set unread flag, read receipt will be sent |
||
3102 | |||
3103 | if (($flags & MSGFLAG_RN_PENDING) && isset($msg_action['send_read_receipt']) && $msg_action['send_read_receipt'] == false) { |
||
3104 | $flag |= SUPPRESS_RECEIPT; |
||
3105 | } |
||
3106 | else { |
||
3107 | if (!($flags & MSGFLAG_READ)) { |
||
3108 | $flag |= CLEAR_READ_FLAG; |
||
3109 | } |
||
3110 | } |
||
3111 | |||
3112 | mapi_message_setreadflag($message, $flag); |
||
3113 | |||
3114 | if (is_array($props)) { |
||
3115 | $props = mapi_getprops($message, [PR_ENTRYID, PR_STORE_ENTRYID, PR_PARENT_ENTRYID]); |
||
3116 | } |
||
3117 | } |
||
3118 | |||
3119 | return true; |
||
3120 | } |
||
3121 | |||
3122 | /** |
||
3123 | * Create a unique folder name based on a provided new folder name. |
||
3124 | * |
||
3125 | * checkFolderNameConflict() checks if a folder name conflict is caused by the given $foldername. |
||
3126 | * This function is used for copying of moving a folder to another folder. It returns |
||
3127 | * a unique foldername. |
||
3128 | * |
||
3129 | * @param object $store MAPI Message Store Object |
||
3130 | * @param object $folder MAPI Folder Object |
||
3131 | * @param string $foldername the folder name |
||
3132 | * |
||
3133 | * @return string correct foldername |
||
3134 | */ |
||
3135 | public function checkFolderNameConflict($store, $folder, $foldername) { |
||
3136 | $folderNames = []; |
||
3137 | |||
3138 | $hierarchyTable = mapi_folder_gethierarchytable($folder, MAPI_DEFERRED_ERRORS); |
||
3139 | mapi_table_sort($hierarchyTable, [PR_DISPLAY_NAME => TABLE_SORT_ASCEND], TBL_BATCH); |
||
3140 | |||
3141 | $subfolders = mapi_table_queryallrows($hierarchyTable, [PR_ENTRYID]); |
||
3142 | |||
3143 | if (is_array($subfolders)) { |
||
3144 | foreach ($subfolders as $subfolder) { |
||
3145 | $folderObject = mapi_msgstore_openentry($store, $subfolder[PR_ENTRYID]); |
||
3146 | $folderProps = mapi_getprops($folderObject, [PR_DISPLAY_NAME]); |
||
3147 | |||
3148 | array_push($folderNames, strtolower((string) $folderProps[PR_DISPLAY_NAME])); |
||
3149 | } |
||
3150 | } |
||
3151 | |||
3152 | if (array_search(strtolower($foldername), $folderNames) !== false) { |
||
3153 | $i = 2; |
||
3154 | while (array_search(strtolower($foldername) . " ({$i})", $folderNames) !== false) { |
||
3155 | ++$i; |
||
3156 | } |
||
3157 | $foldername .= " ({$i})"; |
||
3158 | } |
||
3159 | |||
3160 | return $foldername; |
||
3161 | } |
||
3162 | |||
3163 | /** |
||
3164 | * Set the recipients of a MAPI message. |
||
3165 | * |
||
3166 | * @param object $message MAPI Message Object |
||
3167 | * @param array $recipients XML array structure of recipients |
||
3168 | * @param bool $send true if we are going to send this message else false |
||
3169 | */ |
||
3170 | public function setRecipients($message, $recipients, $send = false) { |
||
3171 | if (empty($recipients)) { |
||
3172 | // no recipients are sent from client |
||
3173 | return; |
||
3174 | } |
||
3175 | |||
3176 | $newRecipients = []; |
||
3177 | $removeRecipients = []; |
||
3178 | $modifyRecipients = []; |
||
3179 | |||
3180 | if (isset($recipients['add']) && !empty($recipients['add'])) { |
||
3181 | $newRecipients = $this->createRecipientList($recipients['add'], 'add', false, $send); |
||
3182 | } |
||
3183 | |||
3184 | if (isset($recipients['remove']) && !empty($recipients['remove'])) { |
||
3185 | $removeRecipients = $this->createRecipientList($recipients['remove'], 'remove'); |
||
3186 | } |
||
3187 | |||
3188 | if (isset($recipients['modify']) && !empty($recipients['modify'])) { |
||
3189 | $modifyRecipients = $this->createRecipientList($recipients['modify'], 'modify', false, $send); |
||
3190 | } |
||
3191 | |||
3192 | if (!empty($removeRecipients)) { |
||
3193 | mapi_message_modifyrecipients($message, MODRECIP_REMOVE, $removeRecipients); |
||
3194 | } |
||
3195 | |||
3196 | if (!empty($modifyRecipients)) { |
||
3197 | mapi_message_modifyrecipients($message, MODRECIP_MODIFY, $modifyRecipients); |
||
3198 | } |
||
3199 | |||
3200 | if (!empty($newRecipients)) { |
||
3201 | mapi_message_modifyrecipients($message, MODRECIP_ADD, $newRecipients); |
||
3202 | } |
||
3203 | } |
||
3204 | |||
3205 | /** |
||
3206 | * Copy recipients from original message. |
||
3207 | * |
||
3208 | * If we are sending mail from a delegator's folder, we need to copy all recipients from the original message |
||
3209 | * |
||
3210 | * @param object $message MAPI Message Object |
||
3211 | * @param MAPIMessage $copyFromMessage If set we copy all recipients from this message |
||
3212 | */ |
||
3213 | public function copyRecipients($message, $copyFromMessage = false) { |
||
3214 | $recipienttable = mapi_message_getrecipienttable($copyFromMessage); |
||
3215 | $messageRecipients = mapi_table_queryallrows($recipienttable, $GLOBALS["properties"]->getRecipientProperties()); |
||
3216 | if (!empty($messageRecipients)) { |
||
3217 | mapi_message_modifyrecipients($message, MODRECIP_ADD, $messageRecipients); |
||
3218 | } |
||
3219 | } |
||
3220 | |||
3221 | /** |
||
3222 | * Set attachments in a MAPI message. |
||
3223 | * |
||
3224 | * This function reads any attachments that have been previously uploaded and copies them into |
||
3225 | * the passed MAPI message resource. For a description of the dialog_attachments variable and |
||
3226 | * generally how attachments work when uploading, see Operations::saveMessage() |
||
3227 | * |
||
3228 | * @see Operations::saveMessage() |
||
3229 | * |
||
3230 | * @param object $message MAPI Message Object |
||
3231 | * @param array $attachments XML array structure of attachments |
||
3232 | * @param AttachmentState $attachment_state the state object in which the attachments are saved |
||
3233 | * between different requests |
||
3234 | */ |
||
3235 | public function setAttachments($message, $attachments, $attachment_state) { |
||
3236 | // Check if attachments should be deleted. This is set in the "upload_attachment.php" file |
||
3237 | if (isset($attachments['dialog_attachments'])) { |
||
3238 | $deleted = $attachment_state->getDeletedAttachments($attachments['dialog_attachments']); |
||
3239 | if ($deleted) { |
||
3240 | foreach ($deleted as $attach_num) { |
||
3241 | try { |
||
3242 | mapi_message_deleteattach($message, (int) $attach_num); |
||
3243 | } |
||
3244 | catch (Exception) { |
||
3245 | continue; |
||
3246 | } |
||
3247 | } |
||
3248 | $attachment_state->clearDeletedAttachments($attachments['dialog_attachments']); |
||
3249 | } |
||
3250 | } |
||
3251 | |||
3252 | $addedInlineAttachmentCidMapping = []; |
||
3253 | if (is_array($attachments) && !empty($attachments)) { |
||
3254 | // Set contentId to saved attachments. |
||
3255 | if (isset($attachments['add']) && is_array($attachments['add']) && !empty($attachments['add'])) { |
||
3256 | foreach ($attachments['add'] as $key => $attach) { |
||
3257 | if ($attach && isset($attach['inline']) && $attach['inline']) { |
||
3258 | $addedInlineAttachmentCidMapping[$attach['attach_num']] = $attach['cid']; |
||
3259 | $msgattachment = mapi_message_openattach($message, $attach['attach_num']); |
||
3260 | if ($msgattachment) { |
||
3261 | $props = [PR_ATTACH_CONTENT_ID => $attach['cid'], PR_ATTACHMENT_HIDDEN => true]; |
||
3262 | mapi_setprops($msgattachment, $props); |
||
3263 | mapi_savechanges($msgattachment); |
||
3264 | } |
||
3265 | } |
||
3266 | } |
||
3267 | } |
||
3268 | |||
3269 | // Delete saved inline images if removed from body. |
||
3270 | if (isset($attachments['remove']) && is_array($attachments['remove']) && !empty($attachments['remove'])) { |
||
3271 | foreach ($attachments['remove'] as $key => $attach) { |
||
3272 | if ($attach && isset($attach['inline']) && $attach['inline']) { |
||
3273 | $msgattachment = mapi_message_openattach($message, $attach['attach_num']); |
||
3274 | if ($msgattachment) { |
||
3275 | mapi_message_deleteattach($message, $attach['attach_num']); |
||
3276 | mapi_savechanges($message); |
||
3277 | } |
||
3278 | } |
||
3279 | } |
||
3280 | } |
||
3281 | } |
||
3282 | |||
3283 | if ($attachments['dialog_attachments']) { |
||
3284 | $dialog_attachments = $attachments['dialog_attachments']; |
||
3285 | } |
||
3286 | else { |
||
3287 | return; |
||
3288 | } |
||
3289 | |||
3290 | $files = $attachment_state->getAttachmentFiles($dialog_attachments); |
||
3291 | if ($files) { |
||
3292 | // Loop through the uploaded attachments |
||
3293 | foreach ($files as $tmpname => $fileinfo) { |
||
3294 | if ($fileinfo['sourcetype'] === 'embedded') { |
||
3295 | // open message which needs to be embedded |
||
3296 | $copyFromStore = $GLOBALS['mapisession']->openMessageStore(hex2bin((string) $fileinfo['store_entryid'])); |
||
3297 | $copyFrom = mapi_msgstore_openentry($copyFromStore, hex2bin((string) $fileinfo['entryid'])); |
||
3298 | |||
3299 | $msgProps = mapi_getprops($copyFrom, [PR_SUBJECT]); |
||
3300 | |||
3301 | // get message and copy it to attachment table as embedded attachment |
||
3302 | $props = []; |
||
3303 | $props[PR_EC_WA_ATTACHMENT_ID] = $fileinfo['attach_id']; |
||
3304 | $props[PR_ATTACH_METHOD] = ATTACH_EMBEDDED_MSG; |
||
3305 | $props[PR_DISPLAY_NAME] = !empty($msgProps[PR_SUBJECT]) ? $msgProps[PR_SUBJECT] : _('Untitled'); |
||
3306 | |||
3307 | // Create new attachment. |
||
3308 | $attachment = mapi_message_createattach($message); |
||
3309 | mapi_setprops($attachment, $props); |
||
3310 | |||
3311 | $imessage = mapi_attach_openobj($attachment, MAPI_CREATE | MAPI_MODIFY); |
||
3312 | |||
3313 | // Copy the properties from the source message to the attachment |
||
3314 | mapi_copyto($copyFrom, [], [], $imessage, 0); // includes attachments and recipients |
||
3315 | |||
3316 | // save changes in the embedded message and the final attachment |
||
3317 | mapi_savechanges($imessage); |
||
3318 | mapi_savechanges($attachment); |
||
3319 | } |
||
3320 | elseif ($fileinfo['sourcetype'] === 'icsfile') { |
||
3321 | $messageStore = $GLOBALS['mapisession']->openMessageStore(hex2bin((string) $fileinfo['store_entryid'])); |
||
3322 | $copyFrom = mapi_msgstore_openentry($messageStore, hex2bin((string) $fileinfo['entryid'])); |
||
3323 | |||
3324 | // Get addressbook for current session |
||
3325 | $addrBook = $GLOBALS['mapisession']->getAddressbook(); |
||
3326 | |||
3327 | // get message properties. |
||
3328 | $messageProps = mapi_getprops($copyFrom, [PR_SUBJECT]); |
||
3329 | |||
3330 | // Read the appointment as RFC2445-formatted ics stream. |
||
3331 | $appointmentStream = mapi_mapitoical($GLOBALS['mapisession']->getSession(), $addrBook, $copyFrom, []); |
||
3332 | |||
3333 | $filename = (!empty($messageProps[PR_SUBJECT])) ? $messageProps[PR_SUBJECT] : _('Untitled'); |
||
3334 | $filename .= '.ics'; |
||
3335 | |||
3336 | $props = [ |
||
3337 | PR_ATTACH_LONG_FILENAME => $filename, |
||
3338 | PR_DISPLAY_NAME => $filename, |
||
3339 | PR_ATTACH_METHOD => ATTACH_BY_VALUE, |
||
3340 | PR_ATTACH_DATA_BIN => "", |
||
3341 | PR_ATTACH_MIME_TAG => "application/octet-stream", |
||
3342 | PR_ATTACHMENT_HIDDEN => false, |
||
3343 | PR_EC_WA_ATTACHMENT_ID => isset($fileinfo["attach_id"]) && !empty($fileinfo["attach_id"]) ? $fileinfo["attach_id"] : uniqid(), |
||
3344 | PR_ATTACH_EXTENSION => pathinfo($filename, PATHINFO_EXTENSION), |
||
3345 | ]; |
||
3346 | |||
3347 | $attachment = mapi_message_createattach($message); |
||
3348 | mapi_setprops($attachment, $props); |
||
3349 | |||
3350 | // Stream the file to the PR_ATTACH_DATA_BIN property |
||
3351 | $stream = mapi_openproperty($attachment, PR_ATTACH_DATA_BIN, IID_IStream, 0, MAPI_CREATE | MAPI_MODIFY); |
||
3352 | mapi_stream_write($stream, $appointmentStream); |
||
3353 | |||
3354 | // Commit the stream and save changes |
||
3355 | mapi_stream_commit($stream); |
||
3356 | mapi_savechanges($attachment); |
||
3357 | } |
||
3358 | else { |
||
3359 | $filepath = $attachment_state->getAttachmentPath($tmpname); |
||
3360 | if (is_file($filepath)) { |
||
3361 | // Set contentId if attachment is inline |
||
3362 | $cid = ''; |
||
3363 | if (isset($addedInlineAttachmentCidMapping[$tmpname])) { |
||
3364 | $cid = $addedInlineAttachmentCidMapping[$tmpname]; |
||
3365 | } |
||
3366 | |||
3367 | // If a .p7m file was manually uploaded by the user, we must change the mime type because |
||
3368 | // otherwise mail applications will think the containing email is an encrypted email. |
||
3369 | // That will make Outlook crash, and it will make grommunio Web show the original mail as encrypted |
||
3370 | // without showing the attachment |
||
3371 | $mimeType = $fileinfo["type"]; |
||
3372 | $smimeTags = ['multipart/signed', 'application/pkcs7-mime', 'application/x-pkcs7-mime']; |
||
3373 | if (in_array($mimeType, $smimeTags)) { |
||
3374 | $mimeType = "application/octet-stream"; |
||
3375 | } |
||
3376 | |||
3377 | // Set attachment properties |
||
3378 | $props = [ |
||
3379 | PR_ATTACH_LONG_FILENAME => $fileinfo["name"], |
||
3380 | PR_DISPLAY_NAME => $fileinfo["name"], |
||
3381 | PR_ATTACH_METHOD => ATTACH_BY_VALUE, |
||
3382 | PR_ATTACH_DATA_BIN => "", |
||
3383 | PR_ATTACH_MIME_TAG => $mimeType, |
||
3384 | PR_ATTACHMENT_HIDDEN => !empty($cid) ? true : false, |
||
3385 | PR_EC_WA_ATTACHMENT_ID => isset($fileinfo["attach_id"]) && !empty($fileinfo["attach_id"]) ? $fileinfo["attach_id"] : uniqid(), |
||
3386 | PR_ATTACH_EXTENSION => pathinfo((string) $fileinfo["name"], PATHINFO_EXTENSION), |
||
3387 | ]; |
||
3388 | |||
3389 | if (isset($fileinfo['sourcetype']) && $fileinfo['sourcetype'] === 'contactphoto') { |
||
3390 | $props[PR_ATTACHMENT_HIDDEN] = true; |
||
3391 | $props[PR_ATTACHMENT_CONTACTPHOTO] = true; |
||
3392 | } |
||
3393 | |||
3394 | if (!empty($cid)) { |
||
3395 | $props[PR_ATTACH_CONTENT_ID] = $cid; |
||
3396 | } |
||
3397 | |||
3398 | // Create attachment and set props |
||
3399 | $attachment = mapi_message_createattach($message); |
||
3400 | mapi_setprops($attachment, $props); |
||
3401 | |||
3402 | // Stream the file to the PR_ATTACH_DATA_BIN property |
||
3403 | $stream = mapi_openproperty($attachment, PR_ATTACH_DATA_BIN, IID_IStream, 0, MAPI_CREATE | MAPI_MODIFY); |
||
3404 | $handle = fopen($filepath, "r"); |
||
3405 | while (!feof($handle)) { |
||
3406 | $contents = fread($handle, BLOCK_SIZE); |
||
3407 | mapi_stream_write($stream, $contents); |
||
3408 | } |
||
3409 | |||
3410 | // Commit the stream and save changes |
||
3411 | mapi_stream_commit($stream); |
||
3412 | mapi_savechanges($attachment); |
||
3413 | fclose($handle); |
||
3414 | unlink($filepath); |
||
3415 | } |
||
3416 | } |
||
3417 | } |
||
3418 | |||
3419 | // Delete all the files in the state. |
||
3420 | $attachment_state->clearAttachmentFiles($dialog_attachments); |
||
3421 | } |
||
3422 | } |
||
3423 | |||
3424 | /** |
||
3425 | * Copy attachments from original message. |
||
3426 | * |
||
3427 | * @see Operations::saveMessage() |
||
3428 | * |
||
3429 | * @param object $message MAPI Message Object |
||
3430 | * @param string $attachments |
||
3431 | * @param MAPIMessage $copyFromMessage if set, copy the attachments from this message in addition to the uploaded attachments |
||
3432 | * @param bool $copyInlineAttachmentsOnly if true then copy only inline attachments |
||
3433 | * @param AttachmentState $attachment_state the state object in which the attachments are saved |
||
3434 | * between different requests |
||
3435 | */ |
||
3436 | public function copyAttachments($message, $attachments, $copyFromMessage, $copyInlineAttachmentsOnly, $attachment_state) { |
||
3437 | $attachmentTable = mapi_message_getattachmenttable($copyFromMessage); |
||
3438 | if ($attachmentTable && isset($attachments['dialog_attachments'])) { |
||
3439 | $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]); |
||
3440 | $deletedAttachments = $attachment_state->getDeletedAttachments($attachments['dialog_attachments']); |
||
3441 | |||
3442 | $plainText = $this->isPlainText($message); |
||
3443 | |||
3444 | $properties = $GLOBALS['properties']->getMailProperties(); |
||
3445 | $blockStatus = mapi_getprops($copyFromMessage, [PR_BLOCK_STATUS]); |
||
3446 | $blockStatus = Conversion::mapMAPI2XML($properties, $blockStatus); |
||
3447 | $isSafeSender = false; |
||
3448 | |||
3449 | // Here if message is HTML and block status is empty then and then call isSafeSender function |
||
3450 | // to check that sender or sender's domain of original message was part of safe sender list. |
||
3451 | if (!$plainText && empty($blockStatus)) { |
||
3452 | $isSafeSender = $this->isSafeSender($copyFromMessage); |
||
3453 | } |
||
3454 | |||
3455 | $body = false; |
||
3456 | foreach ($existingAttachments as $props) { |
||
3457 | // check if this attachment is "deleted" |
||
3458 | |||
3459 | if ($deletedAttachments && in_array($props[PR_ATTACH_NUM], $deletedAttachments)) { |
||
3460 | // skip attachment, remove reference from state as it no longer applies. |
||
3461 | $attachment_state->removeDeletedAttachment($attachments['dialog_attachments'], $props[PR_ATTACH_NUM]); |
||
3462 | |||
3463 | continue; |
||
3464 | } |
||
3465 | |||
3466 | $old = mapi_message_openattach($copyFromMessage, $props[PR_ATTACH_NUM]); |
||
3467 | $isInlineAttachment = $attachment_state->isInlineAttachment($old); |
||
3468 | |||
3469 | /* |
||
3470 | * If reply/reply all message, then copy only inline attachments. |
||
3471 | */ |
||
3472 | if ($copyInlineAttachmentsOnly) { |
||
3473 | /* |
||
3474 | * if message is reply/reply all and format is plain text than ignore inline attachments |
||
3475 | * and normal attachments to copy from original mail. |
||
3476 | */ |
||
3477 | if ($plainText || !$isInlineAttachment) { |
||
3478 | continue; |
||
3479 | } |
||
3480 | } |
||
3481 | elseif ($plainText && $isInlineAttachment) { |
||
3482 | /* |
||
3483 | * If message is forward and format of message is plain text then ignore only inline attachments from the |
||
3484 | * original mail. |
||
3485 | */ |
||
3486 | continue; |
||
3487 | } |
||
3488 | |||
3489 | /* |
||
3490 | * If the inline attachment is referenced with an content-id, |
||
3491 | * manually check if it's still referenced in the body otherwise remove it |
||
3492 | */ |
||
3493 | if ($isInlineAttachment) { |
||
3494 | // Cache body, so we stream it once |
||
3495 | if ($body === false) { |
||
3496 | $body = streamProperty($message, PR_HTML); |
||
3497 | } |
||
3498 | |||
3499 | $contentID = $props[PR_ATTACH_CONTENT_ID]; |
||
3500 | if (!str_contains($body, (string) $contentID)) { |
||
3501 | continue; |
||
3502 | } |
||
3503 | } |
||
3504 | |||
3505 | /* |
||
3506 | * if message is reply/reply all or forward and format of message is HTML but |
||
3507 | * - inline attachments are not downloaded from external source |
||
3508 | * - sender of original message is not safe sender |
||
3509 | * - domain of sender is not part of safe sender list |
||
3510 | * then ignore inline attachments from original message. |
||
3511 | * |
||
3512 | * NOTE : blockStatus is only generated when user has download inline image from external source. |
||
3513 | * it should remains empty if user add the sender in to safe sender list. |
||
3514 | */ |
||
3515 | if (!$plainText && $isInlineAttachment && empty($blockStatus) && !$isSafeSender) { |
||
3516 | continue; |
||
3517 | } |
||
3518 | |||
3519 | $new = mapi_message_createattach($message); |
||
3520 | |||
3521 | try { |
||
3522 | mapi_copyto($old, [], [], $new, 0); |
||
3523 | mapi_savechanges($new); |
||
3524 | } |
||
3525 | catch (MAPIException $e) { |
||
3526 | // This is a workaround for the grommunio-web issue 75 |
||
3527 | // Remove it after gromox issue 253 is resolved |
||
3528 | if ($e->getCode() == ecMsgCycle) { |
||
3529 | $oldstream = mapi_openproperty($old, PR_ATTACH_DATA_BIN, IID_IStream, 0, 0); |
||
3530 | $stat = mapi_stream_stat($oldstream); |
||
3531 | $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]); |
||
3532 | |||
3533 | mapi_setprops($new, [ |
||
3534 | PR_ATTACH_LONG_FILENAME => $props[PR_ATTACH_LONG_FILENAME] ?? '', |
||
3535 | PR_ATTACH_MIME_TAG => $props[PR_ATTACH_MIME_TAG] ?? "application/octet-stream", |
||
3536 | PR_DISPLAY_NAME => $props[PR_DISPLAY_NAME] ?? '', |
||
3537 | PR_ATTACH_METHOD => $props[PR_ATTACH_METHOD] ?? ATTACH_BY_VALUE, |
||
3538 | PR_ATTACH_FILENAME => $props[PR_ATTACH_FILENAME] ?? '', |
||
3539 | PR_ATTACH_DATA_BIN => "", |
||
3540 | PR_ATTACHMENT_HIDDEN => $props[PR_ATTACHMENT_HIDDEN] ?? false, |
||
3541 | PR_ATTACH_EXTENSION => $props[PR_ATTACH_EXTENSION] ?? '', |
||
3542 | PR_ATTACH_FLAGS => $props[PR_ATTACH_FLAGS] ?? 0, |
||
3543 | ]); |
||
3544 | $newstream = mapi_openproperty($new, PR_ATTACH_DATA_BIN, IID_IStream, 0, MAPI_CREATE | MAPI_MODIFY); |
||
3545 | mapi_stream_setsize($newstream, $stat['cb']); |
||
3546 | for ($i = 0; $i < $stat['cb']; $i += BLOCK_SIZE) { |
||
3547 | mapi_stream_write($newstream, mapi_stream_read($oldstream, BLOCK_SIZE)); |
||
3548 | } |
||
3549 | mapi_stream_commit($newstream); |
||
3550 | mapi_savechanges($new); |
||
3551 | } |
||
3552 | } |
||
3553 | } |
||
3554 | } |
||
3555 | } |
||
3556 | |||
3557 | /** |
||
3558 | * Function was used to identify the sender or domain of original mail in safe sender list. |
||
3559 | * |
||
3560 | * @param MAPIMessage $copyFromMessage resource of the message from which we should get |
||
3561 | * the sender of message |
||
3562 | * |
||
3563 | * @return bool true if sender of original mail was safe sender else false |
||
3564 | */ |
||
3565 | public function isSafeSender($copyFromMessage) { |
||
3566 | $safeSenderList = $GLOBALS['settings']->get('zarafa/v1/contexts/mail/safe_senders_list'); |
||
3567 | $senderEntryid = mapi_getprops($copyFromMessage, [PR_SENT_REPRESENTING_ENTRYID]); |
||
3568 | $senderEntryid = $senderEntryid[PR_SENT_REPRESENTING_ENTRYID]; |
||
3569 | |||
3570 | // If sender is user himself (which happens in case of "Send as New message") consider sender as safe |
||
3571 | if ($GLOBALS['entryid']->compareEntryIds($senderEntryid, $GLOBALS["mapisession"]->getUserEntryID())) { |
||
3572 | return true; |
||
3573 | } |
||
3574 | |||
3575 | try { |
||
3576 | $mailuser = mapi_ab_openentry($GLOBALS["mapisession"]->getAddressbook(), $senderEntryid); |
||
3577 | } |
||
3578 | catch (MAPIException) { |
||
3579 | // The user might have a new uidNumber, which makes the user not resolve, see WA-7673 |
||
3580 | // FIXME: Lookup the user by PR_SENDER_NAME or another attribute if PR_SENDER_ADDRTYPE is "EX" |
||
3581 | return false; |
||
3582 | } |
||
3583 | |||
3584 | $addressType = mapi_getprops($mailuser, [PR_ADDRTYPE]); |
||
3585 | |||
3586 | // Here it will check that sender of original mail was address book user. |
||
3587 | // If PR_ADDRTYPE is ZARAFA, it means sender of original mail was address book contact. |
||
3588 | if ($addressType[PR_ADDRTYPE] === 'EX') { |
||
3589 | $address = mapi_getprops($mailuser, [PR_SMTP_ADDRESS]); |
||
3590 | $address = $address[PR_SMTP_ADDRESS]; |
||
3591 | } |
||
3592 | elseif ($addressType[PR_ADDRTYPE] === 'SMTP') { |
||
3593 | // If PR_ADDRTYPE is SMTP, it means sender of original mail was external sender. |
||
3594 | $address = mapi_getprops($mailuser, [PR_EMAIL_ADDRESS]); |
||
3595 | $address = $address[PR_EMAIL_ADDRESS]; |
||
3596 | } |
||
3597 | |||
3598 | // Obtain the Domain address from smtp/email address. |
||
3599 | $domain = substr((string) $address, strpos((string) $address, "@") + 1); |
||
3600 | |||
3601 | if (!empty($safeSenderList)) { |
||
3602 | foreach ($safeSenderList as $safeSender) { |
||
3603 | if ($safeSender === $address || $safeSender === $domain) { |
||
3604 | return true; |
||
3605 | } |
||
3606 | } |
||
3607 | } |
||
3608 | |||
3609 | return false; |
||
3610 | } |
||
3611 | |||
3612 | /** |
||
3613 | * get attachments information of a particular message. |
||
3614 | * |
||
3615 | * @param MapiMessage $message MAPI Message Object |
||
3616 | * @param bool $excludeHidden exclude hidden attachments |
||
3617 | */ |
||
3618 | public function getAttachmentsInfo($message, $excludeHidden = false) { |
||
3619 | $attachmentsInfo = []; |
||
3620 | |||
3621 | $hasattachProp = mapi_getprops($message, [PR_HASATTACH]); |
||
3622 | if (isset($hasattachProp[PR_HASATTACH]) && $hasattachProp[PR_HASATTACH]) { |
||
3623 | $attachmentTable = mapi_message_getattachmenttable($message); |
||
3624 | |||
3625 | $attachments = mapi_table_queryallrows($attachmentTable, [PR_ATTACH_NUM, PR_ATTACH_SIZE, PR_ATTACH_LONG_FILENAME, |
||
3626 | PR_ATTACH_FILENAME, PR_ATTACHMENT_HIDDEN, PR_DISPLAY_NAME, PR_ATTACH_METHOD, |
||
3627 | PR_ATTACH_CONTENT_ID, PR_ATTACH_MIME_TAG, |
||
3628 | PR_ATTACHMENT_CONTACTPHOTO, PR_RECORD_KEY, PR_EC_WA_ATTACHMENT_ID, PR_OBJECT_TYPE, PR_ATTACH_EXTENSION, ]); |
||
3629 | foreach ($attachments as $attachmentRow) { |
||
3630 | $props = []; |
||
3631 | |||
3632 | if (isset($attachmentRow[PR_ATTACH_MIME_TAG])) { |
||
3633 | if ($attachmentRow[PR_ATTACH_MIME_TAG]) { |
||
3634 | $props["filetype"] = $attachmentRow[PR_ATTACH_MIME_TAG]; |
||
3635 | } |
||
3636 | |||
3637 | $smimeTags = ['multipart/signed', 'application/pkcs7-mime', 'application/x-pkcs7-mime']; |
||
3638 | if (in_array($attachmentRow[PR_ATTACH_MIME_TAG], $smimeTags)) { |
||
3639 | // Ignore the message with attachment types set as smime as they are for smime |
||
3640 | continue; |
||
3641 | } |
||
3642 | } |
||
3643 | |||
3644 | $attach_id = ''; |
||
3645 | if (isset($attachmentRow[PR_EC_WA_ATTACHMENT_ID])) { |
||
3646 | $attach_id = $attachmentRow[PR_EC_WA_ATTACHMENT_ID]; |
||
3647 | } |
||
3648 | elseif (isset($attachmentRow[PR_RECORD_KEY])) { |
||
3649 | $attach_id = bin2hex((string) $attachmentRow[PR_RECORD_KEY]); |
||
3650 | } |
||
3651 | else { |
||
3652 | $attach_id = uniqid(); |
||
3653 | } |
||
3654 | |||
3655 | $props["object_type"] = $attachmentRow[PR_OBJECT_TYPE]; |
||
3656 | $props["attach_id"] = $attach_id; |
||
3657 | $props["attach_num"] = $attachmentRow[PR_ATTACH_NUM]; |
||
3658 | $props["attach_method"] = $attachmentRow[PR_ATTACH_METHOD]; |
||
3659 | $props["size"] = $attachmentRow[PR_ATTACH_SIZE]; |
||
3660 | |||
3661 | if (isset($attachmentRow[PR_ATTACH_CONTENT_ID]) && $attachmentRow[PR_ATTACH_CONTENT_ID]) { |
||
3662 | $props["cid"] = $attachmentRow[PR_ATTACH_CONTENT_ID]; |
||
3663 | } |
||
3664 | |||
3665 | $props["hidden"] = $attachmentRow[PR_ATTACHMENT_HIDDEN] ?? false; |
||
3666 | if ($excludeHidden && $props["hidden"]) { |
||
3667 | continue; |
||
3668 | } |
||
3669 | |||
3670 | if (isset($attachmentRow[PR_ATTACH_LONG_FILENAME])) { |
||
3671 | $props["name"] = $attachmentRow[PR_ATTACH_LONG_FILENAME]; |
||
3672 | } |
||
3673 | elseif (isset($attachmentRow[PR_ATTACH_FILENAME])) { |
||
3674 | $props["name"] = $attachmentRow[PR_ATTACH_FILENAME]; |
||
3675 | } |
||
3676 | elseif (isset($attachmentRow[PR_DISPLAY_NAME])) { |
||
3677 | $props["name"] = $attachmentRow[PR_DISPLAY_NAME]; |
||
3678 | } |
||
3679 | else { |
||
3680 | $props["name"] = "untitled"; |
||
3681 | } |
||
3682 | |||
3683 | if (isset($attachmentRow[PR_ATTACH_EXTENSION]) && $attachmentRow[PR_ATTACH_EXTENSION]) { |
||
3684 | $props["extension"] = $attachmentRow[PR_ATTACH_EXTENSION]; |
||
3685 | } |
||
3686 | else { |
||
3687 | // For backward compatibility where attachments doesn't have the extension property |
||
3688 | $props["extension"] = pathinfo((string) $props["name"], PATHINFO_EXTENSION); |
||
3689 | } |
||
3690 | |||
3691 | if (isset($attachmentRow[PR_ATTACHMENT_CONTACTPHOTO]) && $attachmentRow[PR_ATTACHMENT_CONTACTPHOTO]) { |
||
3692 | $props["attachment_contactphoto"] = $attachmentRow[PR_ATTACHMENT_CONTACTPHOTO]; |
||
3693 | $props["hidden"] = true; |
||
3694 | |||
3695 | // Open contact photo attachment in binary format. |
||
3696 | $attach = mapi_message_openattach($message, $props["attach_num"]); |
||
3697 | } |
||
3698 | |||
3699 | if ($props["attach_method"] == ATTACH_EMBEDDED_MSG) { |
||
3700 | // open attachment to get the message class |
||
3701 | $attach = mapi_message_openattach($message, $props["attach_num"]); |
||
3702 | $embMessage = mapi_attach_openobj($attach); |
||
3703 | $embProps = mapi_getprops($embMessage, [PR_MESSAGE_CLASS]); |
||
3704 | if (isset($embProps[PR_MESSAGE_CLASS])) { |
||
3705 | $props["attach_message_class"] = $embProps[PR_MESSAGE_CLASS]; |
||
3706 | } |
||
3707 | } |
||
3708 | |||
3709 | array_push($attachmentsInfo, ["props" => $props]); |
||
3710 | } |
||
3711 | } |
||
3712 | |||
3713 | return $attachmentsInfo; |
||
3714 | } |
||
3715 | |||
3716 | /** |
||
3717 | * get recipients information of a particular message. |
||
3718 | * |
||
3719 | * @param MapiMessage $message MAPI Message Object |
||
3720 | * @param bool $excludeDeleted exclude deleted recipients |
||
3721 | */ |
||
3722 | public function getRecipientsInfo($message, $excludeDeleted = true) { |
||
3723 | $recipientsInfo = []; |
||
3724 | |||
3725 | $recipientTable = mapi_message_getrecipienttable($message); |
||
3726 | if ($recipientTable) { |
||
3727 | $recipients = mapi_table_queryallrows($recipientTable, $GLOBALS['properties']->getRecipientProperties()); |
||
3728 | |||
3729 | foreach ($recipients as $recipientRow) { |
||
3730 | if ($excludeDeleted && isset($recipientRow[PR_RECIPIENT_FLAGS]) && (($recipientRow[PR_RECIPIENT_FLAGS] & recipExceptionalDeleted) == recipExceptionalDeleted)) { |
||
3731 | continue; |
||
3732 | } |
||
3733 | |||
3734 | $props = []; |
||
3735 | $props['rowid'] = $recipientRow[PR_ROWID]; |
||
3736 | $props['search_key'] = isset($recipientRow[PR_SEARCH_KEY]) ? bin2hex((string) $recipientRow[PR_SEARCH_KEY]) : ''; |
||
3737 | $props['display_name'] = $recipientRow[PR_DISPLAY_NAME] ?? ''; |
||
3738 | $props['email_address'] = $recipientRow[PR_EMAIL_ADDRESS] ?? ''; |
||
3739 | $props['smtp_address'] = $recipientRow[PR_SMTP_ADDRESS] ?? ''; |
||
3740 | $props['address_type'] = $recipientRow[PR_ADDRTYPE] ?? ''; |
||
3741 | $props['object_type'] = $recipientRow[PR_OBJECT_TYPE] ?? MAPI_MAILUSER; |
||
3742 | $props['recipient_type'] = $recipientRow[PR_RECIPIENT_TYPE]; |
||
3743 | $props['display_type'] = $recipientRow[PR_DISPLAY_TYPE] ?? DT_MAILUSER; |
||
3744 | $props['display_type_ex'] = $recipientRow[PR_DISPLAY_TYPE_EX] ?? DT_MAILUSER; |
||
3745 | |||
3746 | if (isset($recipientRow[PR_RECIPIENT_FLAGS])) { |
||
3747 | $props['recipient_flags'] = $recipientRow[PR_RECIPIENT_FLAGS]; |
||
3748 | } |
||
3749 | |||
3750 | if (isset($recipientRow[PR_ENTRYID])) { |
||
3751 | $props['entryid'] = bin2hex((string) $recipientRow[PR_ENTRYID]); |
||
3752 | |||
3753 | // Get the SMTP address from the addressbook if no address is found |
||
3754 | if (empty($props['smtp_address']) && ($recipientRow[PR_ADDRTYPE] == 'EX' || $props['address_type'] === 'ZARAFA')) { |
||
3755 | $recipientSearchKey = $recipientRow[PR_SEARCH_KEY] ?? false; |
||
3756 | $props['smtp_address'] = $this->getEmailAddress($recipientRow[PR_ENTRYID], $recipientSearchKey); |
||
3757 | } |
||
3758 | } |
||
3759 | |||
3760 | // smtp address is still empty(in case of external email address) than |
||
3761 | // value of email address is copied into smtp address. |
||
3762 | if ($props['address_type'] == 'SMTP' && empty($props['smtp_address'])) { |
||
3763 | $props['smtp_address'] = $props['email_address']; |
||
3764 | } |
||
3765 | |||
3766 | // PST importer imports items without an entryid and as SMTP recipient, this causes issues for |
||
3767 | // opening meeting requests with removed users as recipient. |
||
3768 | // gromox-kdb2mt might import items without an entryid and |
||
3769 | // PR_ADDRTYPE 'ZARAFA' which causes issues when opening such messages. |
||
3770 | if (empty($props['entryid']) && ($props['address_type'] === 'SMTP' || $props['address_type'] === 'ZARAFA')) { |
||
3771 | $props['entryid'] = bin2hex(mapi_createoneoff($props['display_name'], $props['address_type'], $props['smtp_address'], MAPI_UNICODE)); |
||
3772 | } |
||
3773 | |||
3774 | // Set propose new time properties |
||
3775 | if (isset($recipientRow[PR_RECIPIENT_PROPOSED], $recipientRow[PR_RECIPIENT_PROPOSEDSTARTTIME], $recipientRow[PR_RECIPIENT_PROPOSEDENDTIME])) { |
||
3776 | $props['proposednewtime_start'] = $recipientRow[PR_RECIPIENT_PROPOSEDSTARTTIME]; |
||
3777 | $props['proposednewtime_end'] = $recipientRow[PR_RECIPIENT_PROPOSEDENDTIME]; |
||
3778 | $props['proposednewtime'] = $recipientRow[PR_RECIPIENT_PROPOSED]; |
||
3779 | } |
||
3780 | else { |
||
3781 | $props['proposednewtime'] = false; |
||
3782 | } |
||
3783 | |||
3784 | $props['recipient_trackstatus'] = $recipientRow[PR_RECIPIENT_TRACKSTATUS] ?? olRecipientTrackStatusNone; |
||
3785 | $props['recipient_trackstatus_time'] = $recipientRow[PR_RECIPIENT_TRACKSTATUS_TIME] ?? null; |
||
3786 | |||
3787 | array_push($recipientsInfo, ["props" => $props]); |
||
3788 | } |
||
3789 | } |
||
3790 | |||
3791 | return $recipientsInfo; |
||
3792 | } |
||
3793 | |||
3794 | /** |
||
3795 | * Extracts email address from PR_SEARCH_KEY property if possible. |
||
3796 | * |
||
3797 | * @param string $searchKey The PR_SEARCH_KEY property |
||
3798 | * |
||
3799 | * @return string email address if possible else return empty string |
||
3800 | */ |
||
3801 | public function getEmailAddressFromSearchKey($searchKey) { |
||
3802 | if (str_contains($searchKey, ':') && str_contains($searchKey, '@')) { |
||
3803 | return trim(strtolower(explode(':', $searchKey)[1])); |
||
3804 | } |
||
3805 | |||
3806 | return ""; |
||
3807 | } |
||
3808 | |||
3809 | /** |
||
3810 | * Create a MAPI recipient list from an XML array structure. |
||
3811 | * |
||
3812 | * This functions is used for setting the recipient table of a message. |
||
3813 | * |
||
3814 | * @param array $recipientList a list of recipients as XML array structure |
||
3815 | * @param string $opType the type of operation that will be performed on this recipient list (add, remove, modify) |
||
3816 | * @param bool $send true if we are going to send this message else false |
||
3817 | * @param mixed $isException |
||
3818 | * |
||
3819 | * @return array list of recipients with the correct MAPI properties ready for mapi_message_modifyrecipients() |
||
3820 | */ |
||
3821 | public function createRecipientList($recipientList, $opType = 'add', $isException = false, $send = false) { |
||
3822 | $recipients = []; |
||
3823 | $addrbook = $GLOBALS["mapisession"]->getAddressbook(); |
||
3824 | |||
3825 | foreach ($recipientList as $recipientItem) { |
||
3826 | if ($isException) { |
||
3827 | // We do not add organizer to exception msg in organizer's calendar. |
||
3828 | if (isset($recipientItem[PR_RECIPIENT_FLAGS]) && $recipientItem[PR_RECIPIENT_FLAGS] == (recipSendable | recipOrganizer)) { |
||
3829 | continue; |
||
3830 | } |
||
3831 | |||
3832 | $recipient[PR_RECIPIENT_FLAGS] = (recipSendable | recipExceptionalResponse | recipReserved); |
||
3833 | } |
||
3834 | |||
3835 | if (!empty($recipientItem["smtp_address"]) && empty($recipientItem["email_address"])) { |
||
3836 | $recipientItem["email_address"] = $recipientItem["smtp_address"]; |
||
3837 | } |
||
3838 | |||
3839 | // When saving a mail we can allow an empty email address or entryid, but not when sending it |
||
3840 | if ($send && empty($recipientItem["email_address"]) && empty($recipientItem['entryid'])) { |
||
3841 | return; |
||
3842 | } |
||
3843 | |||
3844 | // to modify or remove recipients we need PR_ROWID property |
||
3845 | if ($opType !== 'add' && (!isset($recipientItem['rowid']) || !is_numeric($recipientItem['rowid']))) { |
||
3846 | continue; |
||
3847 | } |
||
3848 | |||
3849 | if (isset($recipientItem['search_key']) && !empty($recipientItem['search_key'])) { |
||
3850 | // search keys sent from client are in hex format so convert it to binary format |
||
3851 | $recipientItem['search_key'] = hex2bin((string) $recipientItem['search_key']); |
||
3852 | } |
||
3853 | |||
3854 | if (isset($recipientItem["entryid"]) && !empty($recipientItem["entryid"])) { |
||
3855 | // entryids sent from client are in hex format so convert it to binary format |
||
3856 | $recipientItem["entryid"] = hex2bin((string) $recipientItem["entryid"]); |
||
3857 | |||
3858 | // Only resolve the recipient when no entryid is set |
||
3859 | } |
||
3860 | else { |
||
3861 | /** |
||
3862 | * For external contacts (DT_REMOTE_MAILUSER) email_address contains display name of contact |
||
3863 | * which is obviously not unique so for that we need to resolve address based on smtp_address |
||
3864 | * if provided. |
||
3865 | */ |
||
3866 | $addressToResolve = $recipientItem["email_address"]; |
||
3867 | if (!empty($recipientItem["smtp_address"])) { |
||
3868 | $addressToResolve = $recipientItem["smtp_address"]; |
||
3869 | } |
||
3870 | |||
3871 | // Resolve the recipient |
||
3872 | $user = [[PR_DISPLAY_NAME => $addressToResolve]]; |
||
3873 | |||
3874 | try { |
||
3875 | // resolve users based on email address with strict matching |
||
3876 | $user = mapi_ab_resolvename($addrbook, $user, EMS_AB_ADDRESS_LOOKUP); |
||
3877 | $recipientItem["display_name"] = $user[0][PR_DISPLAY_NAME]; |
||
3878 | $recipientItem["entryid"] = $user[0][PR_ENTRYID]; |
||
3879 | $recipientItem["search_key"] = $user[0][PR_SEARCH_KEY]; |
||
3880 | $recipientItem["email_address"] = $user[0][PR_EMAIL_ADDRESS]; |
||
3881 | $recipientItem["address_type"] = $user[0][PR_ADDRTYPE]; |
||
3882 | } |
||
3883 | catch (MAPIException $e) { |
||
3884 | // recipient is not resolved or it got multiple matches, |
||
3885 | // so ignore this error and continue with normal processing |
||
3886 | $e->setHandled(); |
||
3887 | } |
||
3888 | } |
||
3889 | |||
3890 | $recipient = []; |
||
3891 | $recipient[PR_DISPLAY_NAME] = $recipientItem["display_name"]; |
||
3892 | $recipient[PR_DISPLAY_TYPE] = $recipientItem["display_type"]; |
||
3893 | $recipient[PR_DISPLAY_TYPE_EX] = $recipientItem["display_type_ex"]; |
||
3894 | $recipient[PR_EMAIL_ADDRESS] = $recipientItem["email_address"]; |
||
3895 | $recipient[PR_SMTP_ADDRESS] = $recipientItem["smtp_address"]; |
||
3896 | if (isset($recipientItem["search_key"])) { |
||
3897 | $recipient[PR_SEARCH_KEY] = $recipientItem["search_key"]; |
||
3898 | } |
||
3899 | $recipient[PR_ADDRTYPE] = $recipientItem["address_type"]; |
||
3900 | $recipient[PR_OBJECT_TYPE] = $recipientItem["object_type"]; |
||
3901 | $recipient[PR_RECIPIENT_TYPE] = $recipientItem["recipient_type"]; |
||
3902 | if ($opType != 'add') { |
||
3903 | $recipient[PR_ROWID] = $recipientItem["rowid"]; |
||
3904 | } |
||
3905 | |||
3906 | if (isset($recipientItem["recipient_status"]) && !empty($recipientItem["recipient_status"])) { |
||
3907 | $recipient[PR_RECIPIENT_TRACKSTATUS] = $recipientItem["recipient_status"]; |
||
3908 | } |
||
3909 | |||
3910 | if (isset($recipientItem["recipient_flags"]) && !empty($recipient["recipient_flags"])) { |
||
3911 | $recipient[PR_RECIPIENT_FLAGS] = $recipientItem["recipient_flags"]; |
||
3912 | } |
||
3913 | else { |
||
3914 | $recipient[PR_RECIPIENT_FLAGS] = recipSendable; |
||
3915 | } |
||
3916 | |||
3917 | if (isset($recipientItem["proposednewtime"]) && !empty($recipientItem["proposednewtime"]) && isset($recipientItem["proposednewtime_start"], $recipientItem["proposednewtime_end"])) { |
||
3918 | $recipient[PR_RECIPIENT_PROPOSED] = $recipientItem["proposednewtime"]; |
||
3919 | $recipient[PR_RECIPIENT_PROPOSEDSTARTTIME] = $recipientItem["proposednewtime_start"]; |
||
3920 | $recipient[PR_RECIPIENT_PROPOSEDENDTIME] = $recipientItem["proposednewtime_end"]; |
||
3921 | } |
||
3922 | else { |
||
3923 | $recipient[PR_RECIPIENT_PROPOSED] = false; |
||
3924 | } |
||
3925 | |||
3926 | // Use given entryid if possible, otherwise create a one-off entryid |
||
3927 | if (isset($recipientItem["entryid"]) && !empty($recipientItem["entryid"])) { |
||
3928 | $recipient[PR_ENTRYID] = $recipientItem["entryid"]; |
||
3929 | } |
||
3930 | elseif ($send) { |
||
3931 | // only create one-off entryid when we are actually sending the message not saving it |
||
3932 | $recipient[PR_ENTRYID] = mapi_createoneoff($recipient[PR_DISPLAY_NAME], $recipient[PR_ADDRTYPE], $recipient[PR_EMAIL_ADDRESS]); |
||
3933 | } |
||
3934 | |||
3935 | array_push($recipients, $recipient); |
||
3936 | } |
||
3937 | |||
3938 | return $recipients; |
||
3939 | } |
||
3940 | |||
3941 | /** |
||
3942 | * Function which is get store of external resource from entryid. |
||
3943 | * |
||
3944 | * @param string $entryid entryid of the shared folder record |
||
3945 | * |
||
3946 | * @return object/boolean $store store of shared folder if found otherwise false |
||
3947 | * |
||
3948 | * FIXME: this function is pretty inefficient, since it opens the store for every |
||
3949 | * shared user in the worst case. Might be that we could extract the guid from |
||
3950 | * the $entryid and compare it and fetch the guid from the userentryid. |
||
3951 | * C++ has a GetStoreGuidFromEntryId() function. |
||
3952 | */ |
||
3953 | public function getOtherStoreFromEntryid($entryid) { |
||
3954 | // Get all external user from settings |
||
3955 | $otherUsers = $GLOBALS['mapisession']->retrieveOtherUsersFromSettings(); |
||
3956 | |||
3957 | // Fetch the store of each external user and |
||
3958 | // find the record with given entryid |
||
3959 | foreach ($otherUsers as $sharedUser => $values) { |
||
3960 | $userEntryid = mapi_msgstore_createentryid($GLOBALS['mapisession']->getDefaultMessageStore(), $sharedUser); |
||
3961 | $store = $GLOBALS['mapisession']->openMessageStore($userEntryid); |
||
3962 | if ($GLOBALS['entryid']->hasContactProviderGUID($entryid)) { |
||
3963 | $entryid = $GLOBALS["entryid"]->unwrapABEntryIdObj($entryid); |
||
3964 | } |
||
3965 | |||
3966 | try { |
||
3967 | $record = mapi_msgstore_openentry($store, hex2bin((string) $entryid)); |
||
3968 | if ($record) { |
||
3969 | return $store; |
||
3970 | } |
||
3971 | } |
||
3972 | catch (MAPIException) { |
||
3973 | } |
||
3974 | } |
||
3975 | |||
3976 | return false; |
||
3977 | } |
||
3978 | |||
3979 | /** |
||
3980 | * Function which is use to check the contact item (distribution list / contact) |
||
3981 | * belongs to any external folder or not. |
||
3982 | * |
||
3983 | * @param string $entryid entryid of contact item |
||
3984 | * |
||
3985 | * @return bool true if contact item from external folder otherwise false. |
||
3986 | * |
||
3987 | * FIXME: this function is broken and returns true if the user is a contact in a shared store. |
||
3988 | * Also research if we cannot just extract the GUID and compare it with our own GUID. |
||
3989 | * FIXME This function should be renamed, because it's also meant for normal shared folder contacts. |
||
3990 | */ |
||
3991 | public function isExternalContactItem($entryid) { |
||
3992 | try { |
||
3993 | if (!$GLOBALS['entryid']->hasContactProviderGUID(bin2hex($entryid))) { |
||
3994 | $entryid = hex2bin((string) $GLOBALS['entryid']->wrapABEntryIdObj(bin2hex($entryid), MAPI_DISTLIST)); |
||
3995 | } |
||
3996 | mapi_ab_openentry($GLOBALS["mapisession"]->getAddressbook(), $entryid); |
||
3997 | } |
||
3998 | catch (MAPIException) { |
||
3999 | return true; |
||
4000 | } |
||
4001 | |||
4002 | return false; |
||
4003 | } |
||
4004 | |||
4005 | /** |
||
4006 | * Get object type from distlist type of member of distribution list. |
||
4007 | * |
||
4008 | * @param int $distlistType distlist type of distribution list |
||
4009 | * |
||
4010 | * @return int object type of distribution list |
||
4011 | */ |
||
4012 | public function getObjectTypeFromDistlistType($distlistType) { |
||
4013 | return match ($distlistType) { |
||
4014 | DL_DIST, DL_DIST_AB => MAPI_DISTLIST, |
||
4015 | default => MAPI_MAILUSER, |
||
4016 | }; |
||
4017 | } |
||
4018 | |||
4019 | /** |
||
4020 | * Function which fetches all members of shared/internal(Local Contact Folder) |
||
4021 | * folder's distribution list. |
||
4022 | * |
||
4023 | * @param string $distlistEntryid entryid of distribution list |
||
4024 | * @param bool $isRecursive if there is/are distribution list(s) inside the distlist |
||
4025 | * to expand all the members, pass true to expand distlist recursively, false to not expand |
||
4026 | * |
||
4027 | * @return array $members all members of a distribution list |
||
4028 | */ |
||
4029 | public function expandDistList($distlistEntryid, $isRecursive = false) { |
||
4030 | $properties = $GLOBALS['properties']->getDistListProperties(); |
||
4031 | $eidObj = $GLOBALS['entryid']->createABEntryIdObj($distlistEntryid); |
||
4032 | $isMuidGuid = !$GLOBALS['entryid']->hasNoMuid('', $eidObj); |
||
4033 | $extidObj = $isMuidGuid ? |
||
4034 | $GLOBALS['entryid']->createMessageEntryIdObj($eidObj['extid']) : |
||
4035 | $GLOBALS['entryid']->createMessageEntryIdObj($GLOBALS['entryid']->createMessageEntryId($eidObj)); |
||
4036 | |||
4037 | $store = $GLOBALS["mapisession"]->getDefaultMessageStore(); |
||
4038 | $contactFolderId = $this->getPropertiesFromStoreRoot($store, [PR_IPM_CONTACT_ENTRYID]); |
||
4039 | $contactFolderidObj = $GLOBALS['entryid']->createFolderEntryIdObj(bin2hex((string) $contactFolderId[PR_IPM_CONTACT_ENTRYID])); |
||
4040 | |||
4041 | if ($contactFolderidObj['providerguid'] != $extidObj['providerguid'] && $contactFolderidObj['folderdbguid'] != $extidObj['folderdbguid']) { |
||
4042 | $storelist = $GLOBALS["mapisession"]->getAllMessageStores(); |
||
4043 | foreach ($storelist as $storeObj) { |
||
4044 | $contactFolderId = $this->getPropertiesFromStoreRoot($storeObj, [PR_IPM_CONTACT_ENTRYID]); |
||
4045 | if (isset($contactFolderId[PR_IPM_CONTACT_ENTRYID])) { |
||
4046 | $contactFolderidObj = $GLOBALS['entryid']->createFolderEntryIdObj(bin2hex((string) $contactFolderId[PR_IPM_CONTACT_ENTRYID])); |
||
4047 | if ($contactFolderidObj['providerguid'] == $extidObj['providerguid'] && $contactFolderidObj['folderdbguid'] == $extidObj['folderdbguid']) { |
||
4048 | $store = $storeObj; |
||
4049 | break; |
||
4050 | } |
||
4051 | } |
||
4052 | } |
||
4053 | } |
||
4054 | |||
4055 | if ($isMuidGuid) { |
||
4056 | $distlistEntryid = $GLOBALS["entryid"]->unwrapABEntryIdObj($distlistEntryid); |
||
4057 | } |
||
4058 | |||
4059 | try { |
||
4060 | $distlist = $this->openMessage($store, hex2bin((string) $distlistEntryid)); |
||
4061 | } |
||
4062 | catch (Exception) { |
||
4063 | // the distribution list is in a public folder |
||
4064 | $distlist = $this->openMessage($GLOBALS["mapisession"]->getPublicMessageStore(), hex2bin((string) $distlistEntryid)); |
||
4065 | } |
||
4066 | |||
4067 | // Retrieve the members from distribution list. |
||
4068 | $distlistMembers = $this->getMembersFromDistributionList($store, $distlist, $properties, $isRecursive); |
||
4069 | $recipients = []; |
||
4070 | |||
4071 | foreach ($distlistMembers as $member) { |
||
4072 | $props = $this->convertDistlistMemberToRecipient($store, $member); |
||
4073 | array_push($recipients, $props); |
||
4074 | } |
||
4075 | |||
4076 | return $recipients; |
||
4077 | } |
||
4078 | |||
4079 | /** |
||
4080 | * Function Which convert the shared/internal(local contact folder distlist) |
||
4081 | * folder's distlist members to recipient type. |
||
4082 | * |
||
4083 | * @param mapistore $store MAPI store of the message |
||
4084 | * @param array $member of distribution list contacts |
||
4085 | * |
||
4086 | * @return array members properties converted in to recipient |
||
4087 | */ |
||
4088 | public function convertDistlistMemberToRecipient($store, $member) { |
||
4089 | $entryid = $member["props"]["entryid"]; |
||
4090 | $memberProps = $member["props"]; |
||
4091 | $props = []; |
||
4092 | |||
4093 | $distlistType = $memberProps["distlist_type"]; |
||
4094 | $addressType = $memberProps["address_type"]; |
||
4095 | |||
4096 | $isGABDistlList = $distlistType == DL_DIST_AB && $addressType === "EX"; |
||
4097 | $isLocalDistlist = $distlistType == DL_DIST && $addressType === "MAPIPDL"; |
||
4098 | |||
4099 | $isGABContact = $memberProps["address_type"] === 'EX'; |
||
4100 | // If distlist_type is 0 then it means distlist member is external contact. |
||
4101 | // For mare please read server/core/constants.php |
||
4102 | $isLocalContact = !$isGABContact && $distlistType !== 0; |
||
4103 | |||
4104 | /* |
||
4105 | * If distribution list belongs to the local contact folder then open that contact and |
||
4106 | * retrieve all properties which requires to prepare ideal recipient to send mail. |
||
4107 | */ |
||
4108 | if ($isLocalDistlist) { |
||
4109 | try { |
||
4110 | $distlist = $this->openMessage($store, hex2bin((string) $entryid)); |
||
4111 | } |
||
4112 | catch (Exception) { |
||
4113 | $distlist = $this->openMessage($GLOBALS["mapisession"]->getPublicMessageStore(), hex2bin((string) $entryid)); |
||
4114 | } |
||
4115 | |||
4116 | $abProps = $this->getProps($distlist, $GLOBALS['properties']->getRecipientProperties()); |
||
4117 | $props = $abProps["props"]; |
||
4118 | |||
4119 | $props["entryid"] = $GLOBALS["entryid"]->wrapABEntryIdObj($abProps["entryid"], MAPI_DISTLIST); |
||
4120 | $props["display_type"] = DT_DISTLIST; |
||
4121 | $props["display_type_ex"] = DT_DISTLIST; |
||
4122 | $props["address_type"] = $memberProps["address_type"]; |
||
4123 | $emailAddress = !empty($memberProps["email_address"]) ? $memberProps["email_address"] : ""; |
||
4124 | $props["smtp_address"] = $emailAddress; |
||
4125 | $props["email_address"] = $emailAddress; |
||
4126 | } |
||
4127 | elseif ($isGABContact || $isGABDistlList) { |
||
4128 | /* |
||
4129 | * If contact or distribution list belongs to GAB then open that contact and |
||
4130 | * retrieve all properties which requires to prepare ideal recipient to send mail. |
||
4131 | */ |
||
4132 | try { |
||
4133 | $abentry = mapi_ab_openentry($GLOBALS["mapisession"]->getAddressbook(), hex2bin((string) $entryid)); |
||
4134 | $abProps = $this->getProps($abentry, $GLOBALS['properties']->getRecipientProperties()); |
||
4135 | $props = $abProps["props"]; |
||
4136 | $props["entryid"] = $abProps["entryid"]; |
||
4137 | } |
||
4138 | catch (Exception $e) { |
||
4139 | // Throw MAPI_E_NOT_FOUND or MAPI_E_UNKNOWN_ENTRYID it may possible that contact is already |
||
4140 | // deleted from server. so just create recipient |
||
4141 | // with existing information of distlist member. |
||
4142 | // recipient is not valid so sender get report mail for that |
||
4143 | // particular recipient to inform that recipient is not exist. |
||
4144 | if ($e->getCode() == MAPI_E_NOT_FOUND || $e->getCode() == MAPI_E_UNKNOWN_ENTRYID) { |
||
4145 | $props["entryid"] = $memberProps["entryid"]; |
||
4146 | $props["display_type"] = DT_MAILUSER; |
||
4147 | $props["display_type_ex"] = DT_MAILUSER; |
||
4148 | $props["display_name"] = $memberProps["display_name"]; |
||
4149 | $props["smtp_address"] = $memberProps["email_address"]; |
||
4150 | $props["email_address"] = $memberProps["email_address"]; |
||
4151 | $props["address_type"] = !empty($memberProps["address_type"]) ? $memberProps["address_type"] : 'SMTP'; |
||
4152 | } |
||
4153 | else { |
||
4154 | throw $e; |
||
4155 | } |
||
4156 | } |
||
4157 | } |
||
4158 | else { |
||
4159 | /* |
||
4160 | * If contact is belongs to local/shared folder then prepare ideal recipient to send mail |
||
4161 | * as per the contact type. |
||
4162 | */ |
||
4163 | $props["entryid"] = $isLocalContact ? $GLOBALS["entryid"]->wrapABEntryIdObj($entryid, MAPI_MAILUSER) : $memberProps["entryid"]; |
||
4164 | $props["display_type"] = DT_MAILUSER; |
||
4165 | $props["display_type_ex"] = $isLocalContact ? DT_MAILUSER : DT_REMOTE_MAILUSER; |
||
4166 | $props["display_name"] = $memberProps["display_name"]; |
||
4167 | $props["smtp_address"] = $memberProps["email_address"]; |
||
4168 | $props["email_address"] = $memberProps["email_address"]; |
||
4169 | $props["address_type"] = !empty($memberProps["address_type"]) ? $memberProps["address_type"] : 'SMTP'; |
||
4170 | } |
||
4171 | |||
4172 | // Set object type property into each member of distribution list |
||
4173 | $props["object_type"] = $this->getObjectTypeFromDistlistType($memberProps["distlist_type"]); |
||
4174 | |||
4175 | return $props; |
||
4176 | } |
||
4177 | |||
4178 | /** |
||
4179 | * Parse reply-to value from PR_REPLY_RECIPIENT_ENTRIES property. |
||
4180 | * |
||
4181 | * @param string $flatEntryList the PR_REPLY_RECIPIENT_ENTRIES value |
||
4182 | * |
||
4183 | * @return array list of recipients in array structure |
||
4184 | */ |
||
4185 | public function readReplyRecipientEntry($flatEntryList) { |
||
4186 | $addressbook = $GLOBALS["mapisession"]->getAddressbook(); |
||
4187 | $entryids = []; |
||
4188 | |||
4189 | // Unpack number of entries, the byte count and the entries |
||
4190 | $unpacked = unpack('V1cEntries/V1cbEntries/a*', $flatEntryList); |
||
4191 | |||
4192 | // $unpacked consists now of the following fields: |
||
4193 | // 'cEntries' => The number of entryids in our list |
||
4194 | // 'cbEntries' => The total number of bytes inside 'abEntries' |
||
4195 | // 'abEntries' => The list of Entryids |
||
4196 | // |
||
4197 | // Each 'abEntries' can be broken down into groups of 2 fields |
||
4198 | // 'cb' => The length of the entryid |
||
4199 | // 'entryid' => The entryid |
||
4200 | |||
4201 | $position = 8; // sizeof(cEntries) + sizeof(cbEntries); |
||
4202 | |||
4203 | for ($i = 0, $len = $unpacked['cEntries']; $i < $len; ++$i) { |
||
4204 | // Obtain the size for the current entry |
||
4205 | $size = unpack('a' . $position . '/V1cb/a*', $flatEntryList); |
||
4206 | |||
4207 | // We have the size, now can obtain the bytes |
||
4208 | $entryid = unpack('a' . $position . '/V1cb/a' . $size['cb'] . 'entryid/a*', $flatEntryList); |
||
4209 | |||
4210 | // unpack() will remove the NULL characters, re-add |
||
4211 | // them until we match the 'cb' length. |
||
4212 | while ($entryid['cb'] > strlen((string) $entryid['entryid'])) { |
||
4213 | $entryid['entryid'] .= chr(0x00); |
||
4214 | } |
||
4215 | |||
4216 | $entryids[] = $entryid['entryid']; |
||
4217 | |||
4218 | // sizeof(cb) + strlen(entryid) |
||
4219 | $position += 4 + $entryid['cb']; |
||
4220 | } |
||
4221 | |||
4222 | $recipients = []; |
||
4223 | foreach ($entryids as $entryid) { |
||
4224 | // Check if entryid extracted, since unpack errors can not be caught. |
||
4225 | if (!$entryid) { |
||
4226 | continue; |
||
4227 | } |
||
4228 | |||
4229 | // Handle malformed entryids |
||
4230 | try { |
||
4231 | $entry = mapi_ab_openentry($addressbook, $entryid); |
||
4232 | $props = mapi_getprops($entry, [PR_ENTRYID, PR_SEARCH_KEY, PR_OBJECT_TYPE, PR_DISPLAY_NAME, PR_ADDRTYPE, PR_EMAIL_ADDRESS]); |
||
4233 | |||
4234 | // Put data in recipient array |
||
4235 | $recipients[] = $this->composeRecipient(count($recipients), $props); |
||
4236 | } |
||
4237 | catch (MAPIException $e) { |
||
4238 | try { |
||
4239 | $oneoff = mapi_parseoneoff($entryid); |
||
4240 | } |
||
4241 | catch (MAPIException $ex) { |
||
4242 | error_log(sprintf( |
||
4243 | "readReplyRecipientEntry unable to open AB entry and mapi_parseoneoff failed: %s - %s", |
||
4244 | get_mapi_error_name($ex->getCode()), |
||
4245 | $ex->getDisplayMessage() |
||
4246 | )); |
||
4247 | |||
4248 | continue; |
||
4249 | } |
||
4250 | if (!isset($oneoff['address'])) { |
||
4251 | error_log(sprintf( |
||
4252 | "readReplyRecipientEntry unable to open AB entry and oneoff address is not available: %s - %s ", |
||
4253 | get_mapi_error_name($e->getCode()), |
||
4254 | $e->getDisplayMessage() |
||
4255 | )); |
||
4256 | |||
4257 | continue; |
||
4258 | } |
||
4259 | |||
4260 | $entryid = mapi_createoneoff($oneoff['name'] ?? '', $oneoff['type'] ?? 'SMTP', $oneoff['address']); |
||
4261 | $props = [ |
||
4262 | PR_ENTRYID => $entryid, |
||
4263 | PR_DISPLAY_NAME => !empty($oneoff['name']) ? $oneoff['name'] : $oneoff['address'], |
||
4264 | PR_ADDRTYPE => $oneoff['type'] ?? 'SMTP', |
||
4265 | PR_EMAIL_ADDRESS => $oneoff['address'], |
||
4266 | ]; |
||
4267 | $recipients[] = $this->composeRecipient(count($recipients), $props); |
||
4268 | } |
||
4269 | } |
||
4270 | |||
4271 | return $recipients; |
||
4272 | } |
||
4273 | |||
4274 | private function composeRecipient($rowid, $props) { |
||
4275 | return [ |
||
4276 | 'rowid' => $rowid, |
||
4277 | 'props' => [ |
||
4278 | 'entryid' => !empty($props[PR_ENTRYID]) ? bin2hex((string) $props[PR_ENTRYID]) : '', |
||
4279 | 'object_type' => $props[PR_OBJECT_TYPE] ?? MAPI_MAILUSER, |
||
4280 | 'search_key' => $props[PR_SEARCH_KEY] ?? '', |
||
4281 | 'display_name' => !empty($props[PR_DISPLAY_NAME]) ? $props[PR_DISPLAY_NAME] : $props[PR_EMAIL_ADDRESS], |
||
4282 | 'address_type' => $props[PR_ADDRTYPE] ?? 'SMTP', |
||
4283 | 'email_address' => $props[PR_EMAIL_ADDRESS] ?? '', |
||
4284 | 'smtp_address' => $props[PR_EMAIL_ADDRESS] ?? '', |
||
4285 | ], |
||
4286 | ]; |
||
4287 | } |
||
4288 | |||
4289 | /** |
||
4290 | * Build full-page HTML from the TinyMCE HTML. |
||
4291 | * |
||
4292 | * This function basically takes the generated HTML from TinyMCE and embeds it in |
||
4293 | * a standalone HTML page (including header and CSS) to form. |
||
4294 | * |
||
4295 | * @param string $body This is the HTML created by the TinyMCE |
||
4296 | * @param string $title Optional, this string is placed in the <title> |
||
4297 | * |
||
4298 | * @return string full HTML message |
||
4299 | */ |
||
4300 | public function generateBodyHTML($body, $title = "grommunio-web") { |
||
4301 | $html = "<!DOCTYPE html>" . |
||
4302 | "<html>\n" . |
||
4303 | "<head>\n" . |
||
4304 | " <meta name=\"Generator\" content=\"grommunio-web v" . trim(file_get_contents('version')) . "\">\n" . |
||
4305 | " <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\n" . |
||
4306 | " <title>" . htmlspecialchars($title) . "</title>\n"; |
||
4307 | |||
4308 | $html .= "</head>\n" . |
||
4309 | "<body>\n" . |
||
4310 | $body . "\n" . |
||
4311 | "</body>\n" . |
||
4312 | "</html>"; |
||
4313 | |||
4314 | return $html; |
||
4315 | } |
||
4316 | |||
4317 | /** |
||
4318 | * Calculate the total size for all items in the given folder. |
||
4319 | * |
||
4320 | * @param mapifolder $folder The folder for which the size must be calculated |
||
4321 | * |
||
4322 | * @return number The folder size |
||
4323 | */ |
||
4324 | public function calcFolderMessageSize($folder) { |
||
4325 | $folderProps = mapi_getprops($folder, [PR_MESSAGE_SIZE_EXTENDED]); |
||
4326 | |||
4327 | return $folderProps[PR_MESSAGE_SIZE_EXTENDED] ?? 0; |
||
4328 | } |
||
4329 | |||
4330 | /** |
||
4331 | * Detect plaintext body type of message. |
||
4332 | * |
||
4333 | * @param mapimessage $message MAPI message resource to check |
||
4334 | * |
||
4335 | * @return bool TRUE if the message is a plaintext message, FALSE if otherwise |
||
4336 | */ |
||
4337 | public function isPlainText($message) { |
||
4338 | $props = mapi_getprops($message, [PR_NATIVE_BODY_INFO]); |
||
4339 | if (isset($props[PR_NATIVE_BODY_INFO]) && $props[PR_NATIVE_BODY_INFO] == 1) { |
||
4340 | return true; |
||
4341 | } |
||
4342 | |||
4343 | return false; |
||
4344 | } |
||
4345 | |||
4346 | /** |
||
4347 | * Parse email recipient list and add all e-mail addresses to the recipient history. |
||
4348 | * |
||
4349 | * The recipient history is used for auto-suggestion when writing e-mails. This function |
||
4350 | * opens the recipient history property (PR_EC_RECIPIENT_HISTORY_JSON) and updates or appends |
||
4351 | * it with the passed email addresses. |
||
4352 | * |
||
4353 | * @param array $recipients list of recipients |
||
4354 | */ |
||
4355 | public function addRecipientsToRecipientHistory($recipients) { |
||
4356 | $emailAddress = []; |
||
4357 | foreach ($recipients as $key => $value) { |
||
4358 | $emailAddresses[] = $value['props']; |
||
4359 | } |
||
4360 | |||
4361 | if (empty($emailAddresses)) { |
||
4362 | return; |
||
4363 | } |
||
4364 | |||
4365 | // Retrieve the recipient history |
||
4366 | $store = $GLOBALS["mapisession"]->getDefaultMessageStore(); |
||
4367 | $storeProps = mapi_getprops($store, [PR_EC_RECIPIENT_HISTORY_JSON]); |
||
4368 | $recipient_history = []; |
||
4369 | |||
4370 | if (isset($storeProps[PR_EC_RECIPIENT_HISTORY_JSON]) || propIsError(PR_EC_RECIPIENT_HISTORY_JSON, $storeProps) == MAPI_E_NOT_ENOUGH_MEMORY) { |
||
4371 | $datastring = streamProperty($store, PR_EC_RECIPIENT_HISTORY_JSON); |
||
4372 | |||
4373 | if (!empty($datastring)) { |
||
4374 | $recipient_history = json_decode_data($datastring, true); |
||
4375 | } |
||
4376 | } |
||
4377 | |||
4378 | $l_aNewHistoryItems = []; |
||
4379 | // Loop through all new recipients |
||
4380 | for ($i = 0, $len = count($emailAddresses); $i < $len; ++$i) { |
||
4381 | if ($emailAddresses[$i]['address_type'] == 'SMTP') { |
||
4382 | $emailAddress = $emailAddresses[$i]['smtp_address']; |
||
4383 | if (empty($emailAddress)) { |
||
4384 | $emailAddress = $emailAddresses[$i]['email_address']; |
||
4385 | } |
||
4386 | } |
||
4387 | else { // address_type == 'EX' || address_type == 'MAPIPDL' |
||
4388 | $emailAddress = $emailAddresses[$i]['email_address']; |
||
4389 | if (empty($emailAddress)) { |
||
4390 | $emailAddress = $emailAddresses[$i]['smtp_address']; |
||
4391 | } |
||
4392 | } |
||
4393 | |||
4394 | // If no email address property is found, then we can't |
||
4395 | // generate a valid suggestion. |
||
4396 | if (empty($emailAddress)) { |
||
4397 | continue; |
||
4398 | } |
||
4399 | |||
4400 | $l_bFoundInHistory = false; |
||
4401 | // Loop through all the recipients in history |
||
4402 | if (is_array($recipient_history) && !empty($recipient_history['recipients'])) { |
||
4403 | for ($j = 0, $lenJ = count($recipient_history['recipients']); $j < $lenJ; ++$j) { |
||
4404 | // Email address already found in history |
||
4405 | $l_bFoundInHistory = false; |
||
4406 | |||
4407 | // The address_type property must exactly match, |
||
4408 | // when it does, a recipient matches the suggestion |
||
4409 | // if it matches to either the email_address or smtp_address. |
||
4410 | if ($emailAddresses[$i]['address_type'] === $recipient_history['recipients'][$j]['address_type']) { |
||
4411 | if ($emailAddress == $recipient_history['recipients'][$j]['email_address'] || |
||
4412 | $emailAddress == $recipient_history['recipients'][$j]['smtp_address']) { |
||
4413 | $l_bFoundInHistory = true; |
||
4414 | } |
||
4415 | } |
||
4416 | |||
4417 | if ($l_bFoundInHistory === true) { |
||
4418 | // Check if a name has been supplied. |
||
4419 | $newDisplayName = trim((string) $emailAddresses[$i]['display_name']); |
||
4420 | if (!empty($newDisplayName)) { |
||
4421 | $oldDisplayName = trim((string) $recipient_history['recipients'][$j]['display_name']); |
||
4422 | |||
4423 | // Check if the name is not the same as the email address |
||
4424 | if ($newDisplayName != $emailAddresses[$i]['smtp_address']) { |
||
4425 | $recipient_history['recipients'][$j]['display_name'] = $newDisplayName; |
||
4426 | // Check if the recipient history has no name for this email |
||
4427 | } |
||
4428 | elseif (empty($oldDisplayName)) { |
||
4429 | $recipient_history['recipients'][$j]['display_name'] = $newDisplayName; |
||
4430 | } |
||
4431 | } |
||
4432 | ++$recipient_history['recipients'][$j]['count']; |
||
4433 | $recipient_history['recipients'][$j]['last_used'] = time(); |
||
4434 | break; |
||
4435 | } |
||
4436 | } |
||
4437 | } |
||
4438 | if (!$l_bFoundInHistory && !isset($l_aNewHistoryItems[$emailAddress])) { |
||
4439 | $l_aNewHistoryItems[$emailAddress] = [ |
||
4440 | 'display_name' => $emailAddresses[$i]['display_name'], |
||
4441 | 'smtp_address' => $emailAddresses[$i]['smtp_address'], |
||
4442 | 'email_address' => $emailAddresses[$i]['email_address'], |
||
4443 | 'address_type' => $emailAddresses[$i]['address_type'], |
||
4444 | 'count' => 1, |
||
4445 | 'last_used' => time(), |
||
4446 | 'object_type' => $emailAddresses[$i]['object_type'], |
||
4447 | ]; |
||
4448 | } |
||
4449 | } |
||
4450 | if (!empty($l_aNewHistoryItems)) { |
||
4451 | foreach ($l_aNewHistoryItems as $l_aValue) { |
||
4452 | $recipient_history['recipients'][] = $l_aValue; |
||
4453 | } |
||
4454 | } |
||
4455 | |||
4456 | $l_sNewRecipientHistoryJSON = json_encode($recipient_history); |
||
4457 | |||
4458 | $stream = mapi_openproperty($store, PR_EC_RECIPIENT_HISTORY_JSON, IID_IStream, 0, MAPI_CREATE | MAPI_MODIFY); |
||
4459 | mapi_stream_setsize($stream, strlen($l_sNewRecipientHistoryJSON)); |
||
4460 | mapi_stream_write($stream, $l_sNewRecipientHistoryJSON); |
||
4461 | mapi_stream_commit($stream); |
||
4462 | mapi_savechanges($store); |
||
4463 | } |
||
4464 | |||
4465 | /** |
||
4466 | * Get the SMTP e-mail of an addressbook entry. |
||
4467 | * |
||
4468 | * @param string $entryid Addressbook entryid of object |
||
4469 | * |
||
4470 | * @return string SMTP e-mail address of that entry or FALSE on error |
||
4471 | */ |
||
4472 | public function getEmailAddressFromEntryID($entryid) { |
||
4473 | try { |
||
4474 | $mailuser = mapi_ab_openentry($GLOBALS["mapisession"]->getAddressbook(), $entryid); |
||
4475 | } |
||
4476 | catch (MAPIException $e) { |
||
4477 | // if any invalid entryid is passed in this function then it should silently ignore it |
||
4478 | // and continue with execution |
||
4479 | if ($e->getCode() == MAPI_E_UNKNOWN_ENTRYID) { |
||
4480 | $e->setHandled(); |
||
4481 | |||
4482 | return ""; |
||
4483 | } |
||
4484 | } |
||
4485 | |||
4486 | if (!isset($mailuser)) { |
||
4487 | return ""; |
||
4488 | } |
||
4489 | |||
4490 | $abprops = mapi_getprops($mailuser, [PR_SMTP_ADDRESS, PR_EMAIL_ADDRESS]); |
||
4491 | |||
4492 | return $abprops[PR_SMTP_ADDRESS] ?? $abprops[PR_EMAIL_ADDRESS] ?? ""; |
||
4493 | } |
||
4494 | |||
4495 | /** |
||
4496 | * Function which fetches all members of a distribution list recursively. |
||
4497 | * |
||
4498 | * @param resource $store MAPI Message Store Object |
||
4499 | * @param resource $message the distribution list message |
||
4500 | * @param array $properties array of properties to get properties of distlist |
||
4501 | * @param bool $isRecursive function will be called recursively if there is/are |
||
4502 | * distribution list inside the distlist to expand all the members, |
||
4503 | * pass true to expand distlist recursively, false to not expand |
||
4504 | * @param array $listEntryIDs list of already expanded Distribution list from contacts folder, |
||
4505 | * This parameter is used for recursive call of the function |
||
4506 | * |
||
4507 | * @return object $items all members of a distlist |
||
4508 | */ |
||
4509 | public function getMembersFromDistributionList($store, $message, $properties, $isRecursive = false, $listEntryIDs = []) { |
||
4510 | $items = []; |
||
4511 | |||
4512 | $props = mapi_getprops($message, [$properties['oneoff_members'], $properties['members'], PR_ENTRYID]); |
||
4513 | |||
4514 | // only continue when we have something to expand |
||
4515 | if (!isset($props[$properties['oneoff_members']]) || !isset($props[$properties['members']])) { |
||
4516 | return []; |
||
4517 | } |
||
4518 | |||
4519 | if ($isRecursive) { |
||
4520 | // when opening sub message we will not have entryid, so use entryid only when we have it |
||
4521 | if (isset($props[PR_ENTRYID])) { |
||
4522 | // for preventing recursion we need to store entryids, and check if the same distlist is going to be expanded again |
||
4523 | if (in_array($props[PR_ENTRYID], $listEntryIDs)) { |
||
4524 | // don't expand a distlist that is already expanded |
||
4525 | return []; |
||
4526 | } |
||
4527 | |||
4528 | $listEntryIDs[] = $props[PR_ENTRYID]; |
||
4529 | } |
||
4530 | } |
||
4531 | |||
4532 | $members = $props[$properties['members']]; |
||
4533 | |||
4534 | // parse oneoff members |
||
4535 | $oneoffmembers = []; |
||
4536 | foreach ($props[$properties['oneoff_members']] as $key => $item) { |
||
4537 | $oneoffmembers[$key] = mapi_parseoneoff($item); |
||
4538 | } |
||
4539 | |||
4540 | foreach ($members as $key => $item) { |
||
4541 | /* |
||
4542 | * PHP 5.5.0 and greater has made the unpack function incompatible with previous versions by changing: |
||
4543 | * - a = code now retains trailing NULL bytes. |
||
4544 | * - A = code now strips all trailing ASCII whitespace (spaces, tabs, newlines, carriage |
||
4545 | * returns, and NULL bytes). |
||
4546 | * for more http://php.net/manual/en/function.unpack.php |
||
4547 | */ |
||
4548 | if (version_compare(PHP_VERSION, '5.5.0', '>=')) { |
||
4549 | $parts = unpack('Vnull/A16guid/Ctype/a*entryid', (string) $item); |
||
4550 | } |
||
4551 | else { |
||
4552 | $parts = unpack('Vnull/A16guid/Ctype/A*entryid', (string) $item); |
||
4553 | } |
||
4554 | |||
4555 | $memberItem = []; |
||
4556 | $memberItem['props'] = []; |
||
4557 | $memberItem['props']['distlist_type'] = $parts['type']; |
||
4558 | |||
4559 | if ($parts['guid'] === hex2bin('812b1fa4bea310199d6e00dd010f5402')) { |
||
4560 | // custom e-mail address (no user or contact) |
||
4561 | $oneoff = mapi_parseoneoff($item); |
||
4562 | |||
4563 | $memberItem['props']['display_name'] = $oneoff['name']; |
||
4564 | $memberItem['props']['address_type'] = $oneoff['type']; |
||
4565 | $memberItem['props']['email_address'] = $oneoff['address']; |
||
4566 | $memberItem['props']['smtp_address'] = $oneoff['address']; |
||
4567 | $memberItem['props']['entryid'] = bin2hex((string) $members[$key]); |
||
4568 | |||
4569 | $items[] = $memberItem; |
||
4570 | } |
||
4571 | else { |
||
4572 | if ($parts['type'] === DL_DIST && $isRecursive) { |
||
4573 | // Expand distribution list to get distlist members inside the distributionlist. |
||
4574 | $distlist = mapi_msgstore_openentry($store, $parts['entryid']); |
||
4575 | $items = array_merge($items, $this->getMembersFromDistributionList($store, $distlist, $properties, true, $listEntryIDs)); |
||
4576 | } |
||
4577 | else { |
||
4578 | $memberItem['props']['entryid'] = bin2hex((string) $parts['entryid']); |
||
4579 | $memberItem['props']['display_name'] = $oneoffmembers[$key]['name']; |
||
4580 | $memberItem['props']['address_type'] = $oneoffmembers[$key]['type']; |
||
4581 | // distribution lists don't have valid email address so ignore that property |
||
4582 | |||
4583 | if ($parts['type'] !== DL_DIST) { |
||
4584 | $memberItem['props']['email_address'] = $oneoffmembers[$key]['address']; |
||
4585 | |||
4586 | // internal members in distribution list don't have smtp address so add add that property |
||
4587 | $memberProps = $this->convertDistlistMemberToRecipient($store, $memberItem); |
||
4588 | $memberItem['props']['smtp_address'] = $memberProps["smtp_address"] ?? $memberProps["email_address"]; |
||
4589 | } |
||
4590 | |||
4591 | $items[] = $memberItem; |
||
4592 | } |
||
4593 | } |
||
4594 | } |
||
4595 | |||
4596 | return $items; |
||
4597 | } |
||
4598 | |||
4599 | /** |
||
4600 | * Convert inline image <img src="data:image/mimetype;.date> links in HTML email |
||
4601 | * to CID embedded images. Which are supported in major mail clients or |
||
4602 | * providers such as outlook.com or gmail.com. |
||
4603 | * |
||
4604 | * grommunio Web now extracts the base64 image, saves it as hidden attachment, |
||
4605 | * replace the img src tag with the 'cid' which corresponds with the attachments |
||
4606 | * cid. |
||
4607 | * |
||
4608 | * @param MAPIMessage $message the distribution list message |
||
4609 | */ |
||
4610 | public function convertInlineImage($message) { |
||
4611 | $body = streamProperty($message, PR_HTML); |
||
4612 | $imageIDs = []; |
||
4613 | |||
4614 | // Only load the DOM if the HTML contains a img or data:text/plain due to a bug |
||
4615 | // in Chrome on Windows in combination with TinyMCE. |
||
4616 | if (str_contains($body, "img") || str_contains($body, "data:text/plain")) { |
||
4617 | $doc = new DOMDocument(); |
||
4618 | $cpprops = mapi_message_getprops($message, [PR_INTERNET_CPID]); |
||
4619 | $codepage = $cpprops[PR_INTERNET_CPID] ?? 1252; |
||
4620 | $hackEncoding = '<meta http-equiv="Content-Type" content="text/html; charset=' . Conversion::getCodepageCharset($codepage) . '">'; |
||
4621 | // TinyMCE does not generate valid HTML, so we must suppress warnings. |
||
4622 | @$doc->loadHTML($hackEncoding . $body); |
||
4623 | $images = $doc->getElementsByTagName('img'); |
||
4624 | $saveChanges = false; |
||
4625 | |||
4626 | foreach ($images as $image) { |
||
4627 | $src = $image->getAttribute('src'); |
||
4628 | |||
4629 | if (!str_contains($src, "cid:") && (str_contains($src, "data:image") || |
||
4630 | str_contains($body, "data:text/plain"))) { |
||
4631 | $saveChanges = true; |
||
4632 | |||
4633 | // Extract mime type data:image/jpeg; |
||
4634 | $firstOffset = strpos($src, '/') + 1; |
||
4635 | $endOffset = strpos($src, ';'); |
||
4636 | $mimeType = substr($src, $firstOffset, $endOffset - $firstOffset); |
||
4637 | |||
4638 | $dataPosition = strpos($src, ","); |
||
4639 | // Extract encoded data |
||
4640 | $rawImage = base64_decode(substr($src, $dataPosition + 1, strlen($src))); |
||
4641 | |||
4642 | $uniqueId = uniqid(); |
||
4643 | $image->setAttribute('src', 'cid:' . $uniqueId); |
||
4644 | // TinyMCE adds an extra inline image for some reason, remove it. |
||
4645 | $image->setAttribute('data-mce-src', ''); |
||
4646 | |||
4647 | array_push($imageIDs, $uniqueId); |
||
4648 | |||
4649 | // Create hidden attachment with CID |
||
4650 | $inlineImage = mapi_message_createattach($message); |
||
4651 | $props = [ |
||
4652 | PR_ATTACH_METHOD => ATTACH_BY_VALUE, |
||
4653 | PR_ATTACH_CONTENT_ID => $uniqueId, |
||
4654 | PR_ATTACHMENT_HIDDEN => true, |
||
4655 | PR_ATTACH_FLAGS => 4, |
||
4656 | PR_ATTACH_MIME_TAG => $mimeType !== 'plain' ? 'image/' . $mimeType : 'image/png', |
||
4657 | ]; |
||
4658 | mapi_setprops($inlineImage, $props); |
||
4659 | |||
4660 | $stream = mapi_openproperty($inlineImage, PR_ATTACH_DATA_BIN, IID_IStream, 0, MAPI_CREATE | MAPI_MODIFY); |
||
4661 | mapi_stream_setsize($stream, strlen($rawImage)); |
||
4662 | mapi_stream_write($stream, $rawImage); |
||
4663 | mapi_stream_commit($stream); |
||
4664 | mapi_savechanges($inlineImage); |
||
4665 | } |
||
4666 | elseif (str_contains($src, "cid:")) { |
||
4667 | // Check for the cid(there may be http: ) is in the image src. push the cid |
||
4668 | // to $imageIDs array. which further used in clearDeletedInlineAttachments function. |
||
4669 | |||
4670 | $firstOffset = strpos($src, ":") + 1; |
||
4671 | $cid = substr($src, $firstOffset); |
||
4672 | array_push($imageIDs, $cid); |
||
4673 | } |
||
4674 | } |
||
4675 | |||
4676 | if ($saveChanges) { |
||
4677 | // Write the <img src="cid:data"> changes to the HTML property |
||
4678 | $body = $doc->saveHTML(); |
||
4679 | $stream = mapi_openproperty($message, PR_HTML, IID_IStream, 0, MAPI_MODIFY); |
||
4680 | mapi_stream_setsize($stream, strlen($body)); |
||
4681 | mapi_stream_write($stream, $body); |
||
4682 | mapi_stream_commit($stream); |
||
4683 | mapi_savechanges($message); |
||
4684 | } |
||
4685 | } |
||
4686 | $this->clearDeletedInlineAttachments($message, $imageIDs); |
||
4687 | } |
||
4688 | |||
4689 | /** |
||
4690 | * Delete the deleted inline image attachment from attachment store. |
||
4691 | * |
||
4692 | * @param MAPIMessage $message the distribution list message |
||
4693 | * @param array $imageIDs Array of existing inline image PR_ATTACH_CONTENT_ID |
||
4694 | */ |
||
4695 | public function clearDeletedInlineAttachments($message, $imageIDs = []) { |
||
4696 | $attachmentTable = mapi_message_getattachmenttable($message); |
||
4697 | |||
4698 | $restriction = [RES_AND, [ |
||
4699 | [RES_PROPERTY, |
||
4700 | [ |
||
4701 | RELOP => RELOP_EQ, |
||
4702 | ULPROPTAG => PR_ATTACHMENT_HIDDEN, |
||
4703 | VALUE => [PR_ATTACHMENT_HIDDEN => true], |
||
4704 | ], |
||
4705 | ], |
||
4706 | [RES_EXIST, |
||
4707 | [ |
||
4708 | ULPROPTAG => PR_ATTACH_CONTENT_ID, |
||
4709 | ], |
||
4710 | ], |
||
4711 | ]]; |
||
4712 | |||
4713 | $attachments = mapi_table_queryallrows($attachmentTable, [PR_ATTACH_CONTENT_ID, PR_ATTACH_NUM], $restriction); |
||
4714 | foreach ($attachments as $attachment) { |
||
4715 | $clearDeletedInlineAttach = array_search($attachment[PR_ATTACH_CONTENT_ID], $imageIDs) === false; |
||
4716 | if ($clearDeletedInlineAttach) { |
||
4717 | mapi_message_deleteattach($message, $attachment[PR_ATTACH_NUM]); |
||
4718 | } |
||
4719 | } |
||
4720 | } |
||
4721 | |||
4722 | /** |
||
4723 | * This function will fetch the user from mapi session and retrieve its LDAP image. |
||
4724 | * It will return the compressed image using php's GD library. |
||
4725 | * |
||
4726 | * @param string $userEntryId The user entryid which is going to open |
||
4727 | * @param int $compressedQuality The compression factor ranges from 0 (high) to 100 (low) |
||
4728 | * Default value is set to 10 which is nearly |
||
4729 | * extreme compressed image |
||
4730 | * |
||
4731 | * @return string A base64 encoded string (data url) |
||
4732 | */ |
||
4733 | public function getCompressedUserImage($userEntryId, $compressedQuality = 10) { |
||
4734 | try { |
||
4735 | $user = $GLOBALS['mapisession']->getUser($userEntryId); |
||
4736 | } |
||
4737 | catch (Exception $e) { |
||
4738 | $msg = "Problem while getting a user from the addressbook. Error %s : %s."; |
||
4739 | $formattedMsg = sprintf($msg, $e->getCode(), $e->getMessage()); |
||
4740 | error_log($formattedMsg); |
||
4741 | Log::Write(LOGLEVEL_ERROR, "Operations:getCompressedUserImage() " . $formattedMsg); |
||
4742 | |||
4743 | return ""; |
||
4744 | } |
||
4745 | |||
4746 | $userImageProp = mapi_getprops($user, [PR_EMS_AB_THUMBNAIL_PHOTO]); |
||
4747 | if (isset($userImageProp[PR_EMS_AB_THUMBNAIL_PHOTO])) { |
||
4748 | return $this->compressedImage($userImageProp[PR_EMS_AB_THUMBNAIL_PHOTO], $compressedQuality); |
||
4749 | } |
||
4750 | |||
4751 | return ""; |
||
4752 | } |
||
4753 | |||
4754 | /** |
||
4755 | * Function used to compressed the image. |
||
4756 | * |
||
4757 | * @param string $image the image which is going to compress |
||
4758 | * @param int compressedQuality The compression factor range from 0 (high) to 100 (low) |
||
4759 | * Default value is set to 10 which is nearly extreme compressed image |
||
4760 | * @param mixed $compressedQuality |
||
4761 | * |
||
4762 | * @return string A base64 encoded string (data url) |
||
4763 | */ |
||
4764 | public function compressedImage($image, $compressedQuality = 10) { |
||
4792 | } |
||
4793 | |||
4794 | public function getPropertiesFromStoreRoot($store, $props) { |
||
4795 | $root = mapi_msgstore_openentry($store); |
||
4796 | |||
4797 | return mapi_getprops($root, $props); |
||
4798 | } |
||
4799 | |||
4800 | /** |
||
4801 | * Returns the encryption key for sodium functions. |
||
4802 | * |
||
4803 | * It will generate a new one if the user doesn't have an encryption key yet. |
||
4804 | * It will also save the key into EncryptionStore for this session if the key |
||
4805 | * wasn't there yet. |
||
4806 | * |
||
4807 | * @return string |
||
4808 | */ |
||
4809 | public function getFilesEncryptionKey() { |
||
4831 | } |
||
4832 | } |
||
4833 |