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