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