@@ -21,2359 +21,2359 @@ discard block |
||
| 21 | 21 | setlocale(LC_CTYPE, "en_US.UTF-8"); |
| 22 | 22 | |
| 23 | 23 | class Grommunio extends InterProcessData implements IBackend, ISearchProvider, IStateMachine { |
| 24 | - private $mainUser; |
|
| 25 | - private $session; |
|
| 26 | - private $defaultstore; |
|
| 27 | - private $store; |
|
| 28 | - private $storeName; |
|
| 29 | - private $storeCache; |
|
| 30 | - private $notifications; |
|
| 31 | - private $changesSink; |
|
| 32 | - private $changesSinkFolders; |
|
| 33 | - private $changesSinkHierarchyHash; |
|
| 34 | - private $changesSinkStores; |
|
| 35 | - private $wastebasket; |
|
| 36 | - private $addressbook; |
|
| 37 | - private $folderStatCache; |
|
| 38 | - private $impersonateUser; |
|
| 39 | - private $stateFolder; |
|
| 40 | - private $userDeviceData; |
|
| 41 | - |
|
| 42 | - // KC config parameter for PR_EC_ENABLED_FEATURES / PR_EC_DISABLED_FEATURES |
|
| 43 | - public const MOBILE_ENABLED = 'mobile'; |
|
| 44 | - |
|
| 45 | - public const MAXAMBIGUOUSRECIPIENTS = 9999; |
|
| 46 | - public const FREEBUSYENUMBLOCKS = 50; |
|
| 47 | - public const MAXFREEBUSYSLOTS = 32767; // max length of 32k for the MergedFreeBusy element is allowed |
|
| 48 | - public const HALFHOURSECONDS = 1800; |
|
| 49 | - |
|
| 50 | - /** |
|
| 51 | - * Constructor of the grommunio Backend. |
|
| 52 | - */ |
|
| 53 | - public function __construct() { |
|
| 54 | - $this->session = false; |
|
| 55 | - $this->store = false; |
|
| 56 | - $this->storeName = false; |
|
| 57 | - $this->storeCache = []; |
|
| 58 | - $this->notifications = false; |
|
| 59 | - $this->changesSink = false; |
|
| 60 | - $this->changesSinkFolders = []; |
|
| 61 | - $this->changesSinkStores = []; |
|
| 62 | - $this->changesSinkHierarchyHash = false; |
|
| 63 | - $this->wastebasket = false; |
|
| 64 | - $this->session = false; |
|
| 65 | - $this->folderStatCache = []; |
|
| 66 | - $this->impersonateUser = false; |
|
| 67 | - $this->stateFolder = null; |
|
| 68 | - |
|
| 69 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio using PHP-MAPI version: %s - PHP version: %s", phpversion("mapi"), phpversion())); |
|
| 70 | - |
|
| 71 | - # Interprocessdata |
|
| 72 | - $this->allocate = 0; |
|
| 73 | - $this->type = "grommunio-sync:userdevices"; |
|
| 74 | - $this->userDeviceData = "grommunio-sync:statefoldercache"; |
|
| 75 | - parent::__construct(); |
|
| 76 | - } |
|
| 77 | - |
|
| 78 | - /** |
|
| 79 | - * Indicates which StateMachine should be used. |
|
| 80 | - * |
|
| 81 | - * @return bool Grommunio uses own state machine |
|
| 82 | - */ |
|
| 83 | - public function GetStateMachine() { |
|
| 84 | - return $this; |
|
| 85 | - } |
|
| 86 | - |
|
| 87 | - /** |
|
| 88 | - * Returns the Grommunio as it implements the ISearchProvider interface |
|
| 89 | - * This could be overwritten by the global configuration. |
|
| 90 | - * |
|
| 91 | - * @return object Implementation of ISearchProvider |
|
| 92 | - */ |
|
| 93 | - public function GetSearchProvider() { |
|
| 94 | - return $this; |
|
| 95 | - } |
|
| 96 | - |
|
| 97 | - /** |
|
| 98 | - * Indicates which AS version is supported by the backend. |
|
| 99 | - * |
|
| 100 | - * @return string AS version constant |
|
| 101 | - */ |
|
| 102 | - public function GetSupportedASVersion() { |
|
| 103 | - return GSync::ASV_141; |
|
| 104 | - } |
|
| 105 | - |
|
| 106 | - /** |
|
| 107 | - * Authenticates the user with the configured grommunio server. |
|
| 108 | - * |
|
| 109 | - * @param string $username |
|
| 110 | - * @param string $domain |
|
| 111 | - * @param string $password |
|
| 112 | - * @param mixed $user |
|
| 113 | - * @param mixed $pass |
|
| 114 | - * |
|
| 115 | - * @throws AuthenticationRequiredException |
|
| 116 | - * |
|
| 117 | - * @return bool |
|
| 118 | - */ |
|
| 119 | - public function Logon($user, $domain, $pass) { |
|
| 120 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->Logon(): Trying to authenticate user '%s'..", $user)); |
|
| 121 | - |
|
| 122 | - $this->mainUser = strtolower($user); |
|
| 123 | - // TODO the impersonated user should be passed directly to IBackend->Logon() - ZP-1351 |
|
| 124 | - if (Request::GetImpersonatedUser()) { |
|
| 125 | - $this->impersonateUser = strtolower(Request::GetImpersonatedUser()); |
|
| 126 | - } |
|
| 127 | - |
|
| 128 | - // check if we are impersonating someone |
|
| 129 | - // $defaultUser will be used for $this->defaultStore |
|
| 130 | - if ($this->impersonateUser !== false) { |
|
| 131 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->Logon(): Impersonation active - authenticating: '%s' - impersonating '%s'", $this->mainUser, $this->impersonateUser)); |
|
| 132 | - $defaultUser = $this->impersonateUser; |
|
| 133 | - } |
|
| 134 | - else { |
|
| 135 | - $defaultUser = $this->mainUser; |
|
| 136 | - } |
|
| 137 | - |
|
| 138 | - $deviceId = Request::GetDeviceID(); |
|
| 139 | - |
|
| 140 | - try { |
|
| 141 | - // check if notifications are available in php-mapi |
|
| 142 | - if (function_exists('mapi_feature') && mapi_feature('LOGONFLAGS')) { |
|
| 143 | - // send grommunio-sync version and user agent to ZCP - ZP-589 |
|
| 144 | - if (Utils::CheckMapiExtVersion('7.2.0')) { |
|
| 145 | - $gsync_version = 'Grommunio-Sync_' . @constant('GROMMUNIOSYNC_VERSION'); |
|
| 146 | - $user_agent = ($deviceId) ? GSync::GetDeviceManager()->GetUserAgent() : "unknown"; |
|
| 147 | - $this->session = @mapi_logon_zarafa($this->mainUser, $pass, MAPI_SERVER, null, null, 0, $gsync_version, $user_agent); |
|
| 148 | - } |
|
| 149 | - else { |
|
| 150 | - $this->session = @mapi_logon_zarafa($this->mainUser, $pass, MAPI_SERVER, null, null, 0); |
|
| 151 | - } |
|
| 152 | - $this->notifications = true; |
|
| 153 | - } |
|
| 154 | - // old fashioned session |
|
| 155 | - else { |
|
| 156 | - $this->session = @mapi_logon_zarafa($this->mainUser, $pass, MAPI_SERVER); |
|
| 157 | - $this->notifications = false; |
|
| 158 | - } |
|
| 159 | - |
|
| 160 | - if (mapi_last_hresult()) { |
|
| 161 | - SLog::Write(LOGLEVEL_ERROR, sprintf("Grommunio->Logon(): login failed with error code: 0x%X", mapi_last_hresult())); |
|
| 162 | - if (mapi_last_hresult() == MAPI_E_NETWORK_ERROR) { |
|
| 163 | - throw new ServiceUnavailableException("Error connecting to KC (login)"); |
|
| 164 | - } |
|
| 165 | - } |
|
| 166 | - } |
|
| 167 | - catch (MAPIException $ex) { |
|
| 168 | - throw new AuthenticationRequiredException($ex->getDisplayMessage()); |
|
| 169 | - } |
|
| 170 | - |
|
| 171 | - if (!$this->session) { |
|
| 172 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->Logon(): logon failed for user '%s'", $this->mainUser)); |
|
| 173 | - $this->defaultstore = false; |
|
| 174 | - |
|
| 175 | - return false; |
|
| 176 | - } |
|
| 177 | - |
|
| 178 | - // Get/open default store |
|
| 179 | - $this->defaultstore = $this->openMessageStore($this->mainUser); |
|
| 180 | - |
|
| 181 | - // To impersonate, we overwrite the defaultstore. We still need to open it before we can do that. |
|
| 182 | - if ($this->impersonateUser) { |
|
| 183 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->Logon(): Impersonating user '%s'", $defaultUser)); |
|
| 184 | - $this->defaultstore = $this->openMessageStore($defaultUser); |
|
| 185 | - } |
|
| 186 | - |
|
| 187 | - if (mapi_last_hresult() == MAPI_E_FAILONEPROVIDER) { |
|
| 188 | - throw new ServiceUnavailableException("Error connecting to KC (open store)"); |
|
| 189 | - } |
|
| 190 | - |
|
| 191 | - if ($this->defaultstore === false) { |
|
| 192 | - throw new AuthenticationRequiredException(sprintf("Grommunio->Logon(): User '%s' has no default store", $defaultUser)); |
|
| 193 | - } |
|
| 194 | - |
|
| 195 | - $this->store = $this->defaultstore; |
|
| 196 | - $this->storeName = $defaultUser; |
|
| 197 | - |
|
| 198 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->Logon(): User '%s' is authenticated%s", $this->mainUser, ($this->impersonateUser ? " impersonating '" . $this->impersonateUser . "'" : ''))); |
|
| 199 | - |
|
| 200 | - $this->isGSyncEnabled(); |
|
| 201 | - |
|
| 202 | - // check if this is a Zarafa 7 store with unicode support |
|
| 203 | - MAPIUtils::IsUnicodeStore($this->store); |
|
| 204 | - |
|
| 205 | - // open the state folder |
|
| 206 | - $this->getStateFolder($deviceId); |
|
| 207 | - |
|
| 208 | - return true; |
|
| 209 | - } |
|
| 210 | - |
|
| 211 | - /** |
|
| 212 | - * Setup the backend to work on a specific store or checks ACLs there. |
|
| 213 | - * If only the $store is submitted, all Import/Export/Fetch/Etc operations should be |
|
| 214 | - * performed on this store (switch operations store). |
|
| 215 | - * If the ACL check is enabled, this operation should just indicate the ACL status on |
|
| 216 | - * the submitted store, without changing the store for operations. |
|
| 217 | - * For the ACL status, the currently logged on user MUST have access rights on |
|
| 218 | - * - the entire store - admin access if no folderid is sent, or |
|
| 219 | - * - on a specific folderid in the store (secretary/full access rights). |
|
| 220 | - * |
|
| 221 | - * The ACLcheck MUST fail if a folder of the authenticated user is checked! |
|
| 222 | - * |
|
| 223 | - * @param string $store target store, could contain a "domain\user" value |
|
| 224 | - * @param bool $checkACLonly if set to true, Setup() should just check ACLs |
|
| 225 | - * @param string $folderid if set, only ACLs on this folderid are relevant |
|
| 226 | - * |
|
| 227 | - * @return bool |
|
| 228 | - */ |
|
| 229 | - public function Setup($store, $checkACLonly = false, $folderid = false) { |
|
| 230 | - list($user, $domain) = Utils::SplitDomainUser($store); |
|
| 231 | - |
|
| 232 | - if (!isset($this->mainUser)) { |
|
| 233 | - return false; |
|
| 234 | - } |
|
| 235 | - |
|
| 236 | - $mainUser = $this->mainUser; |
|
| 237 | - // when impersonating we need to check against the impersonated user |
|
| 238 | - if ($this->impersonateUser) { |
|
| 239 | - $mainUser = $this->impersonateUser; |
|
| 240 | - } |
|
| 241 | - |
|
| 242 | - if ($user === false) { |
|
| 243 | - $user = $mainUser; |
|
| 244 | - } |
|
| 245 | - |
|
| 246 | - // This is a special case. A user will get his entire folder structure by the foldersync by default. |
|
| 247 | - // The ACL check is executed when an additional folder is going to be sent to the mobile. |
|
| 248 | - // Configured that way the user could receive the same folderid twice, with two different names. |
|
| 249 | - if ($mainUser == $user && $checkACLonly && $folderid && !$this->impersonateUser) { |
|
| 250 | - SLog::Write(LOGLEVEL_DEBUG, "Grommunio->Setup(): Checking ACLs for folder of the users defaultstore. Fail is forced to avoid folder duplications on mobile."); |
|
| 251 | - |
|
| 252 | - return false; |
|
| 253 | - } |
|
| 254 | - |
|
| 255 | - // get the users store |
|
| 256 | - $userstore = $this->openMessageStore($user); |
|
| 257 | - |
|
| 258 | - // only proceed if a store was found, else return false |
|
| 259 | - if ($userstore) { |
|
| 260 | - // only check permissions |
|
| 261 | - if ($checkACLonly == true) { |
|
| 262 | - // check for admin rights |
|
| 263 | - if (!$folderid) { |
|
| 264 | - if ($user != $this->mainUser) { |
|
| 265 | - if ($this->impersonateUser) { |
|
| 266 | - $storeProps = mapi_getprops($userstore, [PR_IPM_SUBTREE_ENTRYID]); |
|
| 267 | - $rights = $this->HasSecretaryACLs($userstore, '', $storeProps[PR_IPM_SUBTREE_ENTRYID]); |
|
| 268 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->Setup(): Checking for secretary ACLs on root folder of impersonated store '%s': '%s'", $user, Utils::PrintAsString($rights))); |
|
| 269 | - } |
|
| 270 | - else { |
|
| 271 | - $zarafauserinfo = @nsp_getuserinfo($this->mainUser); |
|
| 272 | - $rights = (isset($zarafauserinfo['admin']) && $zarafauserinfo['admin']) ? true : false; |
|
| 273 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->Setup(): Checking for admin ACLs on store '%s': '%s'", $user, Utils::PrintAsString($rights))); |
|
| 274 | - } |
|
| 275 | - } |
|
| 276 | - // the user has always full access to his own store |
|
| 277 | - else { |
|
| 278 | - $rights = true; |
|
| 279 | - SLog::Write(LOGLEVEL_DEBUG, "Grommunio->Setup(): the user has always full access to his own store"); |
|
| 280 | - } |
|
| 281 | - |
|
| 282 | - return $rights; |
|
| 283 | - } |
|
| 284 | - // check permissions on this folder |
|
| 285 | - |
|
| 286 | - $rights = $this->HasSecretaryACLs($userstore, $folderid); |
|
| 287 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->Setup(): Checking for secretary ACLs on '%s' of store '%s': '%s'", $folderid, $user, Utils::PrintAsString($rights))); |
|
| 288 | - |
|
| 289 | - return $rights; |
|
| 290 | - } |
|
| 291 | - |
|
| 292 | - // switch operations store |
|
| 293 | - // this should also be done if called with user = mainuser or user = false |
|
| 294 | - // which means to switch back to the default store |
|
| 295 | - |
|
| 296 | - // switch active store |
|
| 297 | - $this->store = $userstore; |
|
| 298 | - $this->storeName = $user; |
|
| 299 | - |
|
| 300 | - return true; |
|
| 301 | - } |
|
| 302 | - |
|
| 303 | - return false; |
|
| 304 | - } |
|
| 305 | - |
|
| 306 | - /** |
|
| 307 | - * Logs off |
|
| 308 | - * Free/Busy information is updated for modified calendars |
|
| 309 | - * This is done after the synchronization process is completed. |
|
| 310 | - * |
|
| 311 | - * @return bool |
|
| 312 | - */ |
|
| 313 | - public function Logoff() { |
|
| 314 | - return true; |
|
| 315 | - } |
|
| 316 | - |
|
| 317 | - /** |
|
| 318 | - * Returns an array of SyncFolder types with the entire folder hierarchy |
|
| 319 | - * on the server (the array itself is flat, but refers to parents via the 'parent' property. |
|
| 320 | - * |
|
| 321 | - * provides AS 1.0 compatibility |
|
| 322 | - * |
|
| 323 | - * @return array SYNC_FOLDER |
|
| 324 | - */ |
|
| 325 | - public function GetHierarchy() { |
|
| 326 | - $folders = []; |
|
| 327 | - $mapiprovider = new MAPIProvider($this->session, $this->store); |
|
| 328 | - $storeProps = $mapiprovider->GetStoreProps(); |
|
| 329 | - |
|
| 330 | - // for SYSTEM user open the public folders |
|
| 331 | - if (strtoupper($this->storeName) == "SYSTEM") { |
|
| 332 | - $rootfolder = mapi_msgstore_openentry($this->store, $storeProps[PR_IPM_PUBLIC_FOLDERS_ENTRYID]); |
|
| 333 | - } |
|
| 334 | - else { |
|
| 335 | - $rootfolder = mapi_msgstore_openentry($this->store); |
|
| 336 | - } |
|
| 337 | - |
|
| 338 | - $rootfolderprops = mapi_getprops($rootfolder, [PR_SOURCE_KEY]); |
|
| 339 | - |
|
| 340 | - $hierarchy = mapi_folder_gethierarchytable($rootfolder, CONVENIENT_DEPTH); |
|
| 341 | - $rows = mapi_table_queryallrows($hierarchy, [PR_DISPLAY_NAME, PR_PARENT_ENTRYID, PR_ENTRYID, PR_SOURCE_KEY, PR_PARENT_SOURCE_KEY, PR_CONTAINER_CLASS, PR_ATTR_HIDDEN, PR_EXTENDED_FOLDER_FLAGS, PR_FOLDER_TYPE]); |
|
| 342 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetHierarchy(): fetched %d folders from MAPI", count($rows))); |
|
| 343 | - |
|
| 344 | - foreach ($rows as $row) { |
|
| 345 | - // do not display hidden and search folders |
|
| 346 | - if ((isset($row[PR_ATTR_HIDDEN]) && $row[PR_ATTR_HIDDEN]) || |
|
| 347 | - (isset($row[PR_FOLDER_TYPE]) && $row[PR_FOLDER_TYPE] == FOLDER_SEARCH) || |
|
| 348 | - // for SYSTEM user $row[PR_PARENT_SOURCE_KEY] == $rootfolderprops[PR_SOURCE_KEY] is true, but we need those folders |
|
| 349 | - (isset($row[PR_PARENT_SOURCE_KEY]) && $row[PR_PARENT_SOURCE_KEY] == $rootfolderprops[PR_SOURCE_KEY] && strtoupper($this->storeName) != "SYSTEM")) { |
|
| 350 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetHierarchy(): ignoring folder '%s' as it's a hidden/search/root folder", (isset($row[PR_DISPLAY_NAME]) ? $row[PR_DISPLAY_NAME] : "unknown"))); |
|
| 351 | - |
|
| 352 | - continue; |
|
| 353 | - } |
|
| 354 | - $folder = $mapiprovider->GetFolder($row); |
|
| 355 | - if ($folder) { |
|
| 356 | - $folders[] = $folder; |
|
| 357 | - } |
|
| 358 | - else { |
|
| 359 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetHierarchy(): ignoring folder '%s' as MAPIProvider->GetFolder() did not return a SyncFolder object", (isset($row[PR_DISPLAY_NAME]) ? $row[PR_DISPLAY_NAME] : "unknown"))); |
|
| 360 | - } |
|
| 361 | - } |
|
| 362 | - |
|
| 363 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetHierarchy(): processed %d folders, starting parent remap", count($folders))); |
|
| 364 | - // reloop the folders to make sure all parentids are mapped correctly |
|
| 365 | - $dm = GSync::GetDeviceManager(); |
|
| 366 | - foreach ($folders as $folder) { |
|
| 367 | - if ($folder->parentid !== "0") { |
|
| 368 | - // SYSTEM user's parentid points to $rootfolderprops[PR_SOURCE_KEY], but they need to be on the top level |
|
| 369 | - $folder->parentid = (strtoupper($this->storeName) == "SYSTEM" && $folder->parentid == bin2hex($rootfolderprops[PR_SOURCE_KEY])) ? '0' : $dm->GetFolderIdForBackendId($folder->parentid); |
|
| 370 | - } |
|
| 371 | - } |
|
| 372 | - |
|
| 373 | - return $folders; |
|
| 374 | - } |
|
| 375 | - |
|
| 376 | - /** |
|
| 377 | - * Returns the importer to process changes from the mobile |
|
| 378 | - * If no $folderid is given, hierarchy importer is expected. |
|
| 379 | - * |
|
| 380 | - * @param string $folderid (opt) |
|
| 381 | - * |
|
| 382 | - * @return object(ImportChanges) |
|
| 383 | - */ |
|
| 384 | - public function GetImporter($folderid = false) { |
|
| 385 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetImporter() folderid: '%s'", Utils::PrintAsString($folderid))); |
|
| 386 | - if ($folderid !== false) { |
|
| 387 | - // check if the user of the current store has permissions to import to this folderid |
|
| 388 | - if ($this->storeName != $this->mainUser && !$this->hasSecretaryACLs($this->store, $folderid)) { |
|
| 389 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetImporter(): missing permissions on folderid: '%s'.", Utils::PrintAsString($folderid))); |
|
| 390 | - |
|
| 391 | - return false; |
|
| 392 | - } |
|
| 393 | - |
|
| 394 | - return new ImportChangesICS($this->session, $this->store, hex2bin($folderid)); |
|
| 395 | - } |
|
| 396 | - |
|
| 397 | - return new ImportChangesICS($this->session, $this->store); |
|
| 398 | - } |
|
| 399 | - |
|
| 400 | - /** |
|
| 401 | - * Returns the exporter to send changes to the mobile |
|
| 402 | - * If no $folderid is given, hierarchy exporter is expected. |
|
| 403 | - * |
|
| 404 | - * @param string $folderid (opt) |
|
| 405 | - * |
|
| 406 | - * @throws StatusException |
|
| 407 | - * |
|
| 408 | - * @return object(ExportChanges) |
|
| 409 | - */ |
|
| 410 | - public function GetExporter($folderid = false) { |
|
| 411 | - if ($folderid !== false) { |
|
| 412 | - // check if the user of the current store has permissions to export from this folderid |
|
| 413 | - if ($this->storeName != $this->mainUser && !$this->hasSecretaryACLs($this->store, $folderid)) { |
|
| 414 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetExporter(): missing permissions on folderid: '%s'.", Utils::PrintAsString($folderid))); |
|
| 415 | - |
|
| 416 | - return false; |
|
| 417 | - } |
|
| 418 | - |
|
| 419 | - return new ExportChangesICS($this->session, $this->store, hex2bin($folderid)); |
|
| 420 | - } |
|
| 421 | - |
|
| 422 | - return new ExportChangesICS($this->session, $this->store); |
|
| 423 | - } |
|
| 424 | - |
|
| 425 | - /** |
|
| 426 | - * Sends an e-mail |
|
| 427 | - * This messages needs to be saved into the 'sent items' folder. |
|
| 428 | - * |
|
| 429 | - * @param SyncSendMail $sm SyncSendMail object |
|
| 430 | - * |
|
| 431 | - * @throws StatusException |
|
| 432 | - * |
|
| 433 | - * @return bool |
|
| 434 | - */ |
|
| 435 | - public function SendMail($sm) { |
|
| 436 | - // Check if imtomapi function is available and use it to send the mime message. |
|
| 437 | - // It is available since ZCP 7.0.6 |
|
| 438 | - // @see http://jira.zarafa.com/browse/ZCP-9508 |
|
| 439 | - if (!(function_exists('mapi_feature') && mapi_feature('INETMAPI_IMTOMAPI'))) { |
|
| 440 | - throw new StatusException("Grommunio->SendMail(): ZCP/KC version is too old, INETMAPI_IMTOMAPI is not available. Install at least ZCP version 7.0.6 or later.", SYNC_COMMONSTATUS_MAILSUBMISSIONFAILED, null, LOGLEVEL_FATAL); |
|
| 441 | - |
|
| 442 | - return false; |
|
| 443 | - } |
|
| 444 | - $mimeLength = strlen($sm->mime); |
|
| 445 | - SLog::Write(LOGLEVEL_DEBUG, sprintf( |
|
| 446 | - "Grommunio->SendMail(): RFC822: %d bytes forward-id: '%s' reply-id: '%s' parent-id: '%s' SaveInSent: '%s' ReplaceMIME: '%s'", |
|
| 447 | - $mimeLength, |
|
| 448 | - Utils::PrintAsString($sm->forwardflag), |
|
| 449 | - Utils::PrintAsString($sm->replyflag), |
|
| 450 | - Utils::PrintAsString((isset($sm->source->folderid) ? $sm->source->folderid : false)), |
|
| 451 | - Utils::PrintAsString(($sm->saveinsent)), |
|
| 452 | - Utils::PrintAsString(isset($sm->replacemime)) |
|
| 453 | - )); |
|
| 454 | - if ($mimeLength == 0) { |
|
| 455 | - throw new StatusException("Grommunio->SendMail(): empty mail data", SYNC_COMMONSTATUS_MAILSUBMISSIONFAILED); |
|
| 456 | - } |
|
| 457 | - |
|
| 458 | - $sendMailProps = MAPIMapping::GetSendMailProperties(); |
|
| 459 | - $sendMailProps = getPropIdsFromStrings($this->defaultstore, $sendMailProps); |
|
| 460 | - |
|
| 461 | - // Open the outbox and create the message there |
|
| 462 | - $storeprops = mapi_getprops($this->defaultstore, [$sendMailProps["outboxentryid"], $sendMailProps["ipmsentmailentryid"]]); |
|
| 463 | - if (isset($storeprops[$sendMailProps["outboxentryid"]])) { |
|
| 464 | - $outbox = mapi_msgstore_openentry($this->defaultstore, $storeprops[$sendMailProps["outboxentryid"]]); |
|
| 465 | - } |
|
| 466 | - |
|
| 467 | - if (!$outbox) { |
|
| 468 | - throw new StatusException(sprintf("Grommunio->SendMail(): No Outbox found or unable to create message: 0x%X", mapi_last_hresult()), SYNC_COMMONSTATUS_SERVERERROR); |
|
| 469 | - } |
|
| 470 | - |
|
| 471 | - $mapimessage = mapi_folder_createmessage($outbox); |
|
| 472 | - |
|
| 473 | - // message properties to be set |
|
| 474 | - $mapiprops = []; |
|
| 475 | - // only save the outgoing in sent items folder if the mobile requests it |
|
| 476 | - $mapiprops[$sendMailProps["sentmailentryid"]] = $storeprops[$sendMailProps["ipmsentmailentryid"]]; |
|
| 477 | - |
|
| 478 | - SLog::Write(LOGLEVEL_DEBUG, "Grommunio->SendMail(): Use the mapi_inetmapi_imtomapi function"); |
|
| 479 | - $ab = mapi_openaddressbook($this->session); |
|
| 480 | - mapi_inetmapi_imtomapi($this->session, $this->defaultstore, $ab, $mapimessage, $sm->mime, []); |
|
| 481 | - |
|
| 482 | - // Set the appSeqNr so that tracking tab can be updated for meeting request updates |
|
| 483 | - // @see http://jira.zarafa.com/browse/ZP-68 |
|
| 484 | - $meetingRequestProps = MAPIMapping::GetMeetingRequestProperties(); |
|
| 485 | - $meetingRequestProps = getPropIdsFromStrings($this->defaultstore, $meetingRequestProps); |
|
| 486 | - $props = mapi_getprops($mapimessage, [PR_MESSAGE_CLASS, $meetingRequestProps["goidtag"], $sendMailProps["internetcpid"], $sendMailProps["body"], $sendMailProps["html"], $sendMailProps["rtf"], $sendMailProps["rtfinsync"]]); |
|
| 487 | - |
|
| 488 | - // Convert sent message's body to UTF-8 if it was a HTML message. |
|
| 489 | - // @see http://jira.zarafa.com/browse/ZP-505 and http://jira.zarafa.com/browse/ZP-555 |
|
| 490 | - if (isset($props[$sendMailProps["internetcpid"]]) && $props[$sendMailProps["internetcpid"]] != INTERNET_CPID_UTF8 && MAPIUtils::GetNativeBodyType($props) == SYNC_BODYPREFERENCE_HTML) { |
|
| 491 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->SendMail(): Sent email cpid is not unicode (%d). Set it to unicode and convert email html body.", $props[$sendMailProps["internetcpid"]])); |
|
| 492 | - $mapiprops[$sendMailProps["internetcpid"]] = INTERNET_CPID_UTF8; |
|
| 493 | - |
|
| 494 | - $bodyHtml = MAPIUtils::readPropStream($mapimessage, PR_HTML); |
|
| 495 | - $bodyHtml = Utils::ConvertCodepageStringToUtf8($props[$sendMailProps["internetcpid"]], $bodyHtml); |
|
| 496 | - $mapiprops[$sendMailProps["html"]] = $bodyHtml; |
|
| 497 | - |
|
| 498 | - mapi_setprops($mapimessage, $mapiprops); |
|
| 499 | - } |
|
| 500 | - if (stripos($props[PR_MESSAGE_CLASS], "IPM.Schedule.Meeting.Resp.") === 0) { |
|
| 501 | - // search for calendar items using goid |
|
| 502 | - $mr = new Meetingrequest($this->defaultstore, $mapimessage); |
|
| 503 | - $appointments = $mr->findCalendarItems($props[$meetingRequestProps["goidtag"]]); |
|
| 504 | - if (is_array($appointments) && !empty($appointments)) { |
|
| 505 | - $app = mapi_msgstore_openentry($this->defaultstore, $appointments[0]); |
|
| 506 | - $appprops = mapi_getprops($app, [$meetingRequestProps["appSeqNr"]]); |
|
| 507 | - if (isset($appprops[$meetingRequestProps["appSeqNr"]]) && $appprops[$meetingRequestProps["appSeqNr"]]) { |
|
| 508 | - $mapiprops[$meetingRequestProps["appSeqNr"]] = $appprops[$meetingRequestProps["appSeqNr"]]; |
|
| 509 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->SendMail(): Set sequence number to:%d", $appprops[$meetingRequestProps["appSeqNr"]])); |
|
| 510 | - } |
|
| 511 | - } |
|
| 512 | - } |
|
| 513 | - |
|
| 514 | - // Delete the PR_SENT_REPRESENTING_* properties because some android devices |
|
| 515 | - // do not send neither From nor Sender header causing empty PR_SENT_REPRESENTING_NAME and |
|
| 516 | - // PR_SENT_REPRESENTING_EMAIL_ADDRESS properties and "broken" PR_SENT_REPRESENTING_ENTRYID |
|
| 517 | - // which results in spooler not being able to send the message. |
|
| 518 | - // @see http://jira.zarafa.com/browse/ZP-85 |
|
| 519 | - mapi_deleteprops( |
|
| 520 | - $mapimessage, |
|
| 521 | - [ |
|
| 522 | - $sendMailProps["sentrepresentingname"], |
|
| 523 | - $sendMailProps["sentrepresentingemail"], |
|
| 524 | - $sendMailProps["representingentryid"], |
|
| 525 | - $sendMailProps["sentrepresentingaddt"], |
|
| 526 | - $sendMailProps["sentrepresentinsrchk"], |
|
| 527 | - ] |
|
| 528 | - ); |
|
| 529 | - |
|
| 530 | - if (isset($sm->source->itemid) && $sm->source->itemid) { |
|
| 531 | - // answering an email in a public/shared folder |
|
| 532 | - // TODO as the store is setup, we should actually user $this->store instead of $this->defaultstore - nevertheless we need to make sure this store is able to send mail (has an outbox) |
|
| 533 | - if (!$this->Setup(GSync::GetAdditionalSyncFolderStore($sm->source->folderid))) { |
|
| 534 | - throw new StatusException(sprintf("Grommunio->SendMail() could not Setup() the backend for folder id '%s'", $sm->source->folderid), SYNC_COMMONSTATUS_SERVERERROR); |
|
| 535 | - } |
|
| 536 | - |
|
| 537 | - $entryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($sm->source->folderid), hex2bin($sm->source->itemid)); |
|
| 538 | - if ($entryid) { |
|
| 539 | - $fwmessage = mapi_msgstore_openentry($this->store, $entryid); |
|
| 540 | - } |
|
| 541 | - |
|
| 542 | - if (isset($fwmessage) && $fwmessage) { |
|
| 543 | - // update icon and last_verb when forwarding or replying message |
|
| 544 | - // reply-all (verb 103) is not supported, as we cannot really detect this case |
|
| 545 | - if ($sm->forwardflag) { |
|
| 546 | - $updateProps = [ |
|
| 547 | - PR_ICON_INDEX => 262, |
|
| 548 | - PR_LAST_VERB_EXECUTED => 104, |
|
| 549 | - ]; |
|
| 550 | - } |
|
| 551 | - elseif ($sm->replyflag) { |
|
| 552 | - $updateProps = [ |
|
| 553 | - PR_ICON_INDEX => 261, |
|
| 554 | - PR_LAST_VERB_EXECUTED => 102, |
|
| 555 | - ]; |
|
| 556 | - } |
|
| 557 | - if (isset($updateProps)) { |
|
| 558 | - $updateProps[PR_LAST_VERB_EXECUTION_TIME] = time(); |
|
| 559 | - mapi_setprops($fwmessage, $updateProps); |
|
| 560 | - mapi_savechanges($fwmessage); |
|
| 561 | - } |
|
| 562 | - |
|
| 563 | - // only attach the original message if the mobile does not send it itself |
|
| 564 | - if (!isset($sm->replacemime)) { |
|
| 565 | - // get message's body in order to append forward or reply text |
|
| 566 | - if (!isset($body)) { |
|
| 567 | - $body = MAPIUtils::readPropStream($mapimessage, PR_BODY); |
|
| 568 | - } |
|
| 569 | - if (!isset($bodyHtml)) { |
|
| 570 | - $bodyHtml = MAPIUtils::readPropStream($mapimessage, PR_HTML); |
|
| 571 | - } |
|
| 572 | - $cpid = mapi_getprops($fwmessage, [$sendMailProps["internetcpid"]]); |
|
| 573 | - if ($sm->forwardflag) { |
|
| 574 | - // attach the original attachments to the outgoing message |
|
| 575 | - $this->copyAttachments($mapimessage, $fwmessage); |
|
| 576 | - } |
|
| 577 | - |
|
| 578 | - // regarding the conversion @see ZP-470 |
|
| 579 | - if (strlen($body) > 0) { |
|
| 580 | - $fwbody = MAPIUtils::readPropStream($fwmessage, PR_BODY); |
|
| 581 | - // if only the old message's cpid is set, convert from old charset to utf-8 |
|
| 582 | - if (isset($cpid[$sendMailProps["internetcpid"]]) && $cpid[$sendMailProps["internetcpid"]] != INTERNET_CPID_UTF8) { |
|
| 583 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->SendMail(): convert plain forwarded message charset (only fw set) from '%s' to '65001'", $cpid[$sendMailProps["internetcpid"]])); |
|
| 584 | - $fwbody = Utils::ConvertCodepageStringToUtf8($cpid[$sendMailProps["internetcpid"]], $fwbody); |
|
| 585 | - } |
|
| 586 | - // otherwise to the general conversion |
|
| 587 | - else { |
|
| 588 | - SLog::Write(LOGLEVEL_DEBUG, "Grommunio->SendMail(): no charset conversion done for plain forwarded message"); |
|
| 589 | - $fwbody = w2u($fwbody); |
|
| 590 | - } |
|
| 591 | - |
|
| 592 | - $mapiprops[$sendMailProps["body"]] = $body . "\r\n\r\n" . $fwbody; |
|
| 593 | - } |
|
| 594 | - |
|
| 595 | - if (strlen($bodyHtml) > 0) { |
|
| 596 | - $fwbodyHtml = MAPIUtils::readPropStream($fwmessage, PR_HTML); |
|
| 597 | - // if only new message's cpid is set, convert to UTF-8 |
|
| 598 | - if (isset($cpid[$sendMailProps["internetcpid"]]) && $cpid[$sendMailProps["internetcpid"]] != INTERNET_CPID_UTF8) { |
|
| 599 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->SendMail(): convert html forwarded message charset (only fw set) from '%s' to '65001'", $cpid[$sendMailProps["internetcpid"]])); |
|
| 600 | - $fwbodyHtml = Utils::ConvertCodepageStringToUtf8($cpid[$sendMailProps["internetcpid"]], $fwbodyHtml); |
|
| 601 | - } |
|
| 602 | - // otherwise to the general conversion |
|
| 603 | - else { |
|
| 604 | - SLog::Write(LOGLEVEL_DEBUG, "Grommunio->SendMail(): no charset conversion done for html forwarded message"); |
|
| 605 | - $fwbodyHtml = w2u($fwbodyHtml); |
|
| 606 | - } |
|
| 607 | - |
|
| 608 | - $mapiprops[$sendMailProps["html"]] = $bodyHtml . "<br><br>" . $fwbodyHtml; |
|
| 609 | - } |
|
| 610 | - } |
|
| 611 | - } |
|
| 612 | - else { |
|
| 613 | - // no fwmessage could be opened and we need it because we do not replace mime |
|
| 614 | - if (!isset($sm->replacemime) || $sm->replacemime == false) { |
|
| 615 | - throw new StatusException(sprintf("Grommunio->SendMail(): Could not open message id '%s' in folder id '%s' to be replied/forwarded: 0x%X", $sm->source->itemid, $sm->source->folderid, mapi_last_hresult()), SYNC_COMMONSTATUS_ITEMNOTFOUND); |
|
| 616 | - } |
|
| 617 | - } |
|
| 618 | - } |
|
| 619 | - |
|
| 620 | - mapi_setprops($mapimessage, $mapiprops); |
|
| 621 | - mapi_savechanges($mapimessage); |
|
| 622 | - mapi_message_submitmessage($mapimessage); |
|
| 623 | - $hr = mapi_last_hresult(); |
|
| 624 | - |
|
| 625 | - if ($hr) { |
|
| 626 | - switch ($hr) { |
|
| 627 | - case MAPI_E_STORE_FULL: |
|
| 628 | - $code = SYNC_COMMONSTATUS_MAILBOXQUOTAEXCEEDED; |
|
| 629 | - break; |
|
| 630 | - |
|
| 631 | - default: |
|
| 632 | - $code = SYNC_COMMONSTATUS_MAILSUBMISSIONFAILED; |
|
| 633 | - break; |
|
| 634 | - } |
|
| 635 | - |
|
| 636 | - throw new StatusException(sprintf("Grommunio->SendMail(): Error saving/submitting the message to the Outbox: 0x%X", $hr), $code); |
|
| 637 | - } |
|
| 638 | - |
|
| 639 | - SLog::Write(LOGLEVEL_DEBUG, "Grommunio->SendMail(): email submitted"); |
|
| 640 | - |
|
| 641 | - return true; |
|
| 642 | - } |
|
| 643 | - |
|
| 644 | - /** |
|
| 645 | - * Returns all available data of a single message. |
|
| 646 | - * |
|
| 647 | - * @param string $folderid |
|
| 648 | - * @param string $id |
|
| 649 | - * @param ContentParameters $contentparameters flag |
|
| 650 | - * |
|
| 651 | - * @throws StatusException |
|
| 652 | - * |
|
| 653 | - * @return object(SyncObject) |
|
| 654 | - */ |
|
| 655 | - public function Fetch($folderid, $id, $contentparameters) { |
|
| 656 | - // SEARCH fetches with folderid == false and PR_ENTRYID as ID |
|
| 657 | - if (!$folderid) { |
|
| 658 | - $entryid = hex2bin($id); |
|
| 659 | - $sk = $id; |
|
| 660 | - } |
|
| 661 | - else { |
|
| 662 | - // id might be in the new longid format, so we have to split it here |
|
| 663 | - list($fsk, $sk) = Utils::SplitMessageId($id); |
|
| 664 | - // get the entry id of the message |
|
| 665 | - $entryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($folderid), hex2bin($sk)); |
|
| 666 | - } |
|
| 667 | - if (!$entryid) { |
|
| 668 | - throw new StatusException(sprintf("Grommunio->Fetch('%s','%s'): Error getting entryid: 0x%X", $folderid, $sk, mapi_last_hresult()), SYNC_STATUS_OBJECTNOTFOUND); |
|
| 669 | - } |
|
| 670 | - |
|
| 671 | - // open the message |
|
| 672 | - $message = mapi_msgstore_openentry($this->store, $entryid); |
|
| 673 | - if (!$message) { |
|
| 674 | - throw new StatusException(sprintf("Grommunio->Fetch('%s','%s'): Error, unable to open message: 0x%X", $folderid, $sk, mapi_last_hresult()), SYNC_STATUS_OBJECTNOTFOUND); |
|
| 675 | - } |
|
| 676 | - |
|
| 677 | - // convert the mapi message into a SyncObject and return it |
|
| 678 | - $mapiprovider = new MAPIProvider($this->session, $this->store); |
|
| 679 | - |
|
| 680 | - // override truncation |
|
| 681 | - $contentparameters->SetTruncation(SYNC_TRUNCATION_ALL); |
|
| 682 | - // TODO check for body preferences |
|
| 683 | - return $mapiprovider->GetMessage($message, $contentparameters); |
|
| 684 | - } |
|
| 685 | - |
|
| 686 | - /** |
|
| 687 | - * Returns the waste basket. |
|
| 688 | - * |
|
| 689 | - * @return string |
|
| 690 | - */ |
|
| 691 | - public function GetWasteBasket() { |
|
| 692 | - if ($this->wastebasket) { |
|
| 693 | - return $this->wastebasket; |
|
| 694 | - } |
|
| 695 | - |
|
| 696 | - $storeprops = mapi_getprops($this->defaultstore, [PR_IPM_WASTEBASKET_ENTRYID]); |
|
| 697 | - if (isset($storeprops[PR_IPM_WASTEBASKET_ENTRYID])) { |
|
| 698 | - $wastebasket = mapi_msgstore_openentry($this->defaultstore, $storeprops[PR_IPM_WASTEBASKET_ENTRYID]); |
|
| 699 | - $wastebasketprops = mapi_getprops($wastebasket, [PR_SOURCE_KEY]); |
|
| 700 | - if (isset($wastebasketprops[PR_SOURCE_KEY])) { |
|
| 701 | - $this->wastebasket = bin2hex($wastebasketprops[PR_SOURCE_KEY]); |
|
| 702 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetWasteBasket(): Got waste basket with id '%s'", $this->wastebasket)); |
|
| 703 | - |
|
| 704 | - return $this->wastebasket; |
|
| 705 | - } |
|
| 706 | - } |
|
| 707 | - |
|
| 708 | - return false; |
|
| 709 | - } |
|
| 710 | - |
|
| 711 | - /** |
|
| 712 | - * Returns the content of the named attachment as stream. |
|
| 713 | - * |
|
| 714 | - * @param string $attname |
|
| 715 | - * |
|
| 716 | - * @throws StatusException |
|
| 717 | - * |
|
| 718 | - * @return SyncItemOperationsAttachment |
|
| 719 | - */ |
|
| 720 | - public function GetAttachmentData($attname) { |
|
| 721 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetAttachmentData('%s')", $attname)); |
|
| 722 | - |
|
| 723 | - if (!strpos($attname, ":")) { |
|
| 724 | - throw new StatusException(sprintf("Grommunio->GetAttachmentData('%s'): Error, attachment requested for non-existing item", $attname), SYNC_ITEMOPERATIONSSTATUS_INVALIDATT); |
|
| 725 | - } |
|
| 726 | - |
|
| 727 | - list($id, $attachnum, $parentEntryid) = explode(":", $attname); |
|
| 728 | - if (isset($parentEntryid)) { |
|
| 729 | - $this->Setup(GSync::GetAdditionalSyncFolderStore($parentEntryid)); |
|
| 730 | - } |
|
| 731 | - |
|
| 732 | - $entryid = hex2bin($id); |
|
| 733 | - $message = mapi_msgstore_openentry($this->store, $entryid); |
|
| 734 | - if (!$message) { |
|
| 735 | - throw new StatusException(sprintf("Grommunio->GetAttachmentData('%s'): Error, unable to open item for attachment data for id '%s' with: 0x%X", $attname, $id, mapi_last_hresult()), SYNC_ITEMOPERATIONSSTATUS_INVALIDATT); |
|
| 736 | - } |
|
| 737 | - |
|
| 738 | - MAPIUtils::ParseSmime($this->session, $this->defaultstore, $this->getAddressbook(), $message); |
|
| 739 | - $attach = mapi_message_openattach($message, $attachnum); |
|
| 740 | - if (!$attach) { |
|
| 741 | - throw new StatusException(sprintf("Grommunio->GetAttachmentData('%s'): Error, unable to open attachment number '%s' with: 0x%X", $attname, $attachnum, mapi_last_hresult()), SYNC_ITEMOPERATIONSSTATUS_INVALIDATT); |
|
| 742 | - } |
|
| 743 | - |
|
| 744 | - // get necessary attachment props |
|
| 745 | - $attprops = mapi_getprops($attach, [PR_ATTACH_MIME_TAG, PR_ATTACH_MIME_TAG_W, PR_ATTACH_METHOD]); |
|
| 746 | - $attachment = new SyncItemOperationsAttachment(); |
|
| 747 | - // check if it's an embedded message and open it in such a case |
|
| 748 | - if (isset($attprops[PR_ATTACH_METHOD]) && $attprops[PR_ATTACH_METHOD] == ATTACH_EMBEDDED_MSG) { |
|
| 749 | - $embMessage = mapi_attach_openobj($attach); |
|
| 750 | - $addrbook = $this->getAddressbook(); |
|
| 751 | - $stream = mapi_inetmapi_imtoinet($this->session, $addrbook, $embMessage, ['use_tnef' => -1]); |
|
| 752 | - // set the default contenttype for this kind of messages |
|
| 753 | - $attachment->contenttype = "message/rfc822"; |
|
| 754 | - } |
|
| 755 | - else { |
|
| 756 | - $stream = mapi_openproperty($attach, PR_ATTACH_DATA_BIN, IID_IStream, 0, 0); |
|
| 757 | - } |
|
| 758 | - |
|
| 759 | - if (!$stream) { |
|
| 760 | - throw new StatusException(sprintf("Grommunio->GetAttachmentData('%s'): Error, unable to open attachment data stream: 0x%X", $attname, mapi_last_hresult()), SYNC_ITEMOPERATIONSSTATUS_INVALIDATT); |
|
| 761 | - } |
|
| 762 | - |
|
| 763 | - // put the mapi stream into a wrapper to get a standard stream |
|
| 764 | - $attachment->data = MAPIStreamWrapper::Open($stream); |
|
| 765 | - if (isset($attprops[PR_ATTACH_MIME_TAG])) { |
|
| 766 | - $attachment->contenttype = $attprops[PR_ATTACH_MIME_TAG]; |
|
| 767 | - } |
|
| 768 | - elseif (isset($attprops[PR_ATTACH_MIME_TAG_W])) { |
|
| 769 | - $attachment->contenttype = $attprops[PR_ATTACH_MIME_TAG_W]; |
|
| 770 | - } |
|
| 771 | - // TODO default contenttype |
|
| 772 | - return $attachment; |
|
| 773 | - } |
|
| 774 | - |
|
| 775 | - /** |
|
| 776 | - * Deletes all contents of the specified folder. |
|
| 777 | - * This is generally used to empty the trash (wastebasked), but could also be used on any |
|
| 778 | - * other folder. |
|
| 779 | - * |
|
| 780 | - * @param string $folderid |
|
| 781 | - * @param bool $includeSubfolders (opt) also delete sub folders, default true |
|
| 782 | - * |
|
| 783 | - * @throws StatusException |
|
| 784 | - * |
|
| 785 | - * @return bool |
|
| 786 | - */ |
|
| 787 | - public function EmptyFolder($folderid, $includeSubfolders = true) { |
|
| 788 | - $folderentryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($folderid)); |
|
| 789 | - if (!$folderentryid) { |
|
| 790 | - throw new StatusException(sprintf("Grommunio->EmptyFolder('%s','%s'): Error, unable to open folder (no entry id)", $folderid, Utils::PrintAsString($includeSubfolders)), SYNC_ITEMOPERATIONSSTATUS_SERVERERROR); |
|
| 791 | - } |
|
| 792 | - $folder = mapi_msgstore_openentry($this->store, $folderentryid); |
|
| 793 | - |
|
| 794 | - if (!$folder) { |
|
| 795 | - throw new StatusException(sprintf("Grommunio->EmptyFolder('%s','%s'): Error, unable to open parent folder (open entry)", $folderid, Utils::PrintAsString($includeSubfolders)), SYNC_ITEMOPERATIONSSTATUS_SERVERERROR); |
|
| 796 | - } |
|
| 797 | - |
|
| 798 | - $flags = 0; |
|
| 799 | - if ($includeSubfolders) { |
|
| 800 | - $flags = DEL_ASSOCIATED; |
|
| 801 | - } |
|
| 802 | - |
|
| 803 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->EmptyFolder('%s','%s'): emptying folder", $folderid, Utils::PrintAsString($includeSubfolders))); |
|
| 804 | - |
|
| 805 | - // empty folder! |
|
| 806 | - mapi_folder_emptyfolder($folder, $flags); |
|
| 807 | - if (mapi_last_hresult()) { |
|
| 808 | - throw new StatusException(sprintf("Grommunio->EmptyFolder('%s','%s'): Error, mapi_folder_emptyfolder() failed: 0x%X", $folderid, Utils::PrintAsString($includeSubfolders), mapi_last_hresult()), SYNC_ITEMOPERATIONSSTATUS_SERVERERROR); |
|
| 809 | - } |
|
| 810 | - |
|
| 811 | - return true; |
|
| 812 | - } |
|
| 813 | - |
|
| 814 | - /** |
|
| 815 | - * Processes a response to a meeting request. |
|
| 816 | - * CalendarID is a reference and has to be set if a new calendar item is created. |
|
| 817 | - * |
|
| 818 | - * @param string $requestid id of the object containing the request |
|
| 819 | - * @param string $folderid id of the parent folder of $requestid |
|
| 820 | - * @param string $response |
|
| 821 | - * |
|
| 822 | - * @throws StatusException |
|
| 823 | - * |
|
| 824 | - * @return string id of the created/updated calendar obj |
|
| 825 | - */ |
|
| 826 | - public function MeetingResponse($requestid, $folderid, $response) { |
|
| 827 | - // Use standard meeting response code to process meeting request |
|
| 828 | - list($fid, $requestid) = Utils::SplitMessageId($requestid); |
|
| 829 | - $reqentryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($folderid), hex2bin($requestid)); |
|
| 830 | - if (!$reqentryid) { |
|
| 831 | - throw new StatusException(sprintf("Grommunio->MeetingResponse('%s', '%s', '%s'): Error, unable to entryid of the message 0x%X", $requestid, $folderid, $response, mapi_last_hresult()), SYNC_MEETRESPSTATUS_INVALIDMEETREQ); |
|
| 832 | - } |
|
| 833 | - |
|
| 834 | - $mapimessage = mapi_msgstore_openentry($this->store, $reqentryid); |
|
| 835 | - if (!$mapimessage) { |
|
| 836 | - throw new StatusException(sprintf("Grommunio->MeetingResponse('%s','%s', '%s'): Error, unable to open request message for response 0x%X", $requestid, $folderid, $response, mapi_last_hresult()), SYNC_MEETRESPSTATUS_INVALIDMEETREQ); |
|
| 837 | - } |
|
| 838 | - |
|
| 839 | - // ios sends calendar item in MeetingResponse |
|
| 840 | - // @see https://jira.z-hub.io/browse/ZP-1524 |
|
| 841 | - $folderClass = GSync::GetDeviceManager()->GetFolderClassFromCacheByID($fid); |
|
| 842 | - // find the corresponding meeting request |
|
| 843 | - if ($folderClass != 'Email') { |
|
| 844 | - $props = MAPIMapping::GetMeetingRequestProperties(); |
|
| 845 | - $props = getPropIdsFromStrings($this->store, $props); |
|
| 846 | - |
|
| 847 | - $messageprops = mapi_getprops($mapimessage, [$props["goidtag"]]); |
|
| 848 | - $goid = $messageprops[$props["goidtag"]]; |
|
| 849 | - |
|
| 850 | - $mapiprovider = new MAPIProvider($this->session, $this->store); |
|
| 851 | - $inboxprops = $mapiprovider->GetInboxProps(); |
|
| 852 | - $folder = mapi_msgstore_openentry($this->store, $inboxprops[PR_ENTRYID]); |
|
| 853 | - |
|
| 854 | - // Find the item by restricting all items to the correct ID |
|
| 855 | - $restrict = [RES_AND, [ |
|
| 856 | - [RES_PROPERTY, |
|
| 857 | - [ |
|
| 858 | - RELOP => RELOP_EQ, |
|
| 859 | - ULPROPTAG => $props["goidtag"], |
|
| 860 | - VALUE => $goid, |
|
| 861 | - ], |
|
| 862 | - ], |
|
| 863 | - ]]; |
|
| 864 | - |
|
| 865 | - $inboxcontents = mapi_folder_getcontentstable($folder); |
|
| 866 | - |
|
| 867 | - $rows = mapi_table_queryallrows($inboxcontents, [PR_ENTRYID], $restrict); |
|
| 868 | - if (empty($rows)) { |
|
| 869 | - throw new StatusException(sprintf("Grommunio->MeetingResponse('%s','%s', '%s'): Error, meeting request not found in the inbox", $requestid, $folderid, $response), SYNC_MEETRESPSTATUS_INVALIDMEETREQ); |
|
| 870 | - } |
|
| 871 | - SLog::Write(LOGLEVEL_DEBUG, "Grommunio->MeetingResponse found meeting request in the inbox"); |
|
| 872 | - $mapimessage = mapi_msgstore_openentry($this->store, $rows[0][PR_ENTRYID]); |
|
| 873 | - $reqentryid = $rows[0][PR_ENTRYID]; |
|
| 874 | - } |
|
| 875 | - |
|
| 876 | - $meetingrequest = new Meetingrequest($this->store, $mapimessage, $this->session); |
|
| 877 | - |
|
| 878 | - if (!$meetingrequest->isMeetingRequest()) { |
|
| 879 | - throw new StatusException(sprintf("Grommunio->MeetingResponse('%s','%s', '%s'): Error, attempt to respond to non-meeting request", $requestid, $folderid, $response), SYNC_MEETRESPSTATUS_INVALIDMEETREQ); |
|
| 880 | - } |
|
| 881 | - |
|
| 882 | - if ($meetingrequest->isLocalOrganiser()) { |
|
| 883 | - throw new StatusException(sprintf("Grommunio->MeetingResponse('%s','%s', '%s'): Error, attempt to response to meeting request that we organized", $requestid, $folderid, $response), SYNC_MEETRESPSTATUS_INVALIDMEETREQ); |
|
| 884 | - } |
|
| 885 | - |
|
| 886 | - // Process the meeting response. We don't have to send the actual meeting response |
|
| 887 | - // e-mail, because the device will send it itself. This seems not to be the case |
|
| 888 | - // anymore for the ios devices since at least version 12.4. grommunio-sync will send the |
|
| 889 | - // accepted email in such a case. |
|
| 890 | - // @see https://jira.z-hub.io/browse/ZP-1524 |
|
| 891 | - $sendresponse = false; |
|
| 892 | - $deviceType = strtolower(Request::GetDeviceType()); |
|
| 893 | - if ($deviceType == 'iphone' || $deviceType == 'ipad' || $deviceType == 'ipod') { |
|
| 894 | - $matches = []; |
|
| 895 | - if (preg_match("/^Apple-.*?\\/(\\d{4})\\./", Request::GetUserAgent(), $matches) && isset($matches[1]) && $matches[1] >= 1607) { |
|
| 896 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->MeetingResponse: iOS device %s->%s", Request::GetDeviceType(), Request::GetUserAgent())); |
|
| 897 | - $sendresponse = true; |
|
| 898 | - } |
|
| 899 | - } |
|
| 900 | - |
|
| 901 | - switch ($response) { |
|
| 902 | - case 1: // accept |
|
| 903 | - default: |
|
| 904 | - $entryid = $meetingrequest->doAccept(false, $sendresponse, false, false, false, false, true); // last true is the $userAction |
|
| 905 | - break; |
|
| 906 | - |
|
| 907 | - case 2: // tentative |
|
| 908 | - $entryid = $meetingrequest->doAccept(true, $sendresponse, false, false, false, false, true); // last true is the $userAction |
|
| 909 | - break; |
|
| 910 | - |
|
| 911 | - case 3: // decline |
|
| 912 | - $meetingrequest->doDecline(false); |
|
| 913 | - break; |
|
| 914 | - } |
|
| 915 | - |
|
| 916 | - // F/B will be updated on logoff |
|
| 917 | - |
|
| 918 | - // We have to return the ID of the new calendar item, so do that here |
|
| 919 | - $calendarid = ""; |
|
| 920 | - $calFolderId = ""; |
|
| 921 | - if (isset($entryid)) { |
|
| 922 | - $newitem = mapi_msgstore_openentry($this->store, $entryid); |
|
| 923 | - // new item might be in a delegator's store. ActiveSync does not support accepting them. |
|
| 924 | - if (!$newitem) { |
|
| 925 | - throw new StatusException(sprintf("Grommunio->MeetingResponse('%s','%s', '%s'): Object with entryid '%s' was not found in user's store (0x%X). It might be in a delegator's store.", $requestid, $folderid, $response, bin2hex($entryid), mapi_last_hresult()), SYNC_MEETRESPSTATUS_SERVERERROR, null, LOGLEVEL_WARN); |
|
| 926 | - } |
|
| 927 | - |
|
| 928 | - $newprops = mapi_getprops($newitem, [PR_SOURCE_KEY, PR_PARENT_SOURCE_KEY]); |
|
| 929 | - $calendarid = bin2hex($newprops[PR_SOURCE_KEY]); |
|
| 930 | - $calFolderId = bin2hex($newprops[PR_PARENT_SOURCE_KEY]); |
|
| 931 | - } |
|
| 932 | - |
|
| 933 | - // on recurring items, the MeetingRequest class responds with a wrong entryid |
|
| 934 | - if ($requestid == $calendarid) { |
|
| 935 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->MeetingResponse('%s','%s', '%s'): returned calendar id is the same as the requestid - re-searching", $requestid, $folderid, $response)); |
|
| 936 | - |
|
| 937 | - if (empty($props)) { |
|
| 938 | - $props = MAPIMapping::GetMeetingRequestProperties(); |
|
| 939 | - $props = getPropIdsFromStrings($this->store, $props); |
|
| 940 | - |
|
| 941 | - $messageprops = mapi_getprops($mapimessage, [$props["goidtag"]]); |
|
| 942 | - $goid = $messageprops[$props["goidtag"]]; |
|
| 943 | - } |
|
| 944 | - |
|
| 945 | - $items = $meetingrequest->findCalendarItems($goid); |
|
| 946 | - |
|
| 947 | - if (is_array($items)) { |
|
| 948 | - $newitem = mapi_msgstore_openentry($this->store, $items[0]); |
|
| 949 | - $newprops = mapi_getprops($newitem, [PR_SOURCE_KEY, PR_PARENT_SOURCE_KEY]); |
|
| 950 | - $calendarid = bin2hex($newprops[PR_SOURCE_KEY]); |
|
| 951 | - $calFolderId = bin2hex($newprops[PR_PARENT_SOURCE_KEY]); |
|
| 952 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->MeetingResponse('%s','%s', '%s'): found other calendar entryid", $requestid, $folderid, $response)); |
|
| 953 | - } |
|
| 954 | - |
|
| 955 | - if ($requestid == $calendarid) { |
|
| 956 | - throw new StatusException(sprintf("Grommunio->MeetingResponse('%s','%s', '%s'): Error finding the accepted meeting response in the calendar", $requestid, $folderid, $response), SYNC_MEETRESPSTATUS_INVALIDMEETREQ); |
|
| 957 | - } |
|
| 958 | - } |
|
| 959 | - |
|
| 960 | - // delete meeting request from Inbox |
|
| 961 | - if ($folderClass == 'Email') { |
|
| 962 | - $folderentryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($folderid)); |
|
| 963 | - $folder = mapi_msgstore_openentry($this->store, $folderentryid); |
|
| 964 | - } |
|
| 965 | - mapi_folder_deletemessages($folder, [$reqentryid], 0); |
|
| 966 | - |
|
| 967 | - $prefix = ''; |
|
| 968 | - // prepend the short folderid of the target calendar: if available and short ids are used |
|
| 969 | - if ($calFolderId) { |
|
| 970 | - $shortFolderId = GSync::GetDeviceManager()->GetFolderIdForBackendId($calFolderId); |
|
| 971 | - if ($calFolderId != $shortFolderId) { |
|
| 972 | - $prefix = $shortFolderId . ':'; |
|
| 973 | - } |
|
| 974 | - } |
|
| 975 | - |
|
| 976 | - return $prefix . $calendarid; |
|
| 977 | - } |
|
| 978 | - |
|
| 979 | - /** |
|
| 980 | - * Indicates if the backend has a ChangesSink. |
|
| 981 | - * A sink is an active notification mechanism which does not need polling. |
|
| 982 | - * Since Zarafa 7.0.5 such a sink is available. |
|
| 983 | - * The grommunio backend uses this method to initialize the sink with mapi. |
|
| 984 | - * |
|
| 985 | - * @return bool |
|
| 986 | - */ |
|
| 987 | - public function HasChangesSink() { |
|
| 988 | - if (!$this->notifications) { |
|
| 989 | - SLog::Write(LOGLEVEL_DEBUG, "Grommunio->HasChangesSink(): sink is not available"); |
|
| 990 | - |
|
| 991 | - return false; |
|
| 992 | - } |
|
| 993 | - |
|
| 994 | - $this->changesSink = @mapi_sink_create(); |
|
| 995 | - |
|
| 996 | - if (!$this->changesSink || mapi_last_hresult()) { |
|
| 997 | - SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->HasChangesSink(): sink could not be created with 0x%X", mapi_last_hresult())); |
|
| 998 | - |
|
| 999 | - return false; |
|
| 1000 | - } |
|
| 1001 | - |
|
| 1002 | - $this->changesSinkHierarchyHash = $this->getHierarchyHash(); |
|
| 1003 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->HasChangesSink(): created - HierarchyHash: %s", $this->changesSinkHierarchyHash)); |
|
| 1004 | - |
|
| 1005 | - // advise the main store and also to check if the connection supports it |
|
| 1006 | - return $this->adviseStoreToSink($this->defaultstore); |
|
| 1007 | - } |
|
| 1008 | - |
|
| 1009 | - /** |
|
| 1010 | - * The folder should be considered by the sink. |
|
| 1011 | - * Folders which were not initialized should not result in a notification |
|
| 1012 | - * of IBackend->ChangesSink(). |
|
| 1013 | - * |
|
| 1014 | - * @param string $folderid |
|
| 1015 | - * |
|
| 1016 | - * @return bool false if entryid can not be found for that folder |
|
| 1017 | - */ |
|
| 1018 | - public function ChangesSinkInitialize($folderid) { |
|
| 1019 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->ChangesSinkInitialize(): folderid '%s'", $folderid)); |
|
| 1020 | - |
|
| 1021 | - $entryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($folderid)); |
|
| 1022 | - if (!$entryid) { |
|
| 1023 | - return false; |
|
| 1024 | - } |
|
| 1025 | - |
|
| 1026 | - // add entryid to the monitored folders |
|
| 1027 | - $this->changesSinkFolders[$entryid] = $folderid; |
|
| 1028 | - |
|
| 1029 | - // advise the current store to the sink |
|
| 1030 | - return $this->adviseStoreToSink($this->store); |
|
| 1031 | - } |
|
| 1032 | - |
|
| 1033 | - /** |
|
| 1034 | - * The actual ChangesSink. |
|
| 1035 | - * For max. the $timeout value this method should block and if no changes |
|
| 1036 | - * are available return an empty array. |
|
| 1037 | - * If changes are available a list of folderids is expected. |
|
| 1038 | - * |
|
| 1039 | - * @param int $timeout max. amount of seconds to block |
|
| 1040 | - * |
|
| 1041 | - * @return array |
|
| 1042 | - */ |
|
| 1043 | - public function ChangesSink($timeout = 30) { |
|
| 1044 | - // clear the folder stats cache |
|
| 1045 | - unset($this->folderStatCache); |
|
| 1046 | - |
|
| 1047 | - $notifications = []; |
|
| 1048 | - $hierarchyNotifications = []; |
|
| 1049 | - $sinkresult = @mapi_sink_timedwait($this->changesSink, $timeout * 1000); |
|
| 1050 | - |
|
| 1051 | - if (!is_array($sinkresult)) { |
|
| 1052 | - throw new StatusException("Grommunio->ChangesSink(): Sink returned invalid notification, aborting", SyncCollections::OBSOLETE_CONNECTION); |
|
| 1053 | - } |
|
| 1054 | - |
|
| 1055 | - // reverse array so that the changes on folders are before changes on messages and |
|
| 1056 | - // it's possible to filter such notifications |
|
| 1057 | - $sinkresult = array_reverse($sinkresult, true); |
|
| 1058 | - foreach ($sinkresult as $sinknotif) { |
|
| 1059 | - // add a notification on a folder |
|
| 1060 | - if ($sinknotif['objtype'] == MAPI_FOLDER) { |
|
| 1061 | - $hierarchyNotifications[$sinknotif['entryid']] = IBackend::HIERARCHYNOTIFICATION; |
|
| 1062 | - } |
|
| 1063 | - // change on a message, remove hierarchy notification |
|
| 1064 | - if (isset($sinknotif['parentid']) && $sinknotif['objtype'] == MAPI_MESSAGE && isset($notifications[$sinknotif['parentid']])) { |
|
| 1065 | - unset($hierarchyNotifications[$sinknotif['parentid']]); |
|
| 1066 | - } |
|
| 1067 | - |
|
| 1068 | - // TODO check if adding $sinknotif['objtype'] = MAPI_MESSAGE wouldn't break anything |
|
| 1069 | - // check if something in the monitored folders changed |
|
| 1070 | - if (isset($sinknotif['parentid']) && array_key_exists($sinknotif['parentid'], $this->changesSinkFolders)) { |
|
| 1071 | - $notifications[] = $this->changesSinkFolders[$sinknotif['parentid']]; |
|
| 1072 | - } |
|
| 1073 | - // deletes and moves |
|
| 1074 | - if (isset($sinknotif['oldparentid']) && array_key_exists($sinknotif['oldparentid'], $this->changesSinkFolders)) { |
|
| 1075 | - $notifications[] = $this->changesSinkFolders[$sinknotif['oldparentid']]; |
|
| 1076 | - } |
|
| 1077 | - } |
|
| 1078 | - |
|
| 1079 | - // validate hierarchy notifications by comparing the hierarchy hashes (too many false positives otherwise) |
|
| 1080 | - if (!empty($hierarchyNotifications)) { |
|
| 1081 | - $hash = $this->getHierarchyHash(); |
|
| 1082 | - if ($hash !== $this->changesSinkHierarchyHash) { |
|
| 1083 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->ChangesSink() Hierarchy notification, pending validation. New hierarchyHash: %s", $hash)); |
|
| 1084 | - $notifications[] = IBackend::HIERARCHYNOTIFICATION; |
|
| 1085 | - $this->changesSinkHierarchyHash = $hash; |
|
| 1086 | - } |
|
| 1087 | - } |
|
| 1088 | - |
|
| 1089 | - return $notifications; |
|
| 1090 | - } |
|
| 1091 | - |
|
| 1092 | - /** |
|
| 1093 | - * Applies settings to and gets information from the device. |
|
| 1094 | - * |
|
| 1095 | - * @param SyncObject $settings (SyncOOF, SyncUserInformation, SyncRightsManagementTemplates possible) |
|
| 1096 | - * |
|
| 1097 | - * @return SyncObject $settings |
|
| 1098 | - */ |
|
| 1099 | - public function Settings($settings) { |
|
| 1100 | - if ($settings instanceof SyncOOF) { |
|
| 1101 | - $this->settingsOOF($settings); |
|
| 1102 | - } |
|
| 1103 | - |
|
| 1104 | - if ($settings instanceof SyncUserInformation) { |
|
| 1105 | - $this->settingsUserInformation($settings); |
|
| 1106 | - } |
|
| 1107 | - |
|
| 1108 | - if ($settings instanceof SyncRightsManagementTemplates) { |
|
| 1109 | - $this->settingsRightsManagementTemplates($settings); |
|
| 1110 | - } |
|
| 1111 | - |
|
| 1112 | - return $settings; |
|
| 1113 | - } |
|
| 1114 | - |
|
| 1115 | - /** |
|
| 1116 | - * Resolves recipients. |
|
| 1117 | - * |
|
| 1118 | - * @param SyncObject $resolveRecipients |
|
| 1119 | - * |
|
| 1120 | - * @return SyncObject $resolveRecipients |
|
| 1121 | - */ |
|
| 1122 | - public function ResolveRecipients($resolveRecipients) { |
|
| 1123 | - if ($resolveRecipients instanceof SyncResolveRecipients) { |
|
| 1124 | - $resolveRecipients->status = SYNC_RESOLVERECIPSSTATUS_SUCCESS; |
|
| 1125 | - $resolveRecipients->response = []; |
|
| 1126 | - $resolveRecipientsOptions = new SyncResolveRecipientsOptions(); |
|
| 1127 | - $maxAmbiguousRecipients = self::MAXAMBIGUOUSRECIPIENTS; |
|
| 1128 | - |
|
| 1129 | - if (isset($resolveRecipients->options)) { |
|
| 1130 | - $resolveRecipientsOptions = $resolveRecipients->options; |
|
| 1131 | - // only limit ambiguous recipients if the client requests it. |
|
| 1132 | - |
|
| 1133 | - if (isset($resolveRecipientsOptions->maxambiguousrecipients) && |
|
| 1134 | - $resolveRecipientsOptions->maxambiguousrecipients >= 0 && |
|
| 1135 | - $resolveRecipientsOptions->maxambiguousrecipients <= self::MAXAMBIGUOUSRECIPIENTS) { |
|
| 1136 | - $maxAmbiguousRecipients = $resolveRecipientsOptions->maxambiguousrecipients; |
|
| 1137 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->ResolveRecipients(): The client requested %d max ambiguous recipients to resolve.", $maxAmbiguousRecipients)); |
|
| 1138 | - } |
|
| 1139 | - } |
|
| 1140 | - |
|
| 1141 | - foreach ($resolveRecipients->to as $i => $to) { |
|
| 1142 | - $response = new SyncResolveRecipientsResponse(); |
|
| 1143 | - $response->to = $to; |
|
| 1144 | - $response->status = SYNC_RESOLVERECIPSSTATUS_SUCCESS; |
|
| 1145 | - |
|
| 1146 | - // do not expand distlists here |
|
| 1147 | - $recipient = $this->resolveRecipient($to, $maxAmbiguousRecipients, false); |
|
| 1148 | - if (is_array($recipient) && !empty($recipient)) { |
|
| 1149 | - $response->recipientcount = 0; |
|
| 1150 | - foreach ($recipient as $entry) { |
|
| 1151 | - if ($entry instanceof SyncResolveRecipient) { |
|
| 1152 | - // certificates are already set. Unset them if they weren't required. |
|
| 1153 | - if (!isset($resolveRecipientsOptions->certificateretrieval)) { |
|
| 1154 | - unset($entry->certificates); |
|
| 1155 | - } |
|
| 1156 | - if (isset($resolveRecipientsOptions->availability)) { |
|
| 1157 | - if (!isset($resolveRecipientsOptions->starttime)) { |
|
| 1158 | - // TODO error, the request must include a valid StartTime element value |
|
| 1159 | - } |
|
| 1160 | - $entry->availability = $this->getAvailability($to, $entry, $resolveRecipientsOptions); |
|
| 1161 | - } |
|
| 1162 | - if (isset($resolveRecipientsOptions->picture)) { |
|
| 1163 | - // TODO implement picture retrieval of the recipient |
|
| 1164 | - } |
|
| 1165 | - ++$response->recipientcount; |
|
| 1166 | - $response->recipient[] = $entry; |
|
| 1167 | - } |
|
| 1168 | - elseif (is_int($recipient)) { |
|
| 1169 | - $response->status = $recipient; |
|
| 1170 | - } |
|
| 1171 | - } |
|
| 1172 | - } |
|
| 1173 | - |
|
| 1174 | - $resolveRecipients->response[$i] = $response; |
|
| 1175 | - } |
|
| 1176 | - |
|
| 1177 | - return $resolveRecipients; |
|
| 1178 | - } |
|
| 1179 | - |
|
| 1180 | - SLog::Write(LOGLEVEL_WARN, "Grommunio->ResolveRecipients(): Not a valid SyncResolveRecipients object."); |
|
| 1181 | - // return a SyncResolveRecipients object so that sync doesn't fail |
|
| 1182 | - $r = new SyncResolveRecipients(); |
|
| 1183 | - $r->status = SYNC_RESOLVERECIPSSTATUS_PROTOCOLERROR; |
|
| 1184 | - |
|
| 1185 | - return $r; |
|
| 1186 | - } |
|
| 1187 | - |
|
| 1188 | - /*---------------------------------------------------------------------------------------------------------- |
|
| 24 | + private $mainUser; |
|
| 25 | + private $session; |
|
| 26 | + private $defaultstore; |
|
| 27 | + private $store; |
|
| 28 | + private $storeName; |
|
| 29 | + private $storeCache; |
|
| 30 | + private $notifications; |
|
| 31 | + private $changesSink; |
|
| 32 | + private $changesSinkFolders; |
|
| 33 | + private $changesSinkHierarchyHash; |
|
| 34 | + private $changesSinkStores; |
|
| 35 | + private $wastebasket; |
|
| 36 | + private $addressbook; |
|
| 37 | + private $folderStatCache; |
|
| 38 | + private $impersonateUser; |
|
| 39 | + private $stateFolder; |
|
| 40 | + private $userDeviceData; |
|
| 41 | + |
|
| 42 | + // KC config parameter for PR_EC_ENABLED_FEATURES / PR_EC_DISABLED_FEATURES |
|
| 43 | + public const MOBILE_ENABLED = 'mobile'; |
|
| 44 | + |
|
| 45 | + public const MAXAMBIGUOUSRECIPIENTS = 9999; |
|
| 46 | + public const FREEBUSYENUMBLOCKS = 50; |
|
| 47 | + public const MAXFREEBUSYSLOTS = 32767; // max length of 32k for the MergedFreeBusy element is allowed |
|
| 48 | + public const HALFHOURSECONDS = 1800; |
|
| 49 | + |
|
| 50 | + /** |
|
| 51 | + * Constructor of the grommunio Backend. |
|
| 52 | + */ |
|
| 53 | + public function __construct() { |
|
| 54 | + $this->session = false; |
|
| 55 | + $this->store = false; |
|
| 56 | + $this->storeName = false; |
|
| 57 | + $this->storeCache = []; |
|
| 58 | + $this->notifications = false; |
|
| 59 | + $this->changesSink = false; |
|
| 60 | + $this->changesSinkFolders = []; |
|
| 61 | + $this->changesSinkStores = []; |
|
| 62 | + $this->changesSinkHierarchyHash = false; |
|
| 63 | + $this->wastebasket = false; |
|
| 64 | + $this->session = false; |
|
| 65 | + $this->folderStatCache = []; |
|
| 66 | + $this->impersonateUser = false; |
|
| 67 | + $this->stateFolder = null; |
|
| 68 | + |
|
| 69 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio using PHP-MAPI version: %s - PHP version: %s", phpversion("mapi"), phpversion())); |
|
| 70 | + |
|
| 71 | + # Interprocessdata |
|
| 72 | + $this->allocate = 0; |
|
| 73 | + $this->type = "grommunio-sync:userdevices"; |
|
| 74 | + $this->userDeviceData = "grommunio-sync:statefoldercache"; |
|
| 75 | + parent::__construct(); |
|
| 76 | + } |
|
| 77 | + |
|
| 78 | + /** |
|
| 79 | + * Indicates which StateMachine should be used. |
|
| 80 | + * |
|
| 81 | + * @return bool Grommunio uses own state machine |
|
| 82 | + */ |
|
| 83 | + public function GetStateMachine() { |
|
| 84 | + return $this; |
|
| 85 | + } |
|
| 86 | + |
|
| 87 | + /** |
|
| 88 | + * Returns the Grommunio as it implements the ISearchProvider interface |
|
| 89 | + * This could be overwritten by the global configuration. |
|
| 90 | + * |
|
| 91 | + * @return object Implementation of ISearchProvider |
|
| 92 | + */ |
|
| 93 | + public function GetSearchProvider() { |
|
| 94 | + return $this; |
|
| 95 | + } |
|
| 96 | + |
|
| 97 | + /** |
|
| 98 | + * Indicates which AS version is supported by the backend. |
|
| 99 | + * |
|
| 100 | + * @return string AS version constant |
|
| 101 | + */ |
|
| 102 | + public function GetSupportedASVersion() { |
|
| 103 | + return GSync::ASV_141; |
|
| 104 | + } |
|
| 105 | + |
|
| 106 | + /** |
|
| 107 | + * Authenticates the user with the configured grommunio server. |
|
| 108 | + * |
|
| 109 | + * @param string $username |
|
| 110 | + * @param string $domain |
|
| 111 | + * @param string $password |
|
| 112 | + * @param mixed $user |
|
| 113 | + * @param mixed $pass |
|
| 114 | + * |
|
| 115 | + * @throws AuthenticationRequiredException |
|
| 116 | + * |
|
| 117 | + * @return bool |
|
| 118 | + */ |
|
| 119 | + public function Logon($user, $domain, $pass) { |
|
| 120 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->Logon(): Trying to authenticate user '%s'..", $user)); |
|
| 121 | + |
|
| 122 | + $this->mainUser = strtolower($user); |
|
| 123 | + // TODO the impersonated user should be passed directly to IBackend->Logon() - ZP-1351 |
|
| 124 | + if (Request::GetImpersonatedUser()) { |
|
| 125 | + $this->impersonateUser = strtolower(Request::GetImpersonatedUser()); |
|
| 126 | + } |
|
| 127 | + |
|
| 128 | + // check if we are impersonating someone |
|
| 129 | + // $defaultUser will be used for $this->defaultStore |
|
| 130 | + if ($this->impersonateUser !== false) { |
|
| 131 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->Logon(): Impersonation active - authenticating: '%s' - impersonating '%s'", $this->mainUser, $this->impersonateUser)); |
|
| 132 | + $defaultUser = $this->impersonateUser; |
|
| 133 | + } |
|
| 134 | + else { |
|
| 135 | + $defaultUser = $this->mainUser; |
|
| 136 | + } |
|
| 137 | + |
|
| 138 | + $deviceId = Request::GetDeviceID(); |
|
| 139 | + |
|
| 140 | + try { |
|
| 141 | + // check if notifications are available in php-mapi |
|
| 142 | + if (function_exists('mapi_feature') && mapi_feature('LOGONFLAGS')) { |
|
| 143 | + // send grommunio-sync version and user agent to ZCP - ZP-589 |
|
| 144 | + if (Utils::CheckMapiExtVersion('7.2.0')) { |
|
| 145 | + $gsync_version = 'Grommunio-Sync_' . @constant('GROMMUNIOSYNC_VERSION'); |
|
| 146 | + $user_agent = ($deviceId) ? GSync::GetDeviceManager()->GetUserAgent() : "unknown"; |
|
| 147 | + $this->session = @mapi_logon_zarafa($this->mainUser, $pass, MAPI_SERVER, null, null, 0, $gsync_version, $user_agent); |
|
| 148 | + } |
|
| 149 | + else { |
|
| 150 | + $this->session = @mapi_logon_zarafa($this->mainUser, $pass, MAPI_SERVER, null, null, 0); |
|
| 151 | + } |
|
| 152 | + $this->notifications = true; |
|
| 153 | + } |
|
| 154 | + // old fashioned session |
|
| 155 | + else { |
|
| 156 | + $this->session = @mapi_logon_zarafa($this->mainUser, $pass, MAPI_SERVER); |
|
| 157 | + $this->notifications = false; |
|
| 158 | + } |
|
| 159 | + |
|
| 160 | + if (mapi_last_hresult()) { |
|
| 161 | + SLog::Write(LOGLEVEL_ERROR, sprintf("Grommunio->Logon(): login failed with error code: 0x%X", mapi_last_hresult())); |
|
| 162 | + if (mapi_last_hresult() == MAPI_E_NETWORK_ERROR) { |
|
| 163 | + throw new ServiceUnavailableException("Error connecting to KC (login)"); |
|
| 164 | + } |
|
| 165 | + } |
|
| 166 | + } |
|
| 167 | + catch (MAPIException $ex) { |
|
| 168 | + throw new AuthenticationRequiredException($ex->getDisplayMessage()); |
|
| 169 | + } |
|
| 170 | + |
|
| 171 | + if (!$this->session) { |
|
| 172 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->Logon(): logon failed for user '%s'", $this->mainUser)); |
|
| 173 | + $this->defaultstore = false; |
|
| 174 | + |
|
| 175 | + return false; |
|
| 176 | + } |
|
| 177 | + |
|
| 178 | + // Get/open default store |
|
| 179 | + $this->defaultstore = $this->openMessageStore($this->mainUser); |
|
| 180 | + |
|
| 181 | + // To impersonate, we overwrite the defaultstore. We still need to open it before we can do that. |
|
| 182 | + if ($this->impersonateUser) { |
|
| 183 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->Logon(): Impersonating user '%s'", $defaultUser)); |
|
| 184 | + $this->defaultstore = $this->openMessageStore($defaultUser); |
|
| 185 | + } |
|
| 186 | + |
|
| 187 | + if (mapi_last_hresult() == MAPI_E_FAILONEPROVIDER) { |
|
| 188 | + throw new ServiceUnavailableException("Error connecting to KC (open store)"); |
|
| 189 | + } |
|
| 190 | + |
|
| 191 | + if ($this->defaultstore === false) { |
|
| 192 | + throw new AuthenticationRequiredException(sprintf("Grommunio->Logon(): User '%s' has no default store", $defaultUser)); |
|
| 193 | + } |
|
| 194 | + |
|
| 195 | + $this->store = $this->defaultstore; |
|
| 196 | + $this->storeName = $defaultUser; |
|
| 197 | + |
|
| 198 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->Logon(): User '%s' is authenticated%s", $this->mainUser, ($this->impersonateUser ? " impersonating '" . $this->impersonateUser . "'" : ''))); |
|
| 199 | + |
|
| 200 | + $this->isGSyncEnabled(); |
|
| 201 | + |
|
| 202 | + // check if this is a Zarafa 7 store with unicode support |
|
| 203 | + MAPIUtils::IsUnicodeStore($this->store); |
|
| 204 | + |
|
| 205 | + // open the state folder |
|
| 206 | + $this->getStateFolder($deviceId); |
|
| 207 | + |
|
| 208 | + return true; |
|
| 209 | + } |
|
| 210 | + |
|
| 211 | + /** |
|
| 212 | + * Setup the backend to work on a specific store or checks ACLs there. |
|
| 213 | + * If only the $store is submitted, all Import/Export/Fetch/Etc operations should be |
|
| 214 | + * performed on this store (switch operations store). |
|
| 215 | + * If the ACL check is enabled, this operation should just indicate the ACL status on |
|
| 216 | + * the submitted store, without changing the store for operations. |
|
| 217 | + * For the ACL status, the currently logged on user MUST have access rights on |
|
| 218 | + * - the entire store - admin access if no folderid is sent, or |
|
| 219 | + * - on a specific folderid in the store (secretary/full access rights). |
|
| 220 | + * |
|
| 221 | + * The ACLcheck MUST fail if a folder of the authenticated user is checked! |
|
| 222 | + * |
|
| 223 | + * @param string $store target store, could contain a "domain\user" value |
|
| 224 | + * @param bool $checkACLonly if set to true, Setup() should just check ACLs |
|
| 225 | + * @param string $folderid if set, only ACLs on this folderid are relevant |
|
| 226 | + * |
|
| 227 | + * @return bool |
|
| 228 | + */ |
|
| 229 | + public function Setup($store, $checkACLonly = false, $folderid = false) { |
|
| 230 | + list($user, $domain) = Utils::SplitDomainUser($store); |
|
| 231 | + |
|
| 232 | + if (!isset($this->mainUser)) { |
|
| 233 | + return false; |
|
| 234 | + } |
|
| 235 | + |
|
| 236 | + $mainUser = $this->mainUser; |
|
| 237 | + // when impersonating we need to check against the impersonated user |
|
| 238 | + if ($this->impersonateUser) { |
|
| 239 | + $mainUser = $this->impersonateUser; |
|
| 240 | + } |
|
| 241 | + |
|
| 242 | + if ($user === false) { |
|
| 243 | + $user = $mainUser; |
|
| 244 | + } |
|
| 245 | + |
|
| 246 | + // This is a special case. A user will get his entire folder structure by the foldersync by default. |
|
| 247 | + // The ACL check is executed when an additional folder is going to be sent to the mobile. |
|
| 248 | + // Configured that way the user could receive the same folderid twice, with two different names. |
|
| 249 | + if ($mainUser == $user && $checkACLonly && $folderid && !$this->impersonateUser) { |
|
| 250 | + SLog::Write(LOGLEVEL_DEBUG, "Grommunio->Setup(): Checking ACLs for folder of the users defaultstore. Fail is forced to avoid folder duplications on mobile."); |
|
| 251 | + |
|
| 252 | + return false; |
|
| 253 | + } |
|
| 254 | + |
|
| 255 | + // get the users store |
|
| 256 | + $userstore = $this->openMessageStore($user); |
|
| 257 | + |
|
| 258 | + // only proceed if a store was found, else return false |
|
| 259 | + if ($userstore) { |
|
| 260 | + // only check permissions |
|
| 261 | + if ($checkACLonly == true) { |
|
| 262 | + // check for admin rights |
|
| 263 | + if (!$folderid) { |
|
| 264 | + if ($user != $this->mainUser) { |
|
| 265 | + if ($this->impersonateUser) { |
|
| 266 | + $storeProps = mapi_getprops($userstore, [PR_IPM_SUBTREE_ENTRYID]); |
|
| 267 | + $rights = $this->HasSecretaryACLs($userstore, '', $storeProps[PR_IPM_SUBTREE_ENTRYID]); |
|
| 268 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->Setup(): Checking for secretary ACLs on root folder of impersonated store '%s': '%s'", $user, Utils::PrintAsString($rights))); |
|
| 269 | + } |
|
| 270 | + else { |
|
| 271 | + $zarafauserinfo = @nsp_getuserinfo($this->mainUser); |
|
| 272 | + $rights = (isset($zarafauserinfo['admin']) && $zarafauserinfo['admin']) ? true : false; |
|
| 273 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->Setup(): Checking for admin ACLs on store '%s': '%s'", $user, Utils::PrintAsString($rights))); |
|
| 274 | + } |
|
| 275 | + } |
|
| 276 | + // the user has always full access to his own store |
|
| 277 | + else { |
|
| 278 | + $rights = true; |
|
| 279 | + SLog::Write(LOGLEVEL_DEBUG, "Grommunio->Setup(): the user has always full access to his own store"); |
|
| 280 | + } |
|
| 281 | + |
|
| 282 | + return $rights; |
|
| 283 | + } |
|
| 284 | + // check permissions on this folder |
|
| 285 | + |
|
| 286 | + $rights = $this->HasSecretaryACLs($userstore, $folderid); |
|
| 287 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->Setup(): Checking for secretary ACLs on '%s' of store '%s': '%s'", $folderid, $user, Utils::PrintAsString($rights))); |
|
| 288 | + |
|
| 289 | + return $rights; |
|
| 290 | + } |
|
| 291 | + |
|
| 292 | + // switch operations store |
|
| 293 | + // this should also be done if called with user = mainuser or user = false |
|
| 294 | + // which means to switch back to the default store |
|
| 295 | + |
|
| 296 | + // switch active store |
|
| 297 | + $this->store = $userstore; |
|
| 298 | + $this->storeName = $user; |
|
| 299 | + |
|
| 300 | + return true; |
|
| 301 | + } |
|
| 302 | + |
|
| 303 | + return false; |
|
| 304 | + } |
|
| 305 | + |
|
| 306 | + /** |
|
| 307 | + * Logs off |
|
| 308 | + * Free/Busy information is updated for modified calendars |
|
| 309 | + * This is done after the synchronization process is completed. |
|
| 310 | + * |
|
| 311 | + * @return bool |
|
| 312 | + */ |
|
| 313 | + public function Logoff() { |
|
| 314 | + return true; |
|
| 315 | + } |
|
| 316 | + |
|
| 317 | + /** |
|
| 318 | + * Returns an array of SyncFolder types with the entire folder hierarchy |
|
| 319 | + * on the server (the array itself is flat, but refers to parents via the 'parent' property. |
|
| 320 | + * |
|
| 321 | + * provides AS 1.0 compatibility |
|
| 322 | + * |
|
| 323 | + * @return array SYNC_FOLDER |
|
| 324 | + */ |
|
| 325 | + public function GetHierarchy() { |
|
| 326 | + $folders = []; |
|
| 327 | + $mapiprovider = new MAPIProvider($this->session, $this->store); |
|
| 328 | + $storeProps = $mapiprovider->GetStoreProps(); |
|
| 329 | + |
|
| 330 | + // for SYSTEM user open the public folders |
|
| 331 | + if (strtoupper($this->storeName) == "SYSTEM") { |
|
| 332 | + $rootfolder = mapi_msgstore_openentry($this->store, $storeProps[PR_IPM_PUBLIC_FOLDERS_ENTRYID]); |
|
| 333 | + } |
|
| 334 | + else { |
|
| 335 | + $rootfolder = mapi_msgstore_openentry($this->store); |
|
| 336 | + } |
|
| 337 | + |
|
| 338 | + $rootfolderprops = mapi_getprops($rootfolder, [PR_SOURCE_KEY]); |
|
| 339 | + |
|
| 340 | + $hierarchy = mapi_folder_gethierarchytable($rootfolder, CONVENIENT_DEPTH); |
|
| 341 | + $rows = mapi_table_queryallrows($hierarchy, [PR_DISPLAY_NAME, PR_PARENT_ENTRYID, PR_ENTRYID, PR_SOURCE_KEY, PR_PARENT_SOURCE_KEY, PR_CONTAINER_CLASS, PR_ATTR_HIDDEN, PR_EXTENDED_FOLDER_FLAGS, PR_FOLDER_TYPE]); |
|
| 342 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetHierarchy(): fetched %d folders from MAPI", count($rows))); |
|
| 343 | + |
|
| 344 | + foreach ($rows as $row) { |
|
| 345 | + // do not display hidden and search folders |
|
| 346 | + if ((isset($row[PR_ATTR_HIDDEN]) && $row[PR_ATTR_HIDDEN]) || |
|
| 347 | + (isset($row[PR_FOLDER_TYPE]) && $row[PR_FOLDER_TYPE] == FOLDER_SEARCH) || |
|
| 348 | + // for SYSTEM user $row[PR_PARENT_SOURCE_KEY] == $rootfolderprops[PR_SOURCE_KEY] is true, but we need those folders |
|
| 349 | + (isset($row[PR_PARENT_SOURCE_KEY]) && $row[PR_PARENT_SOURCE_KEY] == $rootfolderprops[PR_SOURCE_KEY] && strtoupper($this->storeName) != "SYSTEM")) { |
|
| 350 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetHierarchy(): ignoring folder '%s' as it's a hidden/search/root folder", (isset($row[PR_DISPLAY_NAME]) ? $row[PR_DISPLAY_NAME] : "unknown"))); |
|
| 351 | + |
|
| 352 | + continue; |
|
| 353 | + } |
|
| 354 | + $folder = $mapiprovider->GetFolder($row); |
|
| 355 | + if ($folder) { |
|
| 356 | + $folders[] = $folder; |
|
| 357 | + } |
|
| 358 | + else { |
|
| 359 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetHierarchy(): ignoring folder '%s' as MAPIProvider->GetFolder() did not return a SyncFolder object", (isset($row[PR_DISPLAY_NAME]) ? $row[PR_DISPLAY_NAME] : "unknown"))); |
|
| 360 | + } |
|
| 361 | + } |
|
| 362 | + |
|
| 363 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetHierarchy(): processed %d folders, starting parent remap", count($folders))); |
|
| 364 | + // reloop the folders to make sure all parentids are mapped correctly |
|
| 365 | + $dm = GSync::GetDeviceManager(); |
|
| 366 | + foreach ($folders as $folder) { |
|
| 367 | + if ($folder->parentid !== "0") { |
|
| 368 | + // SYSTEM user's parentid points to $rootfolderprops[PR_SOURCE_KEY], but they need to be on the top level |
|
| 369 | + $folder->parentid = (strtoupper($this->storeName) == "SYSTEM" && $folder->parentid == bin2hex($rootfolderprops[PR_SOURCE_KEY])) ? '0' : $dm->GetFolderIdForBackendId($folder->parentid); |
|
| 370 | + } |
|
| 371 | + } |
|
| 372 | + |
|
| 373 | + return $folders; |
|
| 374 | + } |
|
| 375 | + |
|
| 376 | + /** |
|
| 377 | + * Returns the importer to process changes from the mobile |
|
| 378 | + * If no $folderid is given, hierarchy importer is expected. |
|
| 379 | + * |
|
| 380 | + * @param string $folderid (opt) |
|
| 381 | + * |
|
| 382 | + * @return object(ImportChanges) |
|
| 383 | + */ |
|
| 384 | + public function GetImporter($folderid = false) { |
|
| 385 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetImporter() folderid: '%s'", Utils::PrintAsString($folderid))); |
|
| 386 | + if ($folderid !== false) { |
|
| 387 | + // check if the user of the current store has permissions to import to this folderid |
|
| 388 | + if ($this->storeName != $this->mainUser && !$this->hasSecretaryACLs($this->store, $folderid)) { |
|
| 389 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetImporter(): missing permissions on folderid: '%s'.", Utils::PrintAsString($folderid))); |
|
| 390 | + |
|
| 391 | + return false; |
|
| 392 | + } |
|
| 393 | + |
|
| 394 | + return new ImportChangesICS($this->session, $this->store, hex2bin($folderid)); |
|
| 395 | + } |
|
| 396 | + |
|
| 397 | + return new ImportChangesICS($this->session, $this->store); |
|
| 398 | + } |
|
| 399 | + |
|
| 400 | + /** |
|
| 401 | + * Returns the exporter to send changes to the mobile |
|
| 402 | + * If no $folderid is given, hierarchy exporter is expected. |
|
| 403 | + * |
|
| 404 | + * @param string $folderid (opt) |
|
| 405 | + * |
|
| 406 | + * @throws StatusException |
|
| 407 | + * |
|
| 408 | + * @return object(ExportChanges) |
|
| 409 | + */ |
|
| 410 | + public function GetExporter($folderid = false) { |
|
| 411 | + if ($folderid !== false) { |
|
| 412 | + // check if the user of the current store has permissions to export from this folderid |
|
| 413 | + if ($this->storeName != $this->mainUser && !$this->hasSecretaryACLs($this->store, $folderid)) { |
|
| 414 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetExporter(): missing permissions on folderid: '%s'.", Utils::PrintAsString($folderid))); |
|
| 415 | + |
|
| 416 | + return false; |
|
| 417 | + } |
|
| 418 | + |
|
| 419 | + return new ExportChangesICS($this->session, $this->store, hex2bin($folderid)); |
|
| 420 | + } |
|
| 421 | + |
|
| 422 | + return new ExportChangesICS($this->session, $this->store); |
|
| 423 | + } |
|
| 424 | + |
|
| 425 | + /** |
|
| 426 | + * Sends an e-mail |
|
| 427 | + * This messages needs to be saved into the 'sent items' folder. |
|
| 428 | + * |
|
| 429 | + * @param SyncSendMail $sm SyncSendMail object |
|
| 430 | + * |
|
| 431 | + * @throws StatusException |
|
| 432 | + * |
|
| 433 | + * @return bool |
|
| 434 | + */ |
|
| 435 | + public function SendMail($sm) { |
|
| 436 | + // Check if imtomapi function is available and use it to send the mime message. |
|
| 437 | + // It is available since ZCP 7.0.6 |
|
| 438 | + // @see http://jira.zarafa.com/browse/ZCP-9508 |
|
| 439 | + if (!(function_exists('mapi_feature') && mapi_feature('INETMAPI_IMTOMAPI'))) { |
|
| 440 | + throw new StatusException("Grommunio->SendMail(): ZCP/KC version is too old, INETMAPI_IMTOMAPI is not available. Install at least ZCP version 7.0.6 or later.", SYNC_COMMONSTATUS_MAILSUBMISSIONFAILED, null, LOGLEVEL_FATAL); |
|
| 441 | + |
|
| 442 | + return false; |
|
| 443 | + } |
|
| 444 | + $mimeLength = strlen($sm->mime); |
|
| 445 | + SLog::Write(LOGLEVEL_DEBUG, sprintf( |
|
| 446 | + "Grommunio->SendMail(): RFC822: %d bytes forward-id: '%s' reply-id: '%s' parent-id: '%s' SaveInSent: '%s' ReplaceMIME: '%s'", |
|
| 447 | + $mimeLength, |
|
| 448 | + Utils::PrintAsString($sm->forwardflag), |
|
| 449 | + Utils::PrintAsString($sm->replyflag), |
|
| 450 | + Utils::PrintAsString((isset($sm->source->folderid) ? $sm->source->folderid : false)), |
|
| 451 | + Utils::PrintAsString(($sm->saveinsent)), |
|
| 452 | + Utils::PrintAsString(isset($sm->replacemime)) |
|
| 453 | + )); |
|
| 454 | + if ($mimeLength == 0) { |
|
| 455 | + throw new StatusException("Grommunio->SendMail(): empty mail data", SYNC_COMMONSTATUS_MAILSUBMISSIONFAILED); |
|
| 456 | + } |
|
| 457 | + |
|
| 458 | + $sendMailProps = MAPIMapping::GetSendMailProperties(); |
|
| 459 | + $sendMailProps = getPropIdsFromStrings($this->defaultstore, $sendMailProps); |
|
| 460 | + |
|
| 461 | + // Open the outbox and create the message there |
|
| 462 | + $storeprops = mapi_getprops($this->defaultstore, [$sendMailProps["outboxentryid"], $sendMailProps["ipmsentmailentryid"]]); |
|
| 463 | + if (isset($storeprops[$sendMailProps["outboxentryid"]])) { |
|
| 464 | + $outbox = mapi_msgstore_openentry($this->defaultstore, $storeprops[$sendMailProps["outboxentryid"]]); |
|
| 465 | + } |
|
| 466 | + |
|
| 467 | + if (!$outbox) { |
|
| 468 | + throw new StatusException(sprintf("Grommunio->SendMail(): No Outbox found or unable to create message: 0x%X", mapi_last_hresult()), SYNC_COMMONSTATUS_SERVERERROR); |
|
| 469 | + } |
|
| 470 | + |
|
| 471 | + $mapimessage = mapi_folder_createmessage($outbox); |
|
| 472 | + |
|
| 473 | + // message properties to be set |
|
| 474 | + $mapiprops = []; |
|
| 475 | + // only save the outgoing in sent items folder if the mobile requests it |
|
| 476 | + $mapiprops[$sendMailProps["sentmailentryid"]] = $storeprops[$sendMailProps["ipmsentmailentryid"]]; |
|
| 477 | + |
|
| 478 | + SLog::Write(LOGLEVEL_DEBUG, "Grommunio->SendMail(): Use the mapi_inetmapi_imtomapi function"); |
|
| 479 | + $ab = mapi_openaddressbook($this->session); |
|
| 480 | + mapi_inetmapi_imtomapi($this->session, $this->defaultstore, $ab, $mapimessage, $sm->mime, []); |
|
| 481 | + |
|
| 482 | + // Set the appSeqNr so that tracking tab can be updated for meeting request updates |
|
| 483 | + // @see http://jira.zarafa.com/browse/ZP-68 |
|
| 484 | + $meetingRequestProps = MAPIMapping::GetMeetingRequestProperties(); |
|
| 485 | + $meetingRequestProps = getPropIdsFromStrings($this->defaultstore, $meetingRequestProps); |
|
| 486 | + $props = mapi_getprops($mapimessage, [PR_MESSAGE_CLASS, $meetingRequestProps["goidtag"], $sendMailProps["internetcpid"], $sendMailProps["body"], $sendMailProps["html"], $sendMailProps["rtf"], $sendMailProps["rtfinsync"]]); |
|
| 487 | + |
|
| 488 | + // Convert sent message's body to UTF-8 if it was a HTML message. |
|
| 489 | + // @see http://jira.zarafa.com/browse/ZP-505 and http://jira.zarafa.com/browse/ZP-555 |
|
| 490 | + if (isset($props[$sendMailProps["internetcpid"]]) && $props[$sendMailProps["internetcpid"]] != INTERNET_CPID_UTF8 && MAPIUtils::GetNativeBodyType($props) == SYNC_BODYPREFERENCE_HTML) { |
|
| 491 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->SendMail(): Sent email cpid is not unicode (%d). Set it to unicode and convert email html body.", $props[$sendMailProps["internetcpid"]])); |
|
| 492 | + $mapiprops[$sendMailProps["internetcpid"]] = INTERNET_CPID_UTF8; |
|
| 493 | + |
|
| 494 | + $bodyHtml = MAPIUtils::readPropStream($mapimessage, PR_HTML); |
|
| 495 | + $bodyHtml = Utils::ConvertCodepageStringToUtf8($props[$sendMailProps["internetcpid"]], $bodyHtml); |
|
| 496 | + $mapiprops[$sendMailProps["html"]] = $bodyHtml; |
|
| 497 | + |
|
| 498 | + mapi_setprops($mapimessage, $mapiprops); |
|
| 499 | + } |
|
| 500 | + if (stripos($props[PR_MESSAGE_CLASS], "IPM.Schedule.Meeting.Resp.") === 0) { |
|
| 501 | + // search for calendar items using goid |
|
| 502 | + $mr = new Meetingrequest($this->defaultstore, $mapimessage); |
|
| 503 | + $appointments = $mr->findCalendarItems($props[$meetingRequestProps["goidtag"]]); |
|
| 504 | + if (is_array($appointments) && !empty($appointments)) { |
|
| 505 | + $app = mapi_msgstore_openentry($this->defaultstore, $appointments[0]); |
|
| 506 | + $appprops = mapi_getprops($app, [$meetingRequestProps["appSeqNr"]]); |
|
| 507 | + if (isset($appprops[$meetingRequestProps["appSeqNr"]]) && $appprops[$meetingRequestProps["appSeqNr"]]) { |
|
| 508 | + $mapiprops[$meetingRequestProps["appSeqNr"]] = $appprops[$meetingRequestProps["appSeqNr"]]; |
|
| 509 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->SendMail(): Set sequence number to:%d", $appprops[$meetingRequestProps["appSeqNr"]])); |
|
| 510 | + } |
|
| 511 | + } |
|
| 512 | + } |
|
| 513 | + |
|
| 514 | + // Delete the PR_SENT_REPRESENTING_* properties because some android devices |
|
| 515 | + // do not send neither From nor Sender header causing empty PR_SENT_REPRESENTING_NAME and |
|
| 516 | + // PR_SENT_REPRESENTING_EMAIL_ADDRESS properties and "broken" PR_SENT_REPRESENTING_ENTRYID |
|
| 517 | + // which results in spooler not being able to send the message. |
|
| 518 | + // @see http://jira.zarafa.com/browse/ZP-85 |
|
| 519 | + mapi_deleteprops( |
|
| 520 | + $mapimessage, |
|
| 521 | + [ |
|
| 522 | + $sendMailProps["sentrepresentingname"], |
|
| 523 | + $sendMailProps["sentrepresentingemail"], |
|
| 524 | + $sendMailProps["representingentryid"], |
|
| 525 | + $sendMailProps["sentrepresentingaddt"], |
|
| 526 | + $sendMailProps["sentrepresentinsrchk"], |
|
| 527 | + ] |
|
| 528 | + ); |
|
| 529 | + |
|
| 530 | + if (isset($sm->source->itemid) && $sm->source->itemid) { |
|
| 531 | + // answering an email in a public/shared folder |
|
| 532 | + // TODO as the store is setup, we should actually user $this->store instead of $this->defaultstore - nevertheless we need to make sure this store is able to send mail (has an outbox) |
|
| 533 | + if (!$this->Setup(GSync::GetAdditionalSyncFolderStore($sm->source->folderid))) { |
|
| 534 | + throw new StatusException(sprintf("Grommunio->SendMail() could not Setup() the backend for folder id '%s'", $sm->source->folderid), SYNC_COMMONSTATUS_SERVERERROR); |
|
| 535 | + } |
|
| 536 | + |
|
| 537 | + $entryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($sm->source->folderid), hex2bin($sm->source->itemid)); |
|
| 538 | + if ($entryid) { |
|
| 539 | + $fwmessage = mapi_msgstore_openentry($this->store, $entryid); |
|
| 540 | + } |
|
| 541 | + |
|
| 542 | + if (isset($fwmessage) && $fwmessage) { |
|
| 543 | + // update icon and last_verb when forwarding or replying message |
|
| 544 | + // reply-all (verb 103) is not supported, as we cannot really detect this case |
|
| 545 | + if ($sm->forwardflag) { |
|
| 546 | + $updateProps = [ |
|
| 547 | + PR_ICON_INDEX => 262, |
|
| 548 | + PR_LAST_VERB_EXECUTED => 104, |
|
| 549 | + ]; |
|
| 550 | + } |
|
| 551 | + elseif ($sm->replyflag) { |
|
| 552 | + $updateProps = [ |
|
| 553 | + PR_ICON_INDEX => 261, |
|
| 554 | + PR_LAST_VERB_EXECUTED => 102, |
|
| 555 | + ]; |
|
| 556 | + } |
|
| 557 | + if (isset($updateProps)) { |
|
| 558 | + $updateProps[PR_LAST_VERB_EXECUTION_TIME] = time(); |
|
| 559 | + mapi_setprops($fwmessage, $updateProps); |
|
| 560 | + mapi_savechanges($fwmessage); |
|
| 561 | + } |
|
| 562 | + |
|
| 563 | + // only attach the original message if the mobile does not send it itself |
|
| 564 | + if (!isset($sm->replacemime)) { |
|
| 565 | + // get message's body in order to append forward or reply text |
|
| 566 | + if (!isset($body)) { |
|
| 567 | + $body = MAPIUtils::readPropStream($mapimessage, PR_BODY); |
|
| 568 | + } |
|
| 569 | + if (!isset($bodyHtml)) { |
|
| 570 | + $bodyHtml = MAPIUtils::readPropStream($mapimessage, PR_HTML); |
|
| 571 | + } |
|
| 572 | + $cpid = mapi_getprops($fwmessage, [$sendMailProps["internetcpid"]]); |
|
| 573 | + if ($sm->forwardflag) { |
|
| 574 | + // attach the original attachments to the outgoing message |
|
| 575 | + $this->copyAttachments($mapimessage, $fwmessage); |
|
| 576 | + } |
|
| 577 | + |
|
| 578 | + // regarding the conversion @see ZP-470 |
|
| 579 | + if (strlen($body) > 0) { |
|
| 580 | + $fwbody = MAPIUtils::readPropStream($fwmessage, PR_BODY); |
|
| 581 | + // if only the old message's cpid is set, convert from old charset to utf-8 |
|
| 582 | + if (isset($cpid[$sendMailProps["internetcpid"]]) && $cpid[$sendMailProps["internetcpid"]] != INTERNET_CPID_UTF8) { |
|
| 583 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->SendMail(): convert plain forwarded message charset (only fw set) from '%s' to '65001'", $cpid[$sendMailProps["internetcpid"]])); |
|
| 584 | + $fwbody = Utils::ConvertCodepageStringToUtf8($cpid[$sendMailProps["internetcpid"]], $fwbody); |
|
| 585 | + } |
|
| 586 | + // otherwise to the general conversion |
|
| 587 | + else { |
|
| 588 | + SLog::Write(LOGLEVEL_DEBUG, "Grommunio->SendMail(): no charset conversion done for plain forwarded message"); |
|
| 589 | + $fwbody = w2u($fwbody); |
|
| 590 | + } |
|
| 591 | + |
|
| 592 | + $mapiprops[$sendMailProps["body"]] = $body . "\r\n\r\n" . $fwbody; |
|
| 593 | + } |
|
| 594 | + |
|
| 595 | + if (strlen($bodyHtml) > 0) { |
|
| 596 | + $fwbodyHtml = MAPIUtils::readPropStream($fwmessage, PR_HTML); |
|
| 597 | + // if only new message's cpid is set, convert to UTF-8 |
|
| 598 | + if (isset($cpid[$sendMailProps["internetcpid"]]) && $cpid[$sendMailProps["internetcpid"]] != INTERNET_CPID_UTF8) { |
|
| 599 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->SendMail(): convert html forwarded message charset (only fw set) from '%s' to '65001'", $cpid[$sendMailProps["internetcpid"]])); |
|
| 600 | + $fwbodyHtml = Utils::ConvertCodepageStringToUtf8($cpid[$sendMailProps["internetcpid"]], $fwbodyHtml); |
|
| 601 | + } |
|
| 602 | + // otherwise to the general conversion |
|
| 603 | + else { |
|
| 604 | + SLog::Write(LOGLEVEL_DEBUG, "Grommunio->SendMail(): no charset conversion done for html forwarded message"); |
|
| 605 | + $fwbodyHtml = w2u($fwbodyHtml); |
|
| 606 | + } |
|
| 607 | + |
|
| 608 | + $mapiprops[$sendMailProps["html"]] = $bodyHtml . "<br><br>" . $fwbodyHtml; |
|
| 609 | + } |
|
| 610 | + } |
|
| 611 | + } |
|
| 612 | + else { |
|
| 613 | + // no fwmessage could be opened and we need it because we do not replace mime |
|
| 614 | + if (!isset($sm->replacemime) || $sm->replacemime == false) { |
|
| 615 | + throw new StatusException(sprintf("Grommunio->SendMail(): Could not open message id '%s' in folder id '%s' to be replied/forwarded: 0x%X", $sm->source->itemid, $sm->source->folderid, mapi_last_hresult()), SYNC_COMMONSTATUS_ITEMNOTFOUND); |
|
| 616 | + } |
|
| 617 | + } |
|
| 618 | + } |
|
| 619 | + |
|
| 620 | + mapi_setprops($mapimessage, $mapiprops); |
|
| 621 | + mapi_savechanges($mapimessage); |
|
| 622 | + mapi_message_submitmessage($mapimessage); |
|
| 623 | + $hr = mapi_last_hresult(); |
|
| 624 | + |
|
| 625 | + if ($hr) { |
|
| 626 | + switch ($hr) { |
|
| 627 | + case MAPI_E_STORE_FULL: |
|
| 628 | + $code = SYNC_COMMONSTATUS_MAILBOXQUOTAEXCEEDED; |
|
| 629 | + break; |
|
| 630 | + |
|
| 631 | + default: |
|
| 632 | + $code = SYNC_COMMONSTATUS_MAILSUBMISSIONFAILED; |
|
| 633 | + break; |
|
| 634 | + } |
|
| 635 | + |
|
| 636 | + throw new StatusException(sprintf("Grommunio->SendMail(): Error saving/submitting the message to the Outbox: 0x%X", $hr), $code); |
|
| 637 | + } |
|
| 638 | + |
|
| 639 | + SLog::Write(LOGLEVEL_DEBUG, "Grommunio->SendMail(): email submitted"); |
|
| 640 | + |
|
| 641 | + return true; |
|
| 642 | + } |
|
| 643 | + |
|
| 644 | + /** |
|
| 645 | + * Returns all available data of a single message. |
|
| 646 | + * |
|
| 647 | + * @param string $folderid |
|
| 648 | + * @param string $id |
|
| 649 | + * @param ContentParameters $contentparameters flag |
|
| 650 | + * |
|
| 651 | + * @throws StatusException |
|
| 652 | + * |
|
| 653 | + * @return object(SyncObject) |
|
| 654 | + */ |
|
| 655 | + public function Fetch($folderid, $id, $contentparameters) { |
|
| 656 | + // SEARCH fetches with folderid == false and PR_ENTRYID as ID |
|
| 657 | + if (!$folderid) { |
|
| 658 | + $entryid = hex2bin($id); |
|
| 659 | + $sk = $id; |
|
| 660 | + } |
|
| 661 | + else { |
|
| 662 | + // id might be in the new longid format, so we have to split it here |
|
| 663 | + list($fsk, $sk) = Utils::SplitMessageId($id); |
|
| 664 | + // get the entry id of the message |
|
| 665 | + $entryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($folderid), hex2bin($sk)); |
|
| 666 | + } |
|
| 667 | + if (!$entryid) { |
|
| 668 | + throw new StatusException(sprintf("Grommunio->Fetch('%s','%s'): Error getting entryid: 0x%X", $folderid, $sk, mapi_last_hresult()), SYNC_STATUS_OBJECTNOTFOUND); |
|
| 669 | + } |
|
| 670 | + |
|
| 671 | + // open the message |
|
| 672 | + $message = mapi_msgstore_openentry($this->store, $entryid); |
|
| 673 | + if (!$message) { |
|
| 674 | + throw new StatusException(sprintf("Grommunio->Fetch('%s','%s'): Error, unable to open message: 0x%X", $folderid, $sk, mapi_last_hresult()), SYNC_STATUS_OBJECTNOTFOUND); |
|
| 675 | + } |
|
| 676 | + |
|
| 677 | + // convert the mapi message into a SyncObject and return it |
|
| 678 | + $mapiprovider = new MAPIProvider($this->session, $this->store); |
|
| 679 | + |
|
| 680 | + // override truncation |
|
| 681 | + $contentparameters->SetTruncation(SYNC_TRUNCATION_ALL); |
|
| 682 | + // TODO check for body preferences |
|
| 683 | + return $mapiprovider->GetMessage($message, $contentparameters); |
|
| 684 | + } |
|
| 685 | + |
|
| 686 | + /** |
|
| 687 | + * Returns the waste basket. |
|
| 688 | + * |
|
| 689 | + * @return string |
|
| 690 | + */ |
|
| 691 | + public function GetWasteBasket() { |
|
| 692 | + if ($this->wastebasket) { |
|
| 693 | + return $this->wastebasket; |
|
| 694 | + } |
|
| 695 | + |
|
| 696 | + $storeprops = mapi_getprops($this->defaultstore, [PR_IPM_WASTEBASKET_ENTRYID]); |
|
| 697 | + if (isset($storeprops[PR_IPM_WASTEBASKET_ENTRYID])) { |
|
| 698 | + $wastebasket = mapi_msgstore_openentry($this->defaultstore, $storeprops[PR_IPM_WASTEBASKET_ENTRYID]); |
|
| 699 | + $wastebasketprops = mapi_getprops($wastebasket, [PR_SOURCE_KEY]); |
|
| 700 | + if (isset($wastebasketprops[PR_SOURCE_KEY])) { |
|
| 701 | + $this->wastebasket = bin2hex($wastebasketprops[PR_SOURCE_KEY]); |
|
| 702 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetWasteBasket(): Got waste basket with id '%s'", $this->wastebasket)); |
|
| 703 | + |
|
| 704 | + return $this->wastebasket; |
|
| 705 | + } |
|
| 706 | + } |
|
| 707 | + |
|
| 708 | + return false; |
|
| 709 | + } |
|
| 710 | + |
|
| 711 | + /** |
|
| 712 | + * Returns the content of the named attachment as stream. |
|
| 713 | + * |
|
| 714 | + * @param string $attname |
|
| 715 | + * |
|
| 716 | + * @throws StatusException |
|
| 717 | + * |
|
| 718 | + * @return SyncItemOperationsAttachment |
|
| 719 | + */ |
|
| 720 | + public function GetAttachmentData($attname) { |
|
| 721 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetAttachmentData('%s')", $attname)); |
|
| 722 | + |
|
| 723 | + if (!strpos($attname, ":")) { |
|
| 724 | + throw new StatusException(sprintf("Grommunio->GetAttachmentData('%s'): Error, attachment requested for non-existing item", $attname), SYNC_ITEMOPERATIONSSTATUS_INVALIDATT); |
|
| 725 | + } |
|
| 726 | + |
|
| 727 | + list($id, $attachnum, $parentEntryid) = explode(":", $attname); |
|
| 728 | + if (isset($parentEntryid)) { |
|
| 729 | + $this->Setup(GSync::GetAdditionalSyncFolderStore($parentEntryid)); |
|
| 730 | + } |
|
| 731 | + |
|
| 732 | + $entryid = hex2bin($id); |
|
| 733 | + $message = mapi_msgstore_openentry($this->store, $entryid); |
|
| 734 | + if (!$message) { |
|
| 735 | + throw new StatusException(sprintf("Grommunio->GetAttachmentData('%s'): Error, unable to open item for attachment data for id '%s' with: 0x%X", $attname, $id, mapi_last_hresult()), SYNC_ITEMOPERATIONSSTATUS_INVALIDATT); |
|
| 736 | + } |
|
| 737 | + |
|
| 738 | + MAPIUtils::ParseSmime($this->session, $this->defaultstore, $this->getAddressbook(), $message); |
|
| 739 | + $attach = mapi_message_openattach($message, $attachnum); |
|
| 740 | + if (!$attach) { |
|
| 741 | + throw new StatusException(sprintf("Grommunio->GetAttachmentData('%s'): Error, unable to open attachment number '%s' with: 0x%X", $attname, $attachnum, mapi_last_hresult()), SYNC_ITEMOPERATIONSSTATUS_INVALIDATT); |
|
| 742 | + } |
|
| 743 | + |
|
| 744 | + // get necessary attachment props |
|
| 745 | + $attprops = mapi_getprops($attach, [PR_ATTACH_MIME_TAG, PR_ATTACH_MIME_TAG_W, PR_ATTACH_METHOD]); |
|
| 746 | + $attachment = new SyncItemOperationsAttachment(); |
|
| 747 | + // check if it's an embedded message and open it in such a case |
|
| 748 | + if (isset($attprops[PR_ATTACH_METHOD]) && $attprops[PR_ATTACH_METHOD] == ATTACH_EMBEDDED_MSG) { |
|
| 749 | + $embMessage = mapi_attach_openobj($attach); |
|
| 750 | + $addrbook = $this->getAddressbook(); |
|
| 751 | + $stream = mapi_inetmapi_imtoinet($this->session, $addrbook, $embMessage, ['use_tnef' => -1]); |
|
| 752 | + // set the default contenttype for this kind of messages |
|
| 753 | + $attachment->contenttype = "message/rfc822"; |
|
| 754 | + } |
|
| 755 | + else { |
|
| 756 | + $stream = mapi_openproperty($attach, PR_ATTACH_DATA_BIN, IID_IStream, 0, 0); |
|
| 757 | + } |
|
| 758 | + |
|
| 759 | + if (!$stream) { |
|
| 760 | + throw new StatusException(sprintf("Grommunio->GetAttachmentData('%s'): Error, unable to open attachment data stream: 0x%X", $attname, mapi_last_hresult()), SYNC_ITEMOPERATIONSSTATUS_INVALIDATT); |
|
| 761 | + } |
|
| 762 | + |
|
| 763 | + // put the mapi stream into a wrapper to get a standard stream |
|
| 764 | + $attachment->data = MAPIStreamWrapper::Open($stream); |
|
| 765 | + if (isset($attprops[PR_ATTACH_MIME_TAG])) { |
|
| 766 | + $attachment->contenttype = $attprops[PR_ATTACH_MIME_TAG]; |
|
| 767 | + } |
|
| 768 | + elseif (isset($attprops[PR_ATTACH_MIME_TAG_W])) { |
|
| 769 | + $attachment->contenttype = $attprops[PR_ATTACH_MIME_TAG_W]; |
|
| 770 | + } |
|
| 771 | + // TODO default contenttype |
|
| 772 | + return $attachment; |
|
| 773 | + } |
|
| 774 | + |
|
| 775 | + /** |
|
| 776 | + * Deletes all contents of the specified folder. |
|
| 777 | + * This is generally used to empty the trash (wastebasked), but could also be used on any |
|
| 778 | + * other folder. |
|
| 779 | + * |
|
| 780 | + * @param string $folderid |
|
| 781 | + * @param bool $includeSubfolders (opt) also delete sub folders, default true |
|
| 782 | + * |
|
| 783 | + * @throws StatusException |
|
| 784 | + * |
|
| 785 | + * @return bool |
|
| 786 | + */ |
|
| 787 | + public function EmptyFolder($folderid, $includeSubfolders = true) { |
|
| 788 | + $folderentryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($folderid)); |
|
| 789 | + if (!$folderentryid) { |
|
| 790 | + throw new StatusException(sprintf("Grommunio->EmptyFolder('%s','%s'): Error, unable to open folder (no entry id)", $folderid, Utils::PrintAsString($includeSubfolders)), SYNC_ITEMOPERATIONSSTATUS_SERVERERROR); |
|
| 791 | + } |
|
| 792 | + $folder = mapi_msgstore_openentry($this->store, $folderentryid); |
|
| 793 | + |
|
| 794 | + if (!$folder) { |
|
| 795 | + throw new StatusException(sprintf("Grommunio->EmptyFolder('%s','%s'): Error, unable to open parent folder (open entry)", $folderid, Utils::PrintAsString($includeSubfolders)), SYNC_ITEMOPERATIONSSTATUS_SERVERERROR); |
|
| 796 | + } |
|
| 797 | + |
|
| 798 | + $flags = 0; |
|
| 799 | + if ($includeSubfolders) { |
|
| 800 | + $flags = DEL_ASSOCIATED; |
|
| 801 | + } |
|
| 802 | + |
|
| 803 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->EmptyFolder('%s','%s'): emptying folder", $folderid, Utils::PrintAsString($includeSubfolders))); |
|
| 804 | + |
|
| 805 | + // empty folder! |
|
| 806 | + mapi_folder_emptyfolder($folder, $flags); |
|
| 807 | + if (mapi_last_hresult()) { |
|
| 808 | + throw new StatusException(sprintf("Grommunio->EmptyFolder('%s','%s'): Error, mapi_folder_emptyfolder() failed: 0x%X", $folderid, Utils::PrintAsString($includeSubfolders), mapi_last_hresult()), SYNC_ITEMOPERATIONSSTATUS_SERVERERROR); |
|
| 809 | + } |
|
| 810 | + |
|
| 811 | + return true; |
|
| 812 | + } |
|
| 813 | + |
|
| 814 | + /** |
|
| 815 | + * Processes a response to a meeting request. |
|
| 816 | + * CalendarID is a reference and has to be set if a new calendar item is created. |
|
| 817 | + * |
|
| 818 | + * @param string $requestid id of the object containing the request |
|
| 819 | + * @param string $folderid id of the parent folder of $requestid |
|
| 820 | + * @param string $response |
|
| 821 | + * |
|
| 822 | + * @throws StatusException |
|
| 823 | + * |
|
| 824 | + * @return string id of the created/updated calendar obj |
|
| 825 | + */ |
|
| 826 | + public function MeetingResponse($requestid, $folderid, $response) { |
|
| 827 | + // Use standard meeting response code to process meeting request |
|
| 828 | + list($fid, $requestid) = Utils::SplitMessageId($requestid); |
|
| 829 | + $reqentryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($folderid), hex2bin($requestid)); |
|
| 830 | + if (!$reqentryid) { |
|
| 831 | + throw new StatusException(sprintf("Grommunio->MeetingResponse('%s', '%s', '%s'): Error, unable to entryid of the message 0x%X", $requestid, $folderid, $response, mapi_last_hresult()), SYNC_MEETRESPSTATUS_INVALIDMEETREQ); |
|
| 832 | + } |
|
| 833 | + |
|
| 834 | + $mapimessage = mapi_msgstore_openentry($this->store, $reqentryid); |
|
| 835 | + if (!$mapimessage) { |
|
| 836 | + throw new StatusException(sprintf("Grommunio->MeetingResponse('%s','%s', '%s'): Error, unable to open request message for response 0x%X", $requestid, $folderid, $response, mapi_last_hresult()), SYNC_MEETRESPSTATUS_INVALIDMEETREQ); |
|
| 837 | + } |
|
| 838 | + |
|
| 839 | + // ios sends calendar item in MeetingResponse |
|
| 840 | + // @see https://jira.z-hub.io/browse/ZP-1524 |
|
| 841 | + $folderClass = GSync::GetDeviceManager()->GetFolderClassFromCacheByID($fid); |
|
| 842 | + // find the corresponding meeting request |
|
| 843 | + if ($folderClass != 'Email') { |
|
| 844 | + $props = MAPIMapping::GetMeetingRequestProperties(); |
|
| 845 | + $props = getPropIdsFromStrings($this->store, $props); |
|
| 846 | + |
|
| 847 | + $messageprops = mapi_getprops($mapimessage, [$props["goidtag"]]); |
|
| 848 | + $goid = $messageprops[$props["goidtag"]]; |
|
| 849 | + |
|
| 850 | + $mapiprovider = new MAPIProvider($this->session, $this->store); |
|
| 851 | + $inboxprops = $mapiprovider->GetInboxProps(); |
|
| 852 | + $folder = mapi_msgstore_openentry($this->store, $inboxprops[PR_ENTRYID]); |
|
| 853 | + |
|
| 854 | + // Find the item by restricting all items to the correct ID |
|
| 855 | + $restrict = [RES_AND, [ |
|
| 856 | + [RES_PROPERTY, |
|
| 857 | + [ |
|
| 858 | + RELOP => RELOP_EQ, |
|
| 859 | + ULPROPTAG => $props["goidtag"], |
|
| 860 | + VALUE => $goid, |
|
| 861 | + ], |
|
| 862 | + ], |
|
| 863 | + ]]; |
|
| 864 | + |
|
| 865 | + $inboxcontents = mapi_folder_getcontentstable($folder); |
|
| 866 | + |
|
| 867 | + $rows = mapi_table_queryallrows($inboxcontents, [PR_ENTRYID], $restrict); |
|
| 868 | + if (empty($rows)) { |
|
| 869 | + throw new StatusException(sprintf("Grommunio->MeetingResponse('%s','%s', '%s'): Error, meeting request not found in the inbox", $requestid, $folderid, $response), SYNC_MEETRESPSTATUS_INVALIDMEETREQ); |
|
| 870 | + } |
|
| 871 | + SLog::Write(LOGLEVEL_DEBUG, "Grommunio->MeetingResponse found meeting request in the inbox"); |
|
| 872 | + $mapimessage = mapi_msgstore_openentry($this->store, $rows[0][PR_ENTRYID]); |
|
| 873 | + $reqentryid = $rows[0][PR_ENTRYID]; |
|
| 874 | + } |
|
| 875 | + |
|
| 876 | + $meetingrequest = new Meetingrequest($this->store, $mapimessage, $this->session); |
|
| 877 | + |
|
| 878 | + if (!$meetingrequest->isMeetingRequest()) { |
|
| 879 | + throw new StatusException(sprintf("Grommunio->MeetingResponse('%s','%s', '%s'): Error, attempt to respond to non-meeting request", $requestid, $folderid, $response), SYNC_MEETRESPSTATUS_INVALIDMEETREQ); |
|
| 880 | + } |
|
| 881 | + |
|
| 882 | + if ($meetingrequest->isLocalOrganiser()) { |
|
| 883 | + throw new StatusException(sprintf("Grommunio->MeetingResponse('%s','%s', '%s'): Error, attempt to response to meeting request that we organized", $requestid, $folderid, $response), SYNC_MEETRESPSTATUS_INVALIDMEETREQ); |
|
| 884 | + } |
|
| 885 | + |
|
| 886 | + // Process the meeting response. We don't have to send the actual meeting response |
|
| 887 | + // e-mail, because the device will send it itself. This seems not to be the case |
|
| 888 | + // anymore for the ios devices since at least version 12.4. grommunio-sync will send the |
|
| 889 | + // accepted email in such a case. |
|
| 890 | + // @see https://jira.z-hub.io/browse/ZP-1524 |
|
| 891 | + $sendresponse = false; |
|
| 892 | + $deviceType = strtolower(Request::GetDeviceType()); |
|
| 893 | + if ($deviceType == 'iphone' || $deviceType == 'ipad' || $deviceType == 'ipod') { |
|
| 894 | + $matches = []; |
|
| 895 | + if (preg_match("/^Apple-.*?\\/(\\d{4})\\./", Request::GetUserAgent(), $matches) && isset($matches[1]) && $matches[1] >= 1607) { |
|
| 896 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->MeetingResponse: iOS device %s->%s", Request::GetDeviceType(), Request::GetUserAgent())); |
|
| 897 | + $sendresponse = true; |
|
| 898 | + } |
|
| 899 | + } |
|
| 900 | + |
|
| 901 | + switch ($response) { |
|
| 902 | + case 1: // accept |
|
| 903 | + default: |
|
| 904 | + $entryid = $meetingrequest->doAccept(false, $sendresponse, false, false, false, false, true); // last true is the $userAction |
|
| 905 | + break; |
|
| 906 | + |
|
| 907 | + case 2: // tentative |
|
| 908 | + $entryid = $meetingrequest->doAccept(true, $sendresponse, false, false, false, false, true); // last true is the $userAction |
|
| 909 | + break; |
|
| 910 | + |
|
| 911 | + case 3: // decline |
|
| 912 | + $meetingrequest->doDecline(false); |
|
| 913 | + break; |
|
| 914 | + } |
|
| 915 | + |
|
| 916 | + // F/B will be updated on logoff |
|
| 917 | + |
|
| 918 | + // We have to return the ID of the new calendar item, so do that here |
|
| 919 | + $calendarid = ""; |
|
| 920 | + $calFolderId = ""; |
|
| 921 | + if (isset($entryid)) { |
|
| 922 | + $newitem = mapi_msgstore_openentry($this->store, $entryid); |
|
| 923 | + // new item might be in a delegator's store. ActiveSync does not support accepting them. |
|
| 924 | + if (!$newitem) { |
|
| 925 | + throw new StatusException(sprintf("Grommunio->MeetingResponse('%s','%s', '%s'): Object with entryid '%s' was not found in user's store (0x%X). It might be in a delegator's store.", $requestid, $folderid, $response, bin2hex($entryid), mapi_last_hresult()), SYNC_MEETRESPSTATUS_SERVERERROR, null, LOGLEVEL_WARN); |
|
| 926 | + } |
|
| 927 | + |
|
| 928 | + $newprops = mapi_getprops($newitem, [PR_SOURCE_KEY, PR_PARENT_SOURCE_KEY]); |
|
| 929 | + $calendarid = bin2hex($newprops[PR_SOURCE_KEY]); |
|
| 930 | + $calFolderId = bin2hex($newprops[PR_PARENT_SOURCE_KEY]); |
|
| 931 | + } |
|
| 932 | + |
|
| 933 | + // on recurring items, the MeetingRequest class responds with a wrong entryid |
|
| 934 | + if ($requestid == $calendarid) { |
|
| 935 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->MeetingResponse('%s','%s', '%s'): returned calendar id is the same as the requestid - re-searching", $requestid, $folderid, $response)); |
|
| 936 | + |
|
| 937 | + if (empty($props)) { |
|
| 938 | + $props = MAPIMapping::GetMeetingRequestProperties(); |
|
| 939 | + $props = getPropIdsFromStrings($this->store, $props); |
|
| 940 | + |
|
| 941 | + $messageprops = mapi_getprops($mapimessage, [$props["goidtag"]]); |
|
| 942 | + $goid = $messageprops[$props["goidtag"]]; |
|
| 943 | + } |
|
| 944 | + |
|
| 945 | + $items = $meetingrequest->findCalendarItems($goid); |
|
| 946 | + |
|
| 947 | + if (is_array($items)) { |
|
| 948 | + $newitem = mapi_msgstore_openentry($this->store, $items[0]); |
|
| 949 | + $newprops = mapi_getprops($newitem, [PR_SOURCE_KEY, PR_PARENT_SOURCE_KEY]); |
|
| 950 | + $calendarid = bin2hex($newprops[PR_SOURCE_KEY]); |
|
| 951 | + $calFolderId = bin2hex($newprops[PR_PARENT_SOURCE_KEY]); |
|
| 952 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->MeetingResponse('%s','%s', '%s'): found other calendar entryid", $requestid, $folderid, $response)); |
|
| 953 | + } |
|
| 954 | + |
|
| 955 | + if ($requestid == $calendarid) { |
|
| 956 | + throw new StatusException(sprintf("Grommunio->MeetingResponse('%s','%s', '%s'): Error finding the accepted meeting response in the calendar", $requestid, $folderid, $response), SYNC_MEETRESPSTATUS_INVALIDMEETREQ); |
|
| 957 | + } |
|
| 958 | + } |
|
| 959 | + |
|
| 960 | + // delete meeting request from Inbox |
|
| 961 | + if ($folderClass == 'Email') { |
|
| 962 | + $folderentryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($folderid)); |
|
| 963 | + $folder = mapi_msgstore_openentry($this->store, $folderentryid); |
|
| 964 | + } |
|
| 965 | + mapi_folder_deletemessages($folder, [$reqentryid], 0); |
|
| 966 | + |
|
| 967 | + $prefix = ''; |
|
| 968 | + // prepend the short folderid of the target calendar: if available and short ids are used |
|
| 969 | + if ($calFolderId) { |
|
| 970 | + $shortFolderId = GSync::GetDeviceManager()->GetFolderIdForBackendId($calFolderId); |
|
| 971 | + if ($calFolderId != $shortFolderId) { |
|
| 972 | + $prefix = $shortFolderId . ':'; |
|
| 973 | + } |
|
| 974 | + } |
|
| 975 | + |
|
| 976 | + return $prefix . $calendarid; |
|
| 977 | + } |
|
| 978 | + |
|
| 979 | + /** |
|
| 980 | + * Indicates if the backend has a ChangesSink. |
|
| 981 | + * A sink is an active notification mechanism which does not need polling. |
|
| 982 | + * Since Zarafa 7.0.5 such a sink is available. |
|
| 983 | + * The grommunio backend uses this method to initialize the sink with mapi. |
|
| 984 | + * |
|
| 985 | + * @return bool |
|
| 986 | + */ |
|
| 987 | + public function HasChangesSink() { |
|
| 988 | + if (!$this->notifications) { |
|
| 989 | + SLog::Write(LOGLEVEL_DEBUG, "Grommunio->HasChangesSink(): sink is not available"); |
|
| 990 | + |
|
| 991 | + return false; |
|
| 992 | + } |
|
| 993 | + |
|
| 994 | + $this->changesSink = @mapi_sink_create(); |
|
| 995 | + |
|
| 996 | + if (!$this->changesSink || mapi_last_hresult()) { |
|
| 997 | + SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->HasChangesSink(): sink could not be created with 0x%X", mapi_last_hresult())); |
|
| 998 | + |
|
| 999 | + return false; |
|
| 1000 | + } |
|
| 1001 | + |
|
| 1002 | + $this->changesSinkHierarchyHash = $this->getHierarchyHash(); |
|
| 1003 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->HasChangesSink(): created - HierarchyHash: %s", $this->changesSinkHierarchyHash)); |
|
| 1004 | + |
|
| 1005 | + // advise the main store and also to check if the connection supports it |
|
| 1006 | + return $this->adviseStoreToSink($this->defaultstore); |
|
| 1007 | + } |
|
| 1008 | + |
|
| 1009 | + /** |
|
| 1010 | + * The folder should be considered by the sink. |
|
| 1011 | + * Folders which were not initialized should not result in a notification |
|
| 1012 | + * of IBackend->ChangesSink(). |
|
| 1013 | + * |
|
| 1014 | + * @param string $folderid |
|
| 1015 | + * |
|
| 1016 | + * @return bool false if entryid can not be found for that folder |
|
| 1017 | + */ |
|
| 1018 | + public function ChangesSinkInitialize($folderid) { |
|
| 1019 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->ChangesSinkInitialize(): folderid '%s'", $folderid)); |
|
| 1020 | + |
|
| 1021 | + $entryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($folderid)); |
|
| 1022 | + if (!$entryid) { |
|
| 1023 | + return false; |
|
| 1024 | + } |
|
| 1025 | + |
|
| 1026 | + // add entryid to the monitored folders |
|
| 1027 | + $this->changesSinkFolders[$entryid] = $folderid; |
|
| 1028 | + |
|
| 1029 | + // advise the current store to the sink |
|
| 1030 | + return $this->adviseStoreToSink($this->store); |
|
| 1031 | + } |
|
| 1032 | + |
|
| 1033 | + /** |
|
| 1034 | + * The actual ChangesSink. |
|
| 1035 | + * For max. the $timeout value this method should block and if no changes |
|
| 1036 | + * are available return an empty array. |
|
| 1037 | + * If changes are available a list of folderids is expected. |
|
| 1038 | + * |
|
| 1039 | + * @param int $timeout max. amount of seconds to block |
|
| 1040 | + * |
|
| 1041 | + * @return array |
|
| 1042 | + */ |
|
| 1043 | + public function ChangesSink($timeout = 30) { |
|
| 1044 | + // clear the folder stats cache |
|
| 1045 | + unset($this->folderStatCache); |
|
| 1046 | + |
|
| 1047 | + $notifications = []; |
|
| 1048 | + $hierarchyNotifications = []; |
|
| 1049 | + $sinkresult = @mapi_sink_timedwait($this->changesSink, $timeout * 1000); |
|
| 1050 | + |
|
| 1051 | + if (!is_array($sinkresult)) { |
|
| 1052 | + throw new StatusException("Grommunio->ChangesSink(): Sink returned invalid notification, aborting", SyncCollections::OBSOLETE_CONNECTION); |
|
| 1053 | + } |
|
| 1054 | + |
|
| 1055 | + // reverse array so that the changes on folders are before changes on messages and |
|
| 1056 | + // it's possible to filter such notifications |
|
| 1057 | + $sinkresult = array_reverse($sinkresult, true); |
|
| 1058 | + foreach ($sinkresult as $sinknotif) { |
|
| 1059 | + // add a notification on a folder |
|
| 1060 | + if ($sinknotif['objtype'] == MAPI_FOLDER) { |
|
| 1061 | + $hierarchyNotifications[$sinknotif['entryid']] = IBackend::HIERARCHYNOTIFICATION; |
|
| 1062 | + } |
|
| 1063 | + // change on a message, remove hierarchy notification |
|
| 1064 | + if (isset($sinknotif['parentid']) && $sinknotif['objtype'] == MAPI_MESSAGE && isset($notifications[$sinknotif['parentid']])) { |
|
| 1065 | + unset($hierarchyNotifications[$sinknotif['parentid']]); |
|
| 1066 | + } |
|
| 1067 | + |
|
| 1068 | + // TODO check if adding $sinknotif['objtype'] = MAPI_MESSAGE wouldn't break anything |
|
| 1069 | + // check if something in the monitored folders changed |
|
| 1070 | + if (isset($sinknotif['parentid']) && array_key_exists($sinknotif['parentid'], $this->changesSinkFolders)) { |
|
| 1071 | + $notifications[] = $this->changesSinkFolders[$sinknotif['parentid']]; |
|
| 1072 | + } |
|
| 1073 | + // deletes and moves |
|
| 1074 | + if (isset($sinknotif['oldparentid']) && array_key_exists($sinknotif['oldparentid'], $this->changesSinkFolders)) { |
|
| 1075 | + $notifications[] = $this->changesSinkFolders[$sinknotif['oldparentid']]; |
|
| 1076 | + } |
|
| 1077 | + } |
|
| 1078 | + |
|
| 1079 | + // validate hierarchy notifications by comparing the hierarchy hashes (too many false positives otherwise) |
|
| 1080 | + if (!empty($hierarchyNotifications)) { |
|
| 1081 | + $hash = $this->getHierarchyHash(); |
|
| 1082 | + if ($hash !== $this->changesSinkHierarchyHash) { |
|
| 1083 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->ChangesSink() Hierarchy notification, pending validation. New hierarchyHash: %s", $hash)); |
|
| 1084 | + $notifications[] = IBackend::HIERARCHYNOTIFICATION; |
|
| 1085 | + $this->changesSinkHierarchyHash = $hash; |
|
| 1086 | + } |
|
| 1087 | + } |
|
| 1088 | + |
|
| 1089 | + return $notifications; |
|
| 1090 | + } |
|
| 1091 | + |
|
| 1092 | + /** |
|
| 1093 | + * Applies settings to and gets information from the device. |
|
| 1094 | + * |
|
| 1095 | + * @param SyncObject $settings (SyncOOF, SyncUserInformation, SyncRightsManagementTemplates possible) |
|
| 1096 | + * |
|
| 1097 | + * @return SyncObject $settings |
|
| 1098 | + */ |
|
| 1099 | + public function Settings($settings) { |
|
| 1100 | + if ($settings instanceof SyncOOF) { |
|
| 1101 | + $this->settingsOOF($settings); |
|
| 1102 | + } |
|
| 1103 | + |
|
| 1104 | + if ($settings instanceof SyncUserInformation) { |
|
| 1105 | + $this->settingsUserInformation($settings); |
|
| 1106 | + } |
|
| 1107 | + |
|
| 1108 | + if ($settings instanceof SyncRightsManagementTemplates) { |
|
| 1109 | + $this->settingsRightsManagementTemplates($settings); |
|
| 1110 | + } |
|
| 1111 | + |
|
| 1112 | + return $settings; |
|
| 1113 | + } |
|
| 1114 | + |
|
| 1115 | + /** |
|
| 1116 | + * Resolves recipients. |
|
| 1117 | + * |
|
| 1118 | + * @param SyncObject $resolveRecipients |
|
| 1119 | + * |
|
| 1120 | + * @return SyncObject $resolveRecipients |
|
| 1121 | + */ |
|
| 1122 | + public function ResolveRecipients($resolveRecipients) { |
|
| 1123 | + if ($resolveRecipients instanceof SyncResolveRecipients) { |
|
| 1124 | + $resolveRecipients->status = SYNC_RESOLVERECIPSSTATUS_SUCCESS; |
|
| 1125 | + $resolveRecipients->response = []; |
|
| 1126 | + $resolveRecipientsOptions = new SyncResolveRecipientsOptions(); |
|
| 1127 | + $maxAmbiguousRecipients = self::MAXAMBIGUOUSRECIPIENTS; |
|
| 1128 | + |
|
| 1129 | + if (isset($resolveRecipients->options)) { |
|
| 1130 | + $resolveRecipientsOptions = $resolveRecipients->options; |
|
| 1131 | + // only limit ambiguous recipients if the client requests it. |
|
| 1132 | + |
|
| 1133 | + if (isset($resolveRecipientsOptions->maxambiguousrecipients) && |
|
| 1134 | + $resolveRecipientsOptions->maxambiguousrecipients >= 0 && |
|
| 1135 | + $resolveRecipientsOptions->maxambiguousrecipients <= self::MAXAMBIGUOUSRECIPIENTS) { |
|
| 1136 | + $maxAmbiguousRecipients = $resolveRecipientsOptions->maxambiguousrecipients; |
|
| 1137 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->ResolveRecipients(): The client requested %d max ambiguous recipients to resolve.", $maxAmbiguousRecipients)); |
|
| 1138 | + } |
|
| 1139 | + } |
|
| 1140 | + |
|
| 1141 | + foreach ($resolveRecipients->to as $i => $to) { |
|
| 1142 | + $response = new SyncResolveRecipientsResponse(); |
|
| 1143 | + $response->to = $to; |
|
| 1144 | + $response->status = SYNC_RESOLVERECIPSSTATUS_SUCCESS; |
|
| 1145 | + |
|
| 1146 | + // do not expand distlists here |
|
| 1147 | + $recipient = $this->resolveRecipient($to, $maxAmbiguousRecipients, false); |
|
| 1148 | + if (is_array($recipient) && !empty($recipient)) { |
|
| 1149 | + $response->recipientcount = 0; |
|
| 1150 | + foreach ($recipient as $entry) { |
|
| 1151 | + if ($entry instanceof SyncResolveRecipient) { |
|
| 1152 | + // certificates are already set. Unset them if they weren't required. |
|
| 1153 | + if (!isset($resolveRecipientsOptions->certificateretrieval)) { |
|
| 1154 | + unset($entry->certificates); |
|
| 1155 | + } |
|
| 1156 | + if (isset($resolveRecipientsOptions->availability)) { |
|
| 1157 | + if (!isset($resolveRecipientsOptions->starttime)) { |
|
| 1158 | + // TODO error, the request must include a valid StartTime element value |
|
| 1159 | + } |
|
| 1160 | + $entry->availability = $this->getAvailability($to, $entry, $resolveRecipientsOptions); |
|
| 1161 | + } |
|
| 1162 | + if (isset($resolveRecipientsOptions->picture)) { |
|
| 1163 | + // TODO implement picture retrieval of the recipient |
|
| 1164 | + } |
|
| 1165 | + ++$response->recipientcount; |
|
| 1166 | + $response->recipient[] = $entry; |
|
| 1167 | + } |
|
| 1168 | + elseif (is_int($recipient)) { |
|
| 1169 | + $response->status = $recipient; |
|
| 1170 | + } |
|
| 1171 | + } |
|
| 1172 | + } |
|
| 1173 | + |
|
| 1174 | + $resolveRecipients->response[$i] = $response; |
|
| 1175 | + } |
|
| 1176 | + |
|
| 1177 | + return $resolveRecipients; |
|
| 1178 | + } |
|
| 1179 | + |
|
| 1180 | + SLog::Write(LOGLEVEL_WARN, "Grommunio->ResolveRecipients(): Not a valid SyncResolveRecipients object."); |
|
| 1181 | + // return a SyncResolveRecipients object so that sync doesn't fail |
|
| 1182 | + $r = new SyncResolveRecipients(); |
|
| 1183 | + $r->status = SYNC_RESOLVERECIPSSTATUS_PROTOCOLERROR; |
|
| 1184 | + |
|
| 1185 | + return $r; |
|
| 1186 | + } |
|
| 1187 | + |
|
| 1188 | + /*---------------------------------------------------------------------------------------------------------- |
|
| 1189 | 1189 | * Implementation of the ISearchProvider interface |
| 1190 | 1190 | */ |
| 1191 | 1191 | |
| 1192 | - /** |
|
| 1193 | - * Indicates if a search type is supported by this SearchProvider |
|
| 1194 | - * Currently only the type ISearchProvider::SEARCH_GAL (Global Address List) is implemented. |
|
| 1195 | - * |
|
| 1196 | - * @param string $searchtype |
|
| 1197 | - * |
|
| 1198 | - * @return bool |
|
| 1199 | - */ |
|
| 1200 | - public function SupportsType($searchtype) { |
|
| 1201 | - return ($searchtype == ISearchProvider::SEARCH_GAL) || ($searchtype == ISearchProvider::SEARCH_MAILBOX); |
|
| 1202 | - } |
|
| 1203 | - |
|
| 1204 | - /** |
|
| 1205 | - * Searches the GAB of Grommunio |
|
| 1206 | - * Can be overwritten globally by configuring a SearchBackend. |
|
| 1207 | - * |
|
| 1208 | - * @param string $searchquery string to be searched for |
|
| 1209 | - * @param string $searchrange specified searchrange |
|
| 1210 | - * @param SyncResolveRecipientsPicture $searchpicture limitations for picture |
|
| 1211 | - * |
|
| 1212 | - * @throws StatusException |
|
| 1213 | - * |
|
| 1214 | - * @return array search results |
|
| 1215 | - */ |
|
| 1216 | - public function GetGALSearchResults($searchquery, $searchrange, $searchpicture) { |
|
| 1217 | - // only return users whose displayName or the username starts with $name |
|
| 1218 | - // TODO: use PR_ANR for this restriction instead of PR_DISPLAY_NAME and PR_ACCOUNT |
|
| 1219 | - $addrbook = $this->getAddressbook(); |
|
| 1220 | - // FIXME: create a function to get the adressbook contentstable |
|
| 1221 | - if ($addrbook) { |
|
| 1222 | - $ab_entryid = mapi_ab_getdefaultdir($addrbook); |
|
| 1223 | - } |
|
| 1224 | - if ($ab_entryid) { |
|
| 1225 | - $ab_dir = mapi_ab_openentry($addrbook, $ab_entryid); |
|
| 1226 | - } |
|
| 1227 | - if ($ab_dir) { |
|
| 1228 | - $table = mapi_folder_getcontentstable($ab_dir); |
|
| 1229 | - } |
|
| 1230 | - |
|
| 1231 | - if (!$table) { |
|
| 1232 | - throw new StatusException(sprintf("Grommunio->GetGALSearchResults(): could not open addressbook: 0x%X", mapi_last_hresult()), SYNC_SEARCHSTATUS_STORE_CONNECTIONFAILED); |
|
| 1233 | - } |
|
| 1234 | - |
|
| 1235 | - $restriction = MAPIUtils::GetSearchRestriction(u2w($searchquery)); |
|
| 1236 | - mapi_table_restrict($table, $restriction); |
|
| 1237 | - mapi_table_sort($table, [PR_DISPLAY_NAME => TABLE_SORT_ASCEND]); |
|
| 1238 | - |
|
| 1239 | - if (mapi_last_hresult()) { |
|
| 1240 | - throw new StatusException(sprintf("Grommunio->GetGALSearchResults(): could not apply restriction: 0x%X", mapi_last_hresult()), SYNC_SEARCHSTATUS_STORE_TOOCOMPLEX); |
|
| 1241 | - } |
|
| 1242 | - |
|
| 1243 | - // range for the search results, default symbian range end is 50, wm 99, |
|
| 1244 | - // so we'll use that of nokia |
|
| 1245 | - $rangestart = 0; |
|
| 1246 | - $rangeend = 50; |
|
| 1247 | - |
|
| 1248 | - if ($searchrange != '0') { |
|
| 1249 | - $pos = strpos($searchrange, '-'); |
|
| 1250 | - $rangestart = substr($searchrange, 0, $pos); |
|
| 1251 | - $rangeend = substr($searchrange, ($pos + 1)); |
|
| 1252 | - } |
|
| 1253 | - $items = []; |
|
| 1254 | - |
|
| 1255 | - $querycnt = mapi_table_getrowcount($table); |
|
| 1256 | - // do not return more results as requested in range |
|
| 1257 | - $querylimit = (($rangeend + 1) < $querycnt) ? ($rangeend + 1) : $querycnt; |
|
| 1258 | - |
|
| 1259 | - if ($querycnt > 0) { |
|
| 1260 | - $abentries = mapi_table_queryrows($table, [PR_ENTRYID, PR_ACCOUNT, PR_DISPLAY_NAME, PR_SMTP_ADDRESS, PR_BUSINESS_TELEPHONE_NUMBER, PR_GIVEN_NAME, PR_SURNAME, PR_MOBILE_TELEPHONE_NUMBER, PR_HOME_TELEPHONE_NUMBER, PR_TITLE, PR_COMPANY_NAME, PR_OFFICE_LOCATION, PR_EMS_AB_THUMBNAIL_PHOTO], $rangestart, $querylimit); |
|
| 1261 | - } |
|
| 1262 | - |
|
| 1263 | - for ($i = 0; $i < $querylimit; ++$i) { |
|
| 1264 | - if (!isset($abentries[$i][PR_SMTP_ADDRESS])) { |
|
| 1265 | - SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->GetGALSearchResults(): The GAL entry '%s' does not have an email address and will be ignored.", w2u($abentries[$i][PR_DISPLAY_NAME]))); |
|
| 1266 | - |
|
| 1267 | - continue; |
|
| 1268 | - } |
|
| 1269 | - |
|
| 1270 | - $items[$i][SYNC_GAL_DISPLAYNAME] = w2u($abentries[$i][PR_DISPLAY_NAME]); |
|
| 1271 | - |
|
| 1272 | - if (strlen(trim($items[$i][SYNC_GAL_DISPLAYNAME])) == 0) { |
|
| 1273 | - $items[$i][SYNC_GAL_DISPLAYNAME] = w2u($abentries[$i][PR_ACCOUNT]); |
|
| 1274 | - } |
|
| 1275 | - |
|
| 1276 | - $items[$i][SYNC_GAL_ALIAS] = w2u($abentries[$i][PR_ACCOUNT]); |
|
| 1277 | - // it's not possible not get first and last name of an user |
|
| 1278 | - // from the gab and user functions, so we just set lastname |
|
| 1279 | - // to displayname and leave firstname unset |
|
| 1280 | - // this was changed in Zarafa 6.40, so we try to get first and |
|
| 1281 | - // last name and fall back to the old behaviour if these values are not set |
|
| 1282 | - if (isset($abentries[$i][PR_GIVEN_NAME])) { |
|
| 1283 | - $items[$i][SYNC_GAL_FIRSTNAME] = w2u($abentries[$i][PR_GIVEN_NAME]); |
|
| 1284 | - } |
|
| 1285 | - if (isset($abentries[$i][PR_SURNAME])) { |
|
| 1286 | - $items[$i][SYNC_GAL_LASTNAME] = w2u($abentries[$i][PR_SURNAME]); |
|
| 1287 | - } |
|
| 1288 | - |
|
| 1289 | - if (!isset($items[$i][SYNC_GAL_LASTNAME])) { |
|
| 1290 | - $items[$i][SYNC_GAL_LASTNAME] = $items[$i][SYNC_GAL_DISPLAYNAME]; |
|
| 1291 | - } |
|
| 1292 | - |
|
| 1293 | - $items[$i][SYNC_GAL_EMAILADDRESS] = w2u($abentries[$i][PR_SMTP_ADDRESS]); |
|
| 1294 | - // check if an user has an office number or it might produce warnings in the log |
|
| 1295 | - if (isset($abentries[$i][PR_BUSINESS_TELEPHONE_NUMBER])) { |
|
| 1296 | - $items[$i][SYNC_GAL_PHONE] = w2u($abentries[$i][PR_BUSINESS_TELEPHONE_NUMBER]); |
|
| 1297 | - } |
|
| 1298 | - // check if an user has a mobile number or it might produce warnings in the log |
|
| 1299 | - if (isset($abentries[$i][PR_MOBILE_TELEPHONE_NUMBER])) { |
|
| 1300 | - $items[$i][SYNC_GAL_MOBILEPHONE] = w2u($abentries[$i][PR_MOBILE_TELEPHONE_NUMBER]); |
|
| 1301 | - } |
|
| 1302 | - // check if an user has a home number or it might produce warnings in the log |
|
| 1303 | - if (isset($abentries[$i][PR_HOME_TELEPHONE_NUMBER])) { |
|
| 1304 | - $items[$i][SYNC_GAL_HOMEPHONE] = w2u($abentries[$i][PR_HOME_TELEPHONE_NUMBER]); |
|
| 1305 | - } |
|
| 1306 | - |
|
| 1307 | - if (isset($abentries[$i][PR_COMPANY_NAME])) { |
|
| 1308 | - $items[$i][SYNC_GAL_COMPANY] = w2u($abentries[$i][PR_COMPANY_NAME]); |
|
| 1309 | - } |
|
| 1310 | - |
|
| 1311 | - if (isset($abentries[$i][PR_TITLE])) { |
|
| 1312 | - $items[$i][SYNC_GAL_TITLE] = w2u($abentries[$i][PR_TITLE]); |
|
| 1313 | - } |
|
| 1314 | - |
|
| 1315 | - if (isset($abentries[$i][PR_OFFICE_LOCATION])) { |
|
| 1316 | - $items[$i][SYNC_GAL_OFFICE] = w2u($abentries[$i][PR_OFFICE_LOCATION]); |
|
| 1317 | - } |
|
| 1318 | - |
|
| 1319 | - if ($searchpicture !== false && isset($abentries[$i][PR_EMS_AB_THUMBNAIL_PHOTO])) { |
|
| 1320 | - $items[$i][SYNC_GAL_PICTURE] = StringStreamWrapper::Open($abentries[$i][PR_EMS_AB_THUMBNAIL_PHOTO]); |
|
| 1321 | - } |
|
| 1322 | - } |
|
| 1323 | - $nrResults = count($items); |
|
| 1324 | - $items['range'] = ($nrResults > 0) ? $rangestart . '-' . ($nrResults - 1) : '0-0'; |
|
| 1325 | - $items['searchtotal'] = $nrResults; |
|
| 1326 | - |
|
| 1327 | - return $items; |
|
| 1328 | - } |
|
| 1329 | - |
|
| 1330 | - /** |
|
| 1331 | - * Searches for the emails on the server. |
|
| 1332 | - * |
|
| 1333 | - * @param ContentParameter $cpo |
|
| 1334 | - * |
|
| 1335 | - * @return array |
|
| 1336 | - */ |
|
| 1337 | - public function GetMailboxSearchResults($cpo) { |
|
| 1338 | - $searchFolder = $this->getSearchFolder(); |
|
| 1339 | - $searchRestriction = $this->getSearchRestriction($cpo); |
|
| 1340 | - $searchRange = explode('-', $cpo->GetSearchRange()); |
|
| 1341 | - $searchFolderId = $cpo->GetSearchFolderid(); |
|
| 1342 | - $searchFolders = []; |
|
| 1343 | - // search only in required folders |
|
| 1344 | - if (!empty($searchFolderId)) { |
|
| 1345 | - $searchFolderEntryId = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($searchFolderId)); |
|
| 1346 | - $searchFolders[] = $searchFolderEntryId; |
|
| 1347 | - } |
|
| 1348 | - // if no folder was required then search in the entire store |
|
| 1349 | - else { |
|
| 1350 | - $tmp = mapi_getprops($this->store, [PR_ENTRYID, PR_DISPLAY_NAME, PR_IPM_SUBTREE_ENTRYID]); |
|
| 1351 | - $searchFolders[] = $tmp[PR_IPM_SUBTREE_ENTRYID]; |
|
| 1352 | - } |
|
| 1353 | - $items = []; |
|
| 1354 | - $flags = 0; |
|
| 1355 | - // if subfolders are required, do a recursive search |
|
| 1356 | - if ($cpo->GetSearchDeepTraversal()) { |
|
| 1357 | - $flags |= SEARCH_RECURSIVE; |
|
| 1358 | - } |
|
| 1359 | - |
|
| 1360 | - mapi_folder_setsearchcriteria($searchFolder, $searchRestriction, $searchFolders, $flags); |
|
| 1361 | - |
|
| 1362 | - $table = mapi_folder_getcontentstable($searchFolder); |
|
| 1363 | - $searchStart = time(); |
|
| 1364 | - // do the search and wait for all the results available |
|
| 1365 | - while (time() - $searchStart < SEARCH_WAIT) { |
|
| 1366 | - $searchcriteria = mapi_folder_getsearchcriteria($searchFolder); |
|
| 1367 | - if (($searchcriteria["searchstate"] & SEARCH_REBUILD) == 0) { |
|
| 1368 | - break; |
|
| 1369 | - } // Search is done |
|
| 1370 | - sleep(1); |
|
| 1371 | - } |
|
| 1372 | - |
|
| 1373 | - // if the search range is set limit the result to it, otherwise return all found messages |
|
| 1374 | - $rows = (is_array($searchRange) && isset($searchRange[0], $searchRange[1])) ? |
|
| 1375 | - mapi_table_queryrows($table, [PR_ENTRYID], $searchRange[0], $searchRange[1] - $searchRange[0] + 1) : |
|
| 1376 | - mapi_table_queryrows($table, [PR_ENTRYID], 0, SEARCH_MAXRESULTS); |
|
| 1377 | - |
|
| 1378 | - $cnt = count($rows); |
|
| 1379 | - $items['searchtotal'] = $cnt; |
|
| 1380 | - $items["range"] = $cpo->GetSearchRange(); |
|
| 1381 | - for ($i = 0; $i < $cnt; ++$i) { |
|
| 1382 | - $items[$i]['class'] = 'Email'; |
|
| 1383 | - $items[$i]['longid'] = bin2hex($rows[$i][PR_ENTRYID]); |
|
| 1384 | - // $items[$i]['folderid'] = bin2hex($rows[$i][PR_PARENT_SOURCE_KEY]); |
|
| 1385 | - } |
|
| 1386 | - |
|
| 1387 | - return $items; |
|
| 1388 | - } |
|
| 1389 | - |
|
| 1390 | - /** |
|
| 1391 | - * Terminates a search for a given PID. |
|
| 1392 | - * |
|
| 1393 | - * @param int $pid |
|
| 1394 | - * |
|
| 1395 | - * @return bool |
|
| 1396 | - */ |
|
| 1397 | - public function TerminateSearch($pid) { |
|
| 1398 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->TerminateSearch(): terminating search for pid %d", $pid)); |
|
| 1399 | - if (!isset($this->store) || $this->store === false) { |
|
| 1400 | - SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->TerminateSearch(): The store is not available. It is not possible to remove search folder with pid %d", $pid)); |
|
| 1401 | - |
|
| 1402 | - return false; |
|
| 1403 | - } |
|
| 1404 | - |
|
| 1405 | - $storeProps = mapi_getprops($this->store, [PR_STORE_SUPPORT_MASK, PR_FINDER_ENTRYID]); |
|
| 1406 | - if (($storeProps[PR_STORE_SUPPORT_MASK] & STORE_SEARCH_OK) != STORE_SEARCH_OK) { |
|
| 1407 | - SLog::Write(LOGLEVEL_WARN, "Grommunio->TerminateSearch(): Store doesn't support search folders. Public store doesn't have FINDER_ROOT folder"); |
|
| 1408 | - |
|
| 1409 | - return false; |
|
| 1410 | - } |
|
| 1411 | - |
|
| 1412 | - $finderfolder = mapi_msgstore_openentry($this->store, $storeProps[PR_FINDER_ENTRYID]); |
|
| 1413 | - if (mapi_last_hresult() != NOERROR) { |
|
| 1414 | - SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->TerminateSearch(): Unable to open search folder (0x%X)", mapi_last_hresult())); |
|
| 1415 | - |
|
| 1416 | - return false; |
|
| 1417 | - } |
|
| 1418 | - |
|
| 1419 | - $hierarchytable = mapi_folder_gethierarchytable($finderfolder); |
|
| 1420 | - mapi_table_restrict( |
|
| 1421 | - $hierarchytable, |
|
| 1422 | - [RES_CONTENT, |
|
| 1423 | - [ |
|
| 1424 | - FUZZYLEVEL => FL_PREFIX, |
|
| 1425 | - ULPROPTAG => PR_DISPLAY_NAME, |
|
| 1426 | - VALUE => [PR_DISPLAY_NAME => "grommunio-sync Search Folder " . $pid], |
|
| 1427 | - ], |
|
| 1428 | - ], |
|
| 1429 | - TBL_BATCH |
|
| 1430 | - ); |
|
| 1431 | - |
|
| 1432 | - $folders = mapi_table_queryallrows($hierarchytable, [PR_ENTRYID, PR_DISPLAY_NAME, PR_LAST_MODIFICATION_TIME]); |
|
| 1433 | - foreach ($folders as $folder) { |
|
| 1434 | - mapi_folder_deletefolder($finderfolder, $folder[PR_ENTRYID]); |
|
| 1435 | - } |
|
| 1436 | - |
|
| 1437 | - return true; |
|
| 1438 | - } |
|
| 1439 | - |
|
| 1440 | - /** |
|
| 1441 | - * Disconnects from the current search provider. |
|
| 1442 | - * |
|
| 1443 | - * @return bool |
|
| 1444 | - */ |
|
| 1445 | - public function Disconnect() { |
|
| 1446 | - return true; |
|
| 1447 | - } |
|
| 1448 | - |
|
| 1449 | - /** |
|
| 1450 | - * Returns the MAPI store resource for a folderid |
|
| 1451 | - * This is not part of IBackend but necessary for the ImportChangesICS->MoveMessage() operation if |
|
| 1452 | - * the destination folder is not in the default store |
|
| 1453 | - * Note: The current backend store might be changed as IBackend->Setup() is executed. |
|
| 1454 | - * |
|
| 1455 | - * @param string $store target store, could contain a "domain\user" value - if empty default store is returned |
|
| 1456 | - * @param string $folderid |
|
| 1457 | - * |
|
| 1458 | - * @return Resource/boolean |
|
| 1459 | - */ |
|
| 1460 | - public function GetMAPIStoreForFolderId($store, $folderid) { |
|
| 1461 | - if ($store == false) { |
|
| 1462 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetMAPIStoreForFolderId('%s', '%s'): no store specified, returning default store", $store, $folderid)); |
|
| 1463 | - |
|
| 1464 | - return $this->defaultstore; |
|
| 1465 | - } |
|
| 1466 | - |
|
| 1467 | - // setup the correct store |
|
| 1468 | - if ($this->Setup($store, false, $folderid)) { |
|
| 1469 | - return $this->store; |
|
| 1470 | - } |
|
| 1471 | - |
|
| 1472 | - SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->GetMAPIStoreForFolderId('%s', '%s'): store is not available", $store, $folderid)); |
|
| 1473 | - |
|
| 1474 | - return false; |
|
| 1475 | - } |
|
| 1476 | - |
|
| 1477 | - /** |
|
| 1478 | - * Returns the email address and the display name of the user. Used by autodiscover. |
|
| 1479 | - * |
|
| 1480 | - * @param string $username The username |
|
| 1481 | - * |
|
| 1482 | - * @return array |
|
| 1483 | - */ |
|
| 1484 | - public function GetUserDetails($username) { |
|
| 1485 | - SLog::Write(LOGLEVEL_WBXML, sprintf("Grommunio->GetUserDetails for '%s'.", $username)); |
|
| 1486 | - $zarafauserinfo = @nsp_getuserinfo($username); |
|
| 1487 | - $userDetails['emailaddress'] = (isset($zarafauserinfo['primary_email']) && $zarafauserinfo['primary_email']) ? $zarafauserinfo['primary_email'] : false; |
|
| 1488 | - $userDetails['fullname'] = (isset($zarafauserinfo['fullname']) && $zarafauserinfo['fullname']) ? $zarafauserinfo['fullname'] : false; |
|
| 1489 | - |
|
| 1490 | - return $userDetails; |
|
| 1491 | - } |
|
| 1492 | - |
|
| 1493 | - /** |
|
| 1494 | - * Returns the username of the currently active user. |
|
| 1495 | - * |
|
| 1496 | - * @return string |
|
| 1497 | - */ |
|
| 1498 | - public function GetCurrentUsername() { |
|
| 1499 | - return $this->storeName; |
|
| 1500 | - } |
|
| 1501 | - |
|
| 1502 | - /** |
|
| 1503 | - * Returns the impersonated user name. |
|
| 1504 | - * |
|
| 1505 | - * @return string or false if no user is impersonated |
|
| 1506 | - */ |
|
| 1507 | - public function GetImpersonatedUser() { |
|
| 1508 | - return $this->impersonateUser; |
|
| 1509 | - } |
|
| 1510 | - |
|
| 1511 | - /** |
|
| 1512 | - * Returns the authenticated user name. |
|
| 1513 | - * |
|
| 1514 | - * @return string |
|
| 1515 | - */ |
|
| 1516 | - public function GetMainUser() { |
|
| 1517 | - return $this->mainUser; |
|
| 1518 | - } |
|
| 1519 | - |
|
| 1520 | - /** |
|
| 1521 | - * Indicates if the Backend supports folder statistics. |
|
| 1522 | - * |
|
| 1523 | - * @return bool |
|
| 1524 | - */ |
|
| 1525 | - public function HasFolderStats() { |
|
| 1526 | - return true; |
|
| 1527 | - } |
|
| 1528 | - |
|
| 1529 | - /** |
|
| 1530 | - * Returns a status indication of the folder. |
|
| 1531 | - * If there are changes in the folder, the returned value must change. |
|
| 1532 | - * The returned values are compared with '===' to determine if a folder needs synchronization or not. |
|
| 1533 | - * |
|
| 1534 | - * @param string $store the store where the folder resides |
|
| 1535 | - * @param string $folderid the folder id |
|
| 1536 | - * |
|
| 1537 | - * @return string |
|
| 1538 | - */ |
|
| 1539 | - public function GetFolderStat($store, $folderid) { |
|
| 1540 | - list($user, $domain) = Utils::SplitDomainUser($store); |
|
| 1541 | - if ($user === false) { |
|
| 1542 | - $user = $this->mainUser; |
|
| 1543 | - if ($this->impersonateUser) { |
|
| 1544 | - $user = $this->impersonateUser; |
|
| 1545 | - } |
|
| 1546 | - } |
|
| 1547 | - |
|
| 1548 | - if (!isset($this->folderStatCache[$user])) { |
|
| 1549 | - $this->folderStatCache[$user] = []; |
|
| 1550 | - } |
|
| 1551 | - |
|
| 1552 | - // if there is nothing in the cache for a store, load the data for all folders of it |
|
| 1553 | - if (empty($this->folderStatCache[$user])) { |
|
| 1554 | - // get the store |
|
| 1555 | - $userstore = $this->openMessageStore($user); |
|
| 1556 | - $rootfolder = mapi_msgstore_openentry($userstore); |
|
| 1557 | - $hierarchy = mapi_folder_gethierarchytable($rootfolder, CONVENIENT_DEPTH); |
|
| 1558 | - $rows = mapi_table_queryallrows($hierarchy, [PR_SOURCE_KEY, PR_LOCAL_COMMIT_TIME_MAX, PR_CONTENT_COUNT, PR_CONTENT_UNREAD, PR_DELETED_MSG_COUNT]); |
|
| 1559 | - |
|
| 1560 | - if (count($rows) == 0) { |
|
| 1561 | - SLog::Write(LOGLEVEL_INFO, sprintf("Grommunio->GetFolderStat(): could not access folder statistics for user '%s'. Probably missing 'read' permissions on the root folder! Folders of this store will be synchronized ONCE per hour only!", $user)); |
|
| 1562 | - } |
|
| 1563 | - |
|
| 1564 | - foreach ($rows as $folder) { |
|
| 1565 | - $commit_time = isset($folder[PR_LOCAL_COMMIT_TIME_MAX]) ? $folder[PR_LOCAL_COMMIT_TIME_MAX] : "0000000000"; |
|
| 1566 | - $content_count = isset($folder[PR_CONTENT_COUNT]) ? $folder[PR_CONTENT_COUNT] : -1; |
|
| 1567 | - $content_unread = isset($folder[PR_CONTENT_UNREAD]) ? $folder[PR_CONTENT_UNREAD] : -1; |
|
| 1568 | - $content_deleted = isset($folder[PR_DELETED_MSG_COUNT]) ? $folder[PR_DELETED_MSG_COUNT] : -1; |
|
| 1569 | - |
|
| 1570 | - $this->folderStatCache[$user][bin2hex($folder[PR_SOURCE_KEY])] = $commit_time . "/" . $content_count . "/" . $content_unread . "/" . $content_deleted; |
|
| 1571 | - } |
|
| 1572 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetFolderStat() fetched status information of %d folders for store '%s'", count($this->folderStatCache[$user]), $user)); |
|
| 1573 | - } |
|
| 1574 | - |
|
| 1575 | - if (isset($this->folderStatCache[$user][$folderid])) { |
|
| 1576 | - return $this->folderStatCache[$user][$folderid]; |
|
| 1577 | - } |
|
| 1578 | - |
|
| 1579 | - // a timestamp that changes once per hour is returned in case there is no data found for this folder. It will be synchronized only once per hour. |
|
| 1580 | - return gmdate("Y-m-d-H"); |
|
| 1581 | - } |
|
| 1582 | - |
|
| 1583 | - /** |
|
| 1584 | - * Returns information about the user's store: |
|
| 1585 | - * number of folders, store size, full name, email address. |
|
| 1586 | - * |
|
| 1587 | - * @return UserStoreInfo |
|
| 1588 | - */ |
|
| 1589 | - public function GetUserStoreInfo() { |
|
| 1590 | - $userStoreInfo = new UserStoreInfo(); |
|
| 1591 | - |
|
| 1592 | - $rootfolder = mapi_msgstore_openentry($this->store); |
|
| 1593 | - $hierarchy = mapi_folder_gethierarchytable($rootfolder, CONVENIENT_DEPTH); |
|
| 1594 | - // Do not take hidden and system folders into account |
|
| 1595 | - // TODO make this restriction generic and use for hierarchy? |
|
| 1596 | - $restrict = [ |
|
| 1597 | - RES_AND, |
|
| 1598 | - [ |
|
| 1599 | - [ |
|
| 1600 | - RES_PROPERTY, |
|
| 1601 | - [ |
|
| 1602 | - RELOP => RELOP_NE, |
|
| 1603 | - ULPROPTAG => PR_ATTR_HIDDEN, |
|
| 1604 | - VALUE => true, ], |
|
| 1605 | - ], |
|
| 1606 | - [ |
|
| 1607 | - RES_PROPERTY, |
|
| 1608 | - [ |
|
| 1609 | - RELOP => RELOP_EQ, |
|
| 1610 | - ULPROPTAG => PR_FOLDER_TYPE, |
|
| 1611 | - VALUE => FOLDER_GENERIC, ], |
|
| 1612 | - ], |
|
| 1613 | - [ |
|
| 1614 | - RES_EXIST, |
|
| 1615 | - [ULPROPTAG => PR_CONTAINER_CLASS], |
|
| 1616 | - ], |
|
| 1617 | - ], ]; |
|
| 1618 | - mapi_table_restrict($hierarchy, $restrict); |
|
| 1619 | - $foldercount = mapi_table_getrowcount($hierarchy); |
|
| 1620 | - |
|
| 1621 | - $storeProps = mapi_getprops($this->store, [PR_MESSAGE_SIZE_EXTENDED]); |
|
| 1622 | - $storesize = isset($storeProps[PR_MESSAGE_SIZE_EXTENDED]) ? $storeProps[PR_MESSAGE_SIZE_EXTENDED] : 0; |
|
| 1623 | - |
|
| 1624 | - $userDetails = $this->GetUserDetails($this->impersonateUser ?: $this->mainUser); |
|
| 1625 | - $userStoreInfo->SetData($foldercount, $storesize, $userDetails['fullname'], $userDetails['emailaddress']); |
|
| 1626 | - SLog::Write(LOGLEVEL_DEBUG, sprintf( |
|
| 1627 | - "Grommunio->GetUserStoreInfo(): user %s (%s) store size is %d bytes and contains %d folders", |
|
| 1628 | - Utils::PrintAsString($userDetails['fullname']), |
|
| 1629 | - Utils::PrintAsString($userDetails['emailaddress']), |
|
| 1630 | - $storesize, |
|
| 1631 | - $foldercount |
|
| 1632 | - )); |
|
| 1633 | - |
|
| 1634 | - return $userStoreInfo; |
|
| 1635 | - } |
|
| 1636 | - |
|
| 1637 | - /*---------------------------------------------------------------------------------------------------------- |
|
| 1192 | + /** |
|
| 1193 | + * Indicates if a search type is supported by this SearchProvider |
|
| 1194 | + * Currently only the type ISearchProvider::SEARCH_GAL (Global Address List) is implemented. |
|
| 1195 | + * |
|
| 1196 | + * @param string $searchtype |
|
| 1197 | + * |
|
| 1198 | + * @return bool |
|
| 1199 | + */ |
|
| 1200 | + public function SupportsType($searchtype) { |
|
| 1201 | + return ($searchtype == ISearchProvider::SEARCH_GAL) || ($searchtype == ISearchProvider::SEARCH_MAILBOX); |
|
| 1202 | + } |
|
| 1203 | + |
|
| 1204 | + /** |
|
| 1205 | + * Searches the GAB of Grommunio |
|
| 1206 | + * Can be overwritten globally by configuring a SearchBackend. |
|
| 1207 | + * |
|
| 1208 | + * @param string $searchquery string to be searched for |
|
| 1209 | + * @param string $searchrange specified searchrange |
|
| 1210 | + * @param SyncResolveRecipientsPicture $searchpicture limitations for picture |
|
| 1211 | + * |
|
| 1212 | + * @throws StatusException |
|
| 1213 | + * |
|
| 1214 | + * @return array search results |
|
| 1215 | + */ |
|
| 1216 | + public function GetGALSearchResults($searchquery, $searchrange, $searchpicture) { |
|
| 1217 | + // only return users whose displayName or the username starts with $name |
|
| 1218 | + // TODO: use PR_ANR for this restriction instead of PR_DISPLAY_NAME and PR_ACCOUNT |
|
| 1219 | + $addrbook = $this->getAddressbook(); |
|
| 1220 | + // FIXME: create a function to get the adressbook contentstable |
|
| 1221 | + if ($addrbook) { |
|
| 1222 | + $ab_entryid = mapi_ab_getdefaultdir($addrbook); |
|
| 1223 | + } |
|
| 1224 | + if ($ab_entryid) { |
|
| 1225 | + $ab_dir = mapi_ab_openentry($addrbook, $ab_entryid); |
|
| 1226 | + } |
|
| 1227 | + if ($ab_dir) { |
|
| 1228 | + $table = mapi_folder_getcontentstable($ab_dir); |
|
| 1229 | + } |
|
| 1230 | + |
|
| 1231 | + if (!$table) { |
|
| 1232 | + throw new StatusException(sprintf("Grommunio->GetGALSearchResults(): could not open addressbook: 0x%X", mapi_last_hresult()), SYNC_SEARCHSTATUS_STORE_CONNECTIONFAILED); |
|
| 1233 | + } |
|
| 1234 | + |
|
| 1235 | + $restriction = MAPIUtils::GetSearchRestriction(u2w($searchquery)); |
|
| 1236 | + mapi_table_restrict($table, $restriction); |
|
| 1237 | + mapi_table_sort($table, [PR_DISPLAY_NAME => TABLE_SORT_ASCEND]); |
|
| 1238 | + |
|
| 1239 | + if (mapi_last_hresult()) { |
|
| 1240 | + throw new StatusException(sprintf("Grommunio->GetGALSearchResults(): could not apply restriction: 0x%X", mapi_last_hresult()), SYNC_SEARCHSTATUS_STORE_TOOCOMPLEX); |
|
| 1241 | + } |
|
| 1242 | + |
|
| 1243 | + // range for the search results, default symbian range end is 50, wm 99, |
|
| 1244 | + // so we'll use that of nokia |
|
| 1245 | + $rangestart = 0; |
|
| 1246 | + $rangeend = 50; |
|
| 1247 | + |
|
| 1248 | + if ($searchrange != '0') { |
|
| 1249 | + $pos = strpos($searchrange, '-'); |
|
| 1250 | + $rangestart = substr($searchrange, 0, $pos); |
|
| 1251 | + $rangeend = substr($searchrange, ($pos + 1)); |
|
| 1252 | + } |
|
| 1253 | + $items = []; |
|
| 1254 | + |
|
| 1255 | + $querycnt = mapi_table_getrowcount($table); |
|
| 1256 | + // do not return more results as requested in range |
|
| 1257 | + $querylimit = (($rangeend + 1) < $querycnt) ? ($rangeend + 1) : $querycnt; |
|
| 1258 | + |
|
| 1259 | + if ($querycnt > 0) { |
|
| 1260 | + $abentries = mapi_table_queryrows($table, [PR_ENTRYID, PR_ACCOUNT, PR_DISPLAY_NAME, PR_SMTP_ADDRESS, PR_BUSINESS_TELEPHONE_NUMBER, PR_GIVEN_NAME, PR_SURNAME, PR_MOBILE_TELEPHONE_NUMBER, PR_HOME_TELEPHONE_NUMBER, PR_TITLE, PR_COMPANY_NAME, PR_OFFICE_LOCATION, PR_EMS_AB_THUMBNAIL_PHOTO], $rangestart, $querylimit); |
|
| 1261 | + } |
|
| 1262 | + |
|
| 1263 | + for ($i = 0; $i < $querylimit; ++$i) { |
|
| 1264 | + if (!isset($abentries[$i][PR_SMTP_ADDRESS])) { |
|
| 1265 | + SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->GetGALSearchResults(): The GAL entry '%s' does not have an email address and will be ignored.", w2u($abentries[$i][PR_DISPLAY_NAME]))); |
|
| 1266 | + |
|
| 1267 | + continue; |
|
| 1268 | + } |
|
| 1269 | + |
|
| 1270 | + $items[$i][SYNC_GAL_DISPLAYNAME] = w2u($abentries[$i][PR_DISPLAY_NAME]); |
|
| 1271 | + |
|
| 1272 | + if (strlen(trim($items[$i][SYNC_GAL_DISPLAYNAME])) == 0) { |
|
| 1273 | + $items[$i][SYNC_GAL_DISPLAYNAME] = w2u($abentries[$i][PR_ACCOUNT]); |
|
| 1274 | + } |
|
| 1275 | + |
|
| 1276 | + $items[$i][SYNC_GAL_ALIAS] = w2u($abentries[$i][PR_ACCOUNT]); |
|
| 1277 | + // it's not possible not get first and last name of an user |
|
| 1278 | + // from the gab and user functions, so we just set lastname |
|
| 1279 | + // to displayname and leave firstname unset |
|
| 1280 | + // this was changed in Zarafa 6.40, so we try to get first and |
|
| 1281 | + // last name and fall back to the old behaviour if these values are not set |
|
| 1282 | + if (isset($abentries[$i][PR_GIVEN_NAME])) { |
|
| 1283 | + $items[$i][SYNC_GAL_FIRSTNAME] = w2u($abentries[$i][PR_GIVEN_NAME]); |
|
| 1284 | + } |
|
| 1285 | + if (isset($abentries[$i][PR_SURNAME])) { |
|
| 1286 | + $items[$i][SYNC_GAL_LASTNAME] = w2u($abentries[$i][PR_SURNAME]); |
|
| 1287 | + } |
|
| 1288 | + |
|
| 1289 | + if (!isset($items[$i][SYNC_GAL_LASTNAME])) { |
|
| 1290 | + $items[$i][SYNC_GAL_LASTNAME] = $items[$i][SYNC_GAL_DISPLAYNAME]; |
|
| 1291 | + } |
|
| 1292 | + |
|
| 1293 | + $items[$i][SYNC_GAL_EMAILADDRESS] = w2u($abentries[$i][PR_SMTP_ADDRESS]); |
|
| 1294 | + // check if an user has an office number or it might produce warnings in the log |
|
| 1295 | + if (isset($abentries[$i][PR_BUSINESS_TELEPHONE_NUMBER])) { |
|
| 1296 | + $items[$i][SYNC_GAL_PHONE] = w2u($abentries[$i][PR_BUSINESS_TELEPHONE_NUMBER]); |
|
| 1297 | + } |
|
| 1298 | + // check if an user has a mobile number or it might produce warnings in the log |
|
| 1299 | + if (isset($abentries[$i][PR_MOBILE_TELEPHONE_NUMBER])) { |
|
| 1300 | + $items[$i][SYNC_GAL_MOBILEPHONE] = w2u($abentries[$i][PR_MOBILE_TELEPHONE_NUMBER]); |
|
| 1301 | + } |
|
| 1302 | + // check if an user has a home number or it might produce warnings in the log |
|
| 1303 | + if (isset($abentries[$i][PR_HOME_TELEPHONE_NUMBER])) { |
|
| 1304 | + $items[$i][SYNC_GAL_HOMEPHONE] = w2u($abentries[$i][PR_HOME_TELEPHONE_NUMBER]); |
|
| 1305 | + } |
|
| 1306 | + |
|
| 1307 | + if (isset($abentries[$i][PR_COMPANY_NAME])) { |
|
| 1308 | + $items[$i][SYNC_GAL_COMPANY] = w2u($abentries[$i][PR_COMPANY_NAME]); |
|
| 1309 | + } |
|
| 1310 | + |
|
| 1311 | + if (isset($abentries[$i][PR_TITLE])) { |
|
| 1312 | + $items[$i][SYNC_GAL_TITLE] = w2u($abentries[$i][PR_TITLE]); |
|
| 1313 | + } |
|
| 1314 | + |
|
| 1315 | + if (isset($abentries[$i][PR_OFFICE_LOCATION])) { |
|
| 1316 | + $items[$i][SYNC_GAL_OFFICE] = w2u($abentries[$i][PR_OFFICE_LOCATION]); |
|
| 1317 | + } |
|
| 1318 | + |
|
| 1319 | + if ($searchpicture !== false && isset($abentries[$i][PR_EMS_AB_THUMBNAIL_PHOTO])) { |
|
| 1320 | + $items[$i][SYNC_GAL_PICTURE] = StringStreamWrapper::Open($abentries[$i][PR_EMS_AB_THUMBNAIL_PHOTO]); |
|
| 1321 | + } |
|
| 1322 | + } |
|
| 1323 | + $nrResults = count($items); |
|
| 1324 | + $items['range'] = ($nrResults > 0) ? $rangestart . '-' . ($nrResults - 1) : '0-0'; |
|
| 1325 | + $items['searchtotal'] = $nrResults; |
|
| 1326 | + |
|
| 1327 | + return $items; |
|
| 1328 | + } |
|
| 1329 | + |
|
| 1330 | + /** |
|
| 1331 | + * Searches for the emails on the server. |
|
| 1332 | + * |
|
| 1333 | + * @param ContentParameter $cpo |
|
| 1334 | + * |
|
| 1335 | + * @return array |
|
| 1336 | + */ |
|
| 1337 | + public function GetMailboxSearchResults($cpo) { |
|
| 1338 | + $searchFolder = $this->getSearchFolder(); |
|
| 1339 | + $searchRestriction = $this->getSearchRestriction($cpo); |
|
| 1340 | + $searchRange = explode('-', $cpo->GetSearchRange()); |
|
| 1341 | + $searchFolderId = $cpo->GetSearchFolderid(); |
|
| 1342 | + $searchFolders = []; |
|
| 1343 | + // search only in required folders |
|
| 1344 | + if (!empty($searchFolderId)) { |
|
| 1345 | + $searchFolderEntryId = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($searchFolderId)); |
|
| 1346 | + $searchFolders[] = $searchFolderEntryId; |
|
| 1347 | + } |
|
| 1348 | + // if no folder was required then search in the entire store |
|
| 1349 | + else { |
|
| 1350 | + $tmp = mapi_getprops($this->store, [PR_ENTRYID, PR_DISPLAY_NAME, PR_IPM_SUBTREE_ENTRYID]); |
|
| 1351 | + $searchFolders[] = $tmp[PR_IPM_SUBTREE_ENTRYID]; |
|
| 1352 | + } |
|
| 1353 | + $items = []; |
|
| 1354 | + $flags = 0; |
|
| 1355 | + // if subfolders are required, do a recursive search |
|
| 1356 | + if ($cpo->GetSearchDeepTraversal()) { |
|
| 1357 | + $flags |= SEARCH_RECURSIVE; |
|
| 1358 | + } |
|
| 1359 | + |
|
| 1360 | + mapi_folder_setsearchcriteria($searchFolder, $searchRestriction, $searchFolders, $flags); |
|
| 1361 | + |
|
| 1362 | + $table = mapi_folder_getcontentstable($searchFolder); |
|
| 1363 | + $searchStart = time(); |
|
| 1364 | + // do the search and wait for all the results available |
|
| 1365 | + while (time() - $searchStart < SEARCH_WAIT) { |
|
| 1366 | + $searchcriteria = mapi_folder_getsearchcriteria($searchFolder); |
|
| 1367 | + if (($searchcriteria["searchstate"] & SEARCH_REBUILD) == 0) { |
|
| 1368 | + break; |
|
| 1369 | + } // Search is done |
|
| 1370 | + sleep(1); |
|
| 1371 | + } |
|
| 1372 | + |
|
| 1373 | + // if the search range is set limit the result to it, otherwise return all found messages |
|
| 1374 | + $rows = (is_array($searchRange) && isset($searchRange[0], $searchRange[1])) ? |
|
| 1375 | + mapi_table_queryrows($table, [PR_ENTRYID], $searchRange[0], $searchRange[1] - $searchRange[0] + 1) : |
|
| 1376 | + mapi_table_queryrows($table, [PR_ENTRYID], 0, SEARCH_MAXRESULTS); |
|
| 1377 | + |
|
| 1378 | + $cnt = count($rows); |
|
| 1379 | + $items['searchtotal'] = $cnt; |
|
| 1380 | + $items["range"] = $cpo->GetSearchRange(); |
|
| 1381 | + for ($i = 0; $i < $cnt; ++$i) { |
|
| 1382 | + $items[$i]['class'] = 'Email'; |
|
| 1383 | + $items[$i]['longid'] = bin2hex($rows[$i][PR_ENTRYID]); |
|
| 1384 | + // $items[$i]['folderid'] = bin2hex($rows[$i][PR_PARENT_SOURCE_KEY]); |
|
| 1385 | + } |
|
| 1386 | + |
|
| 1387 | + return $items; |
|
| 1388 | + } |
|
| 1389 | + |
|
| 1390 | + /** |
|
| 1391 | + * Terminates a search for a given PID. |
|
| 1392 | + * |
|
| 1393 | + * @param int $pid |
|
| 1394 | + * |
|
| 1395 | + * @return bool |
|
| 1396 | + */ |
|
| 1397 | + public function TerminateSearch($pid) { |
|
| 1398 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->TerminateSearch(): terminating search for pid %d", $pid)); |
|
| 1399 | + if (!isset($this->store) || $this->store === false) { |
|
| 1400 | + SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->TerminateSearch(): The store is not available. It is not possible to remove search folder with pid %d", $pid)); |
|
| 1401 | + |
|
| 1402 | + return false; |
|
| 1403 | + } |
|
| 1404 | + |
|
| 1405 | + $storeProps = mapi_getprops($this->store, [PR_STORE_SUPPORT_MASK, PR_FINDER_ENTRYID]); |
|
| 1406 | + if (($storeProps[PR_STORE_SUPPORT_MASK] & STORE_SEARCH_OK) != STORE_SEARCH_OK) { |
|
| 1407 | + SLog::Write(LOGLEVEL_WARN, "Grommunio->TerminateSearch(): Store doesn't support search folders. Public store doesn't have FINDER_ROOT folder"); |
|
| 1408 | + |
|
| 1409 | + return false; |
|
| 1410 | + } |
|
| 1411 | + |
|
| 1412 | + $finderfolder = mapi_msgstore_openentry($this->store, $storeProps[PR_FINDER_ENTRYID]); |
|
| 1413 | + if (mapi_last_hresult() != NOERROR) { |
|
| 1414 | + SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->TerminateSearch(): Unable to open search folder (0x%X)", mapi_last_hresult())); |
|
| 1415 | + |
|
| 1416 | + return false; |
|
| 1417 | + } |
|
| 1418 | + |
|
| 1419 | + $hierarchytable = mapi_folder_gethierarchytable($finderfolder); |
|
| 1420 | + mapi_table_restrict( |
|
| 1421 | + $hierarchytable, |
|
| 1422 | + [RES_CONTENT, |
|
| 1423 | + [ |
|
| 1424 | + FUZZYLEVEL => FL_PREFIX, |
|
| 1425 | + ULPROPTAG => PR_DISPLAY_NAME, |
|
| 1426 | + VALUE => [PR_DISPLAY_NAME => "grommunio-sync Search Folder " . $pid], |
|
| 1427 | + ], |
|
| 1428 | + ], |
|
| 1429 | + TBL_BATCH |
|
| 1430 | + ); |
|
| 1431 | + |
|
| 1432 | + $folders = mapi_table_queryallrows($hierarchytable, [PR_ENTRYID, PR_DISPLAY_NAME, PR_LAST_MODIFICATION_TIME]); |
|
| 1433 | + foreach ($folders as $folder) { |
|
| 1434 | + mapi_folder_deletefolder($finderfolder, $folder[PR_ENTRYID]); |
|
| 1435 | + } |
|
| 1436 | + |
|
| 1437 | + return true; |
|
| 1438 | + } |
|
| 1439 | + |
|
| 1440 | + /** |
|
| 1441 | + * Disconnects from the current search provider. |
|
| 1442 | + * |
|
| 1443 | + * @return bool |
|
| 1444 | + */ |
|
| 1445 | + public function Disconnect() { |
|
| 1446 | + return true; |
|
| 1447 | + } |
|
| 1448 | + |
|
| 1449 | + /** |
|
| 1450 | + * Returns the MAPI store resource for a folderid |
|
| 1451 | + * This is not part of IBackend but necessary for the ImportChangesICS->MoveMessage() operation if |
|
| 1452 | + * the destination folder is not in the default store |
|
| 1453 | + * Note: The current backend store might be changed as IBackend->Setup() is executed. |
|
| 1454 | + * |
|
| 1455 | + * @param string $store target store, could contain a "domain\user" value - if empty default store is returned |
|
| 1456 | + * @param string $folderid |
|
| 1457 | + * |
|
| 1458 | + * @return Resource/boolean |
|
| 1459 | + */ |
|
| 1460 | + public function GetMAPIStoreForFolderId($store, $folderid) { |
|
| 1461 | + if ($store == false) { |
|
| 1462 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetMAPIStoreForFolderId('%s', '%s'): no store specified, returning default store", $store, $folderid)); |
|
| 1463 | + |
|
| 1464 | + return $this->defaultstore; |
|
| 1465 | + } |
|
| 1466 | + |
|
| 1467 | + // setup the correct store |
|
| 1468 | + if ($this->Setup($store, false, $folderid)) { |
|
| 1469 | + return $this->store; |
|
| 1470 | + } |
|
| 1471 | + |
|
| 1472 | + SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->GetMAPIStoreForFolderId('%s', '%s'): store is not available", $store, $folderid)); |
|
| 1473 | + |
|
| 1474 | + return false; |
|
| 1475 | + } |
|
| 1476 | + |
|
| 1477 | + /** |
|
| 1478 | + * Returns the email address and the display name of the user. Used by autodiscover. |
|
| 1479 | + * |
|
| 1480 | + * @param string $username The username |
|
| 1481 | + * |
|
| 1482 | + * @return array |
|
| 1483 | + */ |
|
| 1484 | + public function GetUserDetails($username) { |
|
| 1485 | + SLog::Write(LOGLEVEL_WBXML, sprintf("Grommunio->GetUserDetails for '%s'.", $username)); |
|
| 1486 | + $zarafauserinfo = @nsp_getuserinfo($username); |
|
| 1487 | + $userDetails['emailaddress'] = (isset($zarafauserinfo['primary_email']) && $zarafauserinfo['primary_email']) ? $zarafauserinfo['primary_email'] : false; |
|
| 1488 | + $userDetails['fullname'] = (isset($zarafauserinfo['fullname']) && $zarafauserinfo['fullname']) ? $zarafauserinfo['fullname'] : false; |
|
| 1489 | + |
|
| 1490 | + return $userDetails; |
|
| 1491 | + } |
|
| 1492 | + |
|
| 1493 | + /** |
|
| 1494 | + * Returns the username of the currently active user. |
|
| 1495 | + * |
|
| 1496 | + * @return string |
|
| 1497 | + */ |
|
| 1498 | + public function GetCurrentUsername() { |
|
| 1499 | + return $this->storeName; |
|
| 1500 | + } |
|
| 1501 | + |
|
| 1502 | + /** |
|
| 1503 | + * Returns the impersonated user name. |
|
| 1504 | + * |
|
| 1505 | + * @return string or false if no user is impersonated |
|
| 1506 | + */ |
|
| 1507 | + public function GetImpersonatedUser() { |
|
| 1508 | + return $this->impersonateUser; |
|
| 1509 | + } |
|
| 1510 | + |
|
| 1511 | + /** |
|
| 1512 | + * Returns the authenticated user name. |
|
| 1513 | + * |
|
| 1514 | + * @return string |
|
| 1515 | + */ |
|
| 1516 | + public function GetMainUser() { |
|
| 1517 | + return $this->mainUser; |
|
| 1518 | + } |
|
| 1519 | + |
|
| 1520 | + /** |
|
| 1521 | + * Indicates if the Backend supports folder statistics. |
|
| 1522 | + * |
|
| 1523 | + * @return bool |
|
| 1524 | + */ |
|
| 1525 | + public function HasFolderStats() { |
|
| 1526 | + return true; |
|
| 1527 | + } |
|
| 1528 | + |
|
| 1529 | + /** |
|
| 1530 | + * Returns a status indication of the folder. |
|
| 1531 | + * If there are changes in the folder, the returned value must change. |
|
| 1532 | + * The returned values are compared with '===' to determine if a folder needs synchronization or not. |
|
| 1533 | + * |
|
| 1534 | + * @param string $store the store where the folder resides |
|
| 1535 | + * @param string $folderid the folder id |
|
| 1536 | + * |
|
| 1537 | + * @return string |
|
| 1538 | + */ |
|
| 1539 | + public function GetFolderStat($store, $folderid) { |
|
| 1540 | + list($user, $domain) = Utils::SplitDomainUser($store); |
|
| 1541 | + if ($user === false) { |
|
| 1542 | + $user = $this->mainUser; |
|
| 1543 | + if ($this->impersonateUser) { |
|
| 1544 | + $user = $this->impersonateUser; |
|
| 1545 | + } |
|
| 1546 | + } |
|
| 1547 | + |
|
| 1548 | + if (!isset($this->folderStatCache[$user])) { |
|
| 1549 | + $this->folderStatCache[$user] = []; |
|
| 1550 | + } |
|
| 1551 | + |
|
| 1552 | + // if there is nothing in the cache for a store, load the data for all folders of it |
|
| 1553 | + if (empty($this->folderStatCache[$user])) { |
|
| 1554 | + // get the store |
|
| 1555 | + $userstore = $this->openMessageStore($user); |
|
| 1556 | + $rootfolder = mapi_msgstore_openentry($userstore); |
|
| 1557 | + $hierarchy = mapi_folder_gethierarchytable($rootfolder, CONVENIENT_DEPTH); |
|
| 1558 | + $rows = mapi_table_queryallrows($hierarchy, [PR_SOURCE_KEY, PR_LOCAL_COMMIT_TIME_MAX, PR_CONTENT_COUNT, PR_CONTENT_UNREAD, PR_DELETED_MSG_COUNT]); |
|
| 1559 | + |
|
| 1560 | + if (count($rows) == 0) { |
|
| 1561 | + SLog::Write(LOGLEVEL_INFO, sprintf("Grommunio->GetFolderStat(): could not access folder statistics for user '%s'. Probably missing 'read' permissions on the root folder! Folders of this store will be synchronized ONCE per hour only!", $user)); |
|
| 1562 | + } |
|
| 1563 | + |
|
| 1564 | + foreach ($rows as $folder) { |
|
| 1565 | + $commit_time = isset($folder[PR_LOCAL_COMMIT_TIME_MAX]) ? $folder[PR_LOCAL_COMMIT_TIME_MAX] : "0000000000"; |
|
| 1566 | + $content_count = isset($folder[PR_CONTENT_COUNT]) ? $folder[PR_CONTENT_COUNT] : -1; |
|
| 1567 | + $content_unread = isset($folder[PR_CONTENT_UNREAD]) ? $folder[PR_CONTENT_UNREAD] : -1; |
|
| 1568 | + $content_deleted = isset($folder[PR_DELETED_MSG_COUNT]) ? $folder[PR_DELETED_MSG_COUNT] : -1; |
|
| 1569 | + |
|
| 1570 | + $this->folderStatCache[$user][bin2hex($folder[PR_SOURCE_KEY])] = $commit_time . "/" . $content_count . "/" . $content_unread . "/" . $content_deleted; |
|
| 1571 | + } |
|
| 1572 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetFolderStat() fetched status information of %d folders for store '%s'", count($this->folderStatCache[$user]), $user)); |
|
| 1573 | + } |
|
| 1574 | + |
|
| 1575 | + if (isset($this->folderStatCache[$user][$folderid])) { |
|
| 1576 | + return $this->folderStatCache[$user][$folderid]; |
|
| 1577 | + } |
|
| 1578 | + |
|
| 1579 | + // a timestamp that changes once per hour is returned in case there is no data found for this folder. It will be synchronized only once per hour. |
|
| 1580 | + return gmdate("Y-m-d-H"); |
|
| 1581 | + } |
|
| 1582 | + |
|
| 1583 | + /** |
|
| 1584 | + * Returns information about the user's store: |
|
| 1585 | + * number of folders, store size, full name, email address. |
|
| 1586 | + * |
|
| 1587 | + * @return UserStoreInfo |
|
| 1588 | + */ |
|
| 1589 | + public function GetUserStoreInfo() { |
|
| 1590 | + $userStoreInfo = new UserStoreInfo(); |
|
| 1591 | + |
|
| 1592 | + $rootfolder = mapi_msgstore_openentry($this->store); |
|
| 1593 | + $hierarchy = mapi_folder_gethierarchytable($rootfolder, CONVENIENT_DEPTH); |
|
| 1594 | + // Do not take hidden and system folders into account |
|
| 1595 | + // TODO make this restriction generic and use for hierarchy? |
|
| 1596 | + $restrict = [ |
|
| 1597 | + RES_AND, |
|
| 1598 | + [ |
|
| 1599 | + [ |
|
| 1600 | + RES_PROPERTY, |
|
| 1601 | + [ |
|
| 1602 | + RELOP => RELOP_NE, |
|
| 1603 | + ULPROPTAG => PR_ATTR_HIDDEN, |
|
| 1604 | + VALUE => true, ], |
|
| 1605 | + ], |
|
| 1606 | + [ |
|
| 1607 | + RES_PROPERTY, |
|
| 1608 | + [ |
|
| 1609 | + RELOP => RELOP_EQ, |
|
| 1610 | + ULPROPTAG => PR_FOLDER_TYPE, |
|
| 1611 | + VALUE => FOLDER_GENERIC, ], |
|
| 1612 | + ], |
|
| 1613 | + [ |
|
| 1614 | + RES_EXIST, |
|
| 1615 | + [ULPROPTAG => PR_CONTAINER_CLASS], |
|
| 1616 | + ], |
|
| 1617 | + ], ]; |
|
| 1618 | + mapi_table_restrict($hierarchy, $restrict); |
|
| 1619 | + $foldercount = mapi_table_getrowcount($hierarchy); |
|
| 1620 | + |
|
| 1621 | + $storeProps = mapi_getprops($this->store, [PR_MESSAGE_SIZE_EXTENDED]); |
|
| 1622 | + $storesize = isset($storeProps[PR_MESSAGE_SIZE_EXTENDED]) ? $storeProps[PR_MESSAGE_SIZE_EXTENDED] : 0; |
|
| 1623 | + |
|
| 1624 | + $userDetails = $this->GetUserDetails($this->impersonateUser ?: $this->mainUser); |
|
| 1625 | + $userStoreInfo->SetData($foldercount, $storesize, $userDetails['fullname'], $userDetails['emailaddress']); |
|
| 1626 | + SLog::Write(LOGLEVEL_DEBUG, sprintf( |
|
| 1627 | + "Grommunio->GetUserStoreInfo(): user %s (%s) store size is %d bytes and contains %d folders", |
|
| 1628 | + Utils::PrintAsString($userDetails['fullname']), |
|
| 1629 | + Utils::PrintAsString($userDetails['emailaddress']), |
|
| 1630 | + $storesize, |
|
| 1631 | + $foldercount |
|
| 1632 | + )); |
|
| 1633 | + |
|
| 1634 | + return $userStoreInfo; |
|
| 1635 | + } |
|
| 1636 | + |
|
| 1637 | + /*---------------------------------------------------------------------------------------------------------- |
|
| 1638 | 1638 | * Implementation of the IStateMachine interface |
| 1639 | 1639 | */ |
| 1640 | 1640 | |
| 1641 | - /** |
|
| 1642 | - * Gets a hash value indicating the latest dataset of the named |
|
| 1643 | - * state with a specified key and counter. |
|
| 1644 | - * If the state is changed between two calls of this method |
|
| 1645 | - * the returned hash should be different. |
|
| 1646 | - * |
|
| 1647 | - * @param string $devid the device id |
|
| 1648 | - * @param string $type the state type |
|
| 1649 | - * @param string $key (opt) |
|
| 1650 | - * @param string $counter (opt) |
|
| 1651 | - * |
|
| 1652 | - * @throws StateNotFoundException |
|
| 1653 | - * |
|
| 1654 | - * @return string |
|
| 1655 | - */ |
|
| 1656 | - public function GetStateHash($devid, $type, $key = false, $counter = false) { |
|
| 1657 | - try { |
|
| 1658 | - $stateMessage = $this->getStateMessage($devid, $type, $key, $counter); |
|
| 1659 | - $stateMessageProps = mapi_getprops($stateMessage, [PR_LAST_MODIFICATION_TIME]); |
|
| 1660 | - if (isset($stateMessageProps[PR_LAST_MODIFICATION_TIME])) { |
|
| 1661 | - return $stateMessageProps[PR_LAST_MODIFICATION_TIME]; |
|
| 1662 | - } |
|
| 1663 | - } |
|
| 1664 | - catch (StateNotFoundException $e) { |
|
| 1665 | - } |
|
| 1666 | - |
|
| 1667 | - return "0"; |
|
| 1668 | - } |
|
| 1669 | - |
|
| 1670 | - /** |
|
| 1671 | - * Gets a state for a specified key and counter. |
|
| 1672 | - * This method should call IStateMachine->CleanStates() |
|
| 1673 | - * to remove older states (same key, previous counters). |
|
| 1674 | - * |
|
| 1675 | - * @param string $devid the device id |
|
| 1676 | - * @param string $type the state type |
|
| 1677 | - * @param string $key (opt) |
|
| 1678 | - * @param string $counter (opt) |
|
| 1679 | - * @param string $cleanstates (opt) |
|
| 1680 | - * |
|
| 1681 | - * @throws StateNotFoundException, StateInvalidException, UnavailableException |
|
| 1682 | - * |
|
| 1683 | - * @return mixed |
|
| 1684 | - */ |
|
| 1685 | - public function GetState($devid, $type, $key = false, $counter = false, $cleanstates = true) { |
|
| 1686 | - if ($counter && $cleanstates) { |
|
| 1687 | - $this->CleanStates($devid, $type, $key, $counter); |
|
| 1688 | - // also clean Failsave state for previous counter |
|
| 1689 | - if ($key == false) { |
|
| 1690 | - $this->CleanStates($devid, $type, IStateMachine::FAILSAVE, $counter); |
|
| 1691 | - } |
|
| 1692 | - } |
|
| 1693 | - $stateMessage = $this->getStateMessage($devid, $type, $key, $counter); |
|
| 1694 | - $state = base64_decode(MAPIUtils::readPropStream($stateMessage, PR_BODY)); |
|
| 1695 | - |
|
| 1696 | - if ($state && $state[0] === '{') { |
|
| 1697 | - $jsonDec = json_decode($state); |
|
| 1698 | - if (isset($jsonDec->gsSyncStateClass)) { |
|
| 1699 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetState(): top class '%s'", $jsonDec->gsSyncStateClass)); |
|
| 1700 | - $gsObj = new $jsonDec->gsSyncStateClass(); |
|
| 1701 | - $gsObj->jsonDeserialize($jsonDec); |
|
| 1702 | - $gsObj->postUnserialize(); |
|
| 1703 | - } |
|
| 1704 | - } |
|
| 1705 | - |
|
| 1706 | - return isset($gsObj) && is_object($gsObj) ? $gsObj : $state; |
|
| 1707 | - } |
|
| 1708 | - |
|
| 1709 | - /** |
|
| 1710 | - * Writes ta state to for a key and counter. |
|
| 1711 | - * |
|
| 1712 | - * @param mixed $state |
|
| 1713 | - * @param string $devid the device id |
|
| 1714 | - * @param string $type the state type |
|
| 1715 | - * @param string $key (opt) |
|
| 1716 | - * @param int $counter (opt) |
|
| 1717 | - * |
|
| 1718 | - * @throws StateInvalidException, UnavailableException |
|
| 1719 | - * |
|
| 1720 | - * @return bool |
|
| 1721 | - */ |
|
| 1722 | - public function SetState($state, $devid, $type, $key = false, $counter = false) { |
|
| 1723 | - return $this->setStateMessage($state, $devid, $type, $key, $counter); |
|
| 1724 | - } |
|
| 1725 | - |
|
| 1726 | - /** |
|
| 1727 | - * Cleans up all older states. |
|
| 1728 | - * If called with a $counter, all states previous state counter can be removed. |
|
| 1729 | - * If additionally the $thisCounterOnly flag is true, only that specific counter will be removed. |
|
| 1730 | - * If called without $counter, all keys (independently from the counter) can be removed. |
|
| 1731 | - * |
|
| 1732 | - * @param string $devid the device id |
|
| 1733 | - * @param string $type the state type |
|
| 1734 | - * @param string $key |
|
| 1735 | - * @param string $counter (opt) |
|
| 1736 | - * @param string $thisCounterOnly (opt) if provided, the exact counter only will be removed |
|
| 1737 | - * |
|
| 1738 | - * @throws StateInvalidException |
|
| 1739 | - * |
|
| 1740 | - * @return |
|
| 1741 | - */ |
|
| 1742 | - public function CleanStates($devid, $type, $key, $counter = false, $thisCounterOnly = false) { |
|
| 1743 | - if (!$this->stateFolder) { |
|
| 1744 | - $this->getStateFolder($devid); |
|
| 1745 | - if (!$this->stateFolder) { |
|
| 1746 | - throw new StateNotFoundException(sprintf( |
|
| 1747 | - "Grommunio->getStateMessage(): Could not locate the state folder for device '%s'", |
|
| 1748 | - $devid |
|
| 1749 | - )); |
|
| 1750 | - } |
|
| 1751 | - } |
|
| 1752 | - $messageName = rtrim((($key !== false) ? $key . "-" : "") . (($type !== "") ? $type : ""), "-"); |
|
| 1753 | - $restriction = $this->getStateMessageRestriction($messageName, $counter, $thisCounterOnly); |
|
| 1754 | - $stateFolderContents = mapi_folder_getcontentstable($this->stateFolder, MAPI_ASSOCIATED); |
|
| 1755 | - if ($stateFolderContents) { |
|
| 1756 | - mapi_table_restrict($stateFolderContents, $restriction); |
|
| 1757 | - $rowCnt = mapi_table_getrowcount($stateFolderContents); |
|
| 1758 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->CleanStates(): Found %d states to clean (%s)", $rowCnt, $messageName)); |
|
| 1759 | - if ($rowCnt > 0) { |
|
| 1760 | - $rows = mapi_table_queryallrows($stateFolderContents, [PR_ENTRYID]); |
|
| 1761 | - $entryids = []; |
|
| 1762 | - foreach ($rows as $row) { |
|
| 1763 | - $entryids[] = $row[PR_ENTRYID]; |
|
| 1764 | - } |
|
| 1765 | - mapi_folder_deletemessages($this->stateFolder, $entryids, DELETE_HARD_DELETE); |
|
| 1766 | - } |
|
| 1767 | - } |
|
| 1768 | - } |
|
| 1769 | - |
|
| 1770 | - /** |
|
| 1771 | - * Links a user to a device. |
|
| 1772 | - * |
|
| 1773 | - * @param string $username |
|
| 1774 | - * @param string $devid |
|
| 1775 | - * |
|
| 1776 | - * @return bool indicating if the user was added or not (existed already) |
|
| 1777 | - */ |
|
| 1778 | - public function LinkUserDevice($username, $devid) { |
|
| 1779 | - $device = [$devid => time()]; |
|
| 1780 | - $this->setDeviceUserData($this->type, $device, $username, -1, $subkey = -1, $doCas = "merge"); |
|
| 1781 | - |
|
| 1782 | - return false; |
|
| 1783 | - } |
|
| 1784 | - |
|
| 1785 | - /** |
|
| 1786 | - * Unlinks a device from a user. |
|
| 1787 | - * |
|
| 1788 | - * @param string $username |
|
| 1789 | - * @param string $devid |
|
| 1790 | - * |
|
| 1791 | - * @return bool |
|
| 1792 | - */ |
|
| 1793 | - public function UnLinkUserDevice($username, $devid) { |
|
| 1794 | - // TODO: Implement |
|
| 1795 | - return false; |
|
| 1796 | - } |
|
| 1797 | - |
|
| 1798 | - /** |
|
| 1799 | - * Returns the current version of the state files |
|
| 1800 | - * grommunio: This is not relevant atm. IStateMachine::STATEVERSION_02 will match GSync::GetLatestStateVersion(). |
|
| 1801 | - * If it might be required to update states in the future, this could be implemented on a store level, |
|
| 1802 | - * where states are then migrated "on-the-fly" |
|
| 1803 | - * or |
|
| 1804 | - * in a global settings where all states in all stores are migrated once. |
|
| 1805 | - * |
|
| 1806 | - * @return int |
|
| 1807 | - */ |
|
| 1808 | - public function GetStateVersion() { |
|
| 1809 | - return IStateMachine::STATEVERSION_02; |
|
| 1810 | - } |
|
| 1811 | - |
|
| 1812 | - /** |
|
| 1813 | - * Sets the current version of the state files. |
|
| 1814 | - * |
|
| 1815 | - * @param int $version the new supported version |
|
| 1816 | - * |
|
| 1817 | - * @return bool |
|
| 1818 | - */ |
|
| 1819 | - public function SetStateVersion($version) { |
|
| 1820 | - return true; |
|
| 1821 | - } |
|
| 1822 | - |
|
| 1823 | - /** |
|
| 1824 | - * Returns MAPIFolder object which contains the state information. |
|
| 1825 | - * Creates this folder if it is not available yet. |
|
| 1826 | - * |
|
| 1827 | - * @param string $devid the device id |
|
| 1828 | - * |
|
| 1829 | - * @return MAPIFolder |
|
| 1830 | - */ |
|
| 1831 | - private function getStateFolder($devid) { |
|
| 1832 | - // Options request doesn't send device id |
|
| 1833 | - if (strlen($devid) == 0) { |
|
| 1834 | - return false; |
|
| 1835 | - } |
|
| 1836 | - // Try to get the state folder id from redis |
|
| 1837 | - if (!$this->stateFolder) { |
|
| 1838 | - $folderentryid = $this->getDeviceUserData($this->userDeviceData, $devid, $this->mainUser, "statefolder"); |
|
| 1839 | - if ($folderentryid) { |
|
| 1840 | - $this->stateFolder = mapi_msgstore_openentry($this->store, hex2bin($folderentryid)); |
|
| 1841 | - } |
|
| 1842 | - } |
|
| 1843 | - |
|
| 1844 | - // fallback code |
|
| 1845 | - if (!$this->stateFolder) { |
|
| 1846 | - SLog::Write(LOGLEVEL_INFO, sprintf("Grommunio->getStateFolder(): state folder not set. Use fallback")); |
|
| 1847 | - $rootfolder = mapi_msgstore_openentry($this->store); |
|
| 1848 | - $hierarchy = mapi_folder_gethierarchytable($rootfolder, CONVENIENT_DEPTH); |
|
| 1849 | - $restriction = $this->getStateFolderRestriction($devid); |
|
| 1850 | - // restrict the hierarchy to the grommunio-sync search folder only |
|
| 1851 | - mapi_table_restrict($hierarchy, $restriction); |
|
| 1852 | - $rowCnt = mapi_table_getrowcount($hierarchy); |
|
| 1853 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->getStateFolder(): found %d device state folders", $rowCnt)); |
|
| 1854 | - if ($rowCnt == 1) { |
|
| 1855 | - $hierarchyRows = mapi_table_queryrows($hierarchy, [PR_ENTRYID], 0, 1); |
|
| 1856 | - $this->stateFolder = mapi_msgstore_openentry($this->store, $hierarchyRows[0][PR_ENTRYID]); |
|
| 1857 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->getStateFolder(): %s", bin2hex($hierarchyRows[0][PR_ENTRYID]))); |
|
| 1858 | - // put found id in redis |
|
| 1859 | - if ($devid) { |
|
| 1860 | - $this->setDeviceUserData($this->userDeviceData, bin2hex($hierarchyRows[0][PR_ENTRYID]), $devid, $this->mainUser, "statefolder"); |
|
| 1861 | - } |
|
| 1862 | - } |
|
| 1863 | - elseif ($rowCnt == 0) { |
|
| 1864 | - // legacy code: create the hidden state folder and the device subfolder |
|
| 1865 | - // this should happen when the user configures the device (autodiscover or first sync if no autodiscover) |
|
| 1866 | - |
|
| 1867 | - $hierarchy = mapi_folder_gethierarchytable($rootfolder, CONVENIENT_DEPTH); |
|
| 1868 | - $restriction = $this->getStateFolderRestriction(STORE_STATE_FOLDER); |
|
| 1869 | - mapi_table_restrict($hierarchy, $restriction); |
|
| 1870 | - $rowCnt = mapi_table_getrowcount($hierarchy); |
|
| 1871 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->getStateFolder(): found %d store state folders", $rowCnt)); |
|
| 1872 | - if ($rowCnt == 1) { |
|
| 1873 | - $hierarchyRows = mapi_table_queryrows($hierarchy, [PR_ENTRYID], 0, 1); |
|
| 1874 | - $stateFolder = mapi_msgstore_openentry($this->store, $hierarchyRows[0][PR_ENTRYID]); |
|
| 1875 | - } |
|
| 1876 | - elseif ($rowCnt == 0) { |
|
| 1877 | - $stateFolder = mapi_folder_createfolder($rootfolder, STORE_STATE_FOLDER, ""); |
|
| 1878 | - mapi_setprops($stateFolder, [PR_ATTR_HIDDEN => true]); |
|
| 1879 | - } |
|
| 1880 | - |
|
| 1881 | - // TODO: handle this |
|
| 1882 | - |
|
| 1883 | - if (isset($stateFolder) && $stateFolder) { |
|
| 1884 | - $devStateFolder = mapi_folder_createfolder($stateFolder, $devid, ""); |
|
| 1885 | - $devStateFolderProps = mapi_getprops($devStateFolder); |
|
| 1886 | - $this->stateFolder = mapi_msgstore_openentry($this->store, $devStateFolderProps[PR_ENTRYID]); |
|
| 1887 | - mapi_setprops($this->stateFolder, [PR_ATTR_HIDDEN => true]); |
|
| 1888 | - // we don't cache the entryid in redis, because this will happen on the next request anyway |
|
| 1889 | - } |
|
| 1890 | - |
|
| 1891 | - // TODO: unable to create state folder - throw exception |
|
| 1892 | - } |
|
| 1893 | - |
|
| 1894 | - // This case is rather unlikely that there would be several |
|
| 1895 | - // hidden folders having PR_DISPLAY_NAME the same as device id. |
|
| 1896 | - |
|
| 1897 | - // TODO: get the hierarchy table again, get entry id of STORE_STATE_FOLDER |
|
| 1898 | - // and compare it to the parent id of those folders. |
|
| 1899 | - } |
|
| 1900 | - |
|
| 1901 | - return $this->stateFolder; |
|
| 1902 | - } |
|
| 1903 | - |
|
| 1904 | - /** |
|
| 1905 | - * Returns the associated MAPIMessage which contains the state information. |
|
| 1906 | - * |
|
| 1907 | - * @param string $devid the device id |
|
| 1908 | - * @param string $type the state type |
|
| 1909 | - * @param string $key (opt) |
|
| 1910 | - * @param string $counter state counter |
|
| 1911 | - * |
|
| 1912 | - * @throws StateNotFoundException |
|
| 1913 | - * |
|
| 1914 | - * @return MAPIMessage |
|
| 1915 | - */ |
|
| 1916 | - private function getStateMessage($devid, $type, $key, $counter) { |
|
| 1917 | - if (!$this->stateFolder) { |
|
| 1918 | - $this->getStateFolder(Request::GetDeviceID()); |
|
| 1919 | - if (!$this->stateFolder) { |
|
| 1920 | - throw new StateNotFoundException(sprintf( |
|
| 1921 | - "Grommunio->getStateMessage(): Could not locate the state folder for device '%s'", |
|
| 1922 | - $devid |
|
| 1923 | - )); |
|
| 1924 | - } |
|
| 1925 | - } |
|
| 1926 | - $messageName = rtrim((($key !== false) ? $key . "-" : "") . (($type !== "") ? $type : ""), "-"); |
|
| 1927 | - $restriction = $this->getStateMessageRestriction($messageName, $counter, true); |
|
| 1928 | - $stateFolderContents = mapi_folder_getcontentstable($this->stateFolder, MAPI_ASSOCIATED); |
|
| 1929 | - if ($stateFolderContents) { |
|
| 1930 | - mapi_table_restrict($stateFolderContents, $restriction); |
|
| 1931 | - $rowCnt = mapi_table_getrowcount($stateFolderContents); |
|
| 1932 | - if ($rowCnt == 1) { |
|
| 1933 | - $stateFolderRows = mapi_table_queryrows($stateFolderContents, [PR_ENTRYID], 0, 1); |
|
| 1934 | - |
|
| 1935 | - return mapi_msgstore_openentry($this->store, $stateFolderRows[0][PR_ENTRYID]); |
|
| 1936 | - } |
|
| 1937 | - if ($rowCnt > 1) { |
|
| 1938 | - SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->getStateMessage(): Found several (%d) states for '%s'", $rowCnt, $messageName)); |
|
| 1939 | - } |
|
| 1940 | - } |
|
| 1941 | - |
|
| 1942 | - throw new StateNotFoundException(sprintf( |
|
| 1943 | - "Grommunio->getStateMessage(): Could not locate the state message '%s-%s'", |
|
| 1944 | - $messageName, |
|
| 1945 | - Utils::PrintAsString($counter) |
|
| 1946 | - )); |
|
| 1947 | - } |
|
| 1948 | - |
|
| 1949 | - /** |
|
| 1950 | - * Writes ta state to for a key and counter. |
|
| 1951 | - * |
|
| 1952 | - * @param mixed $state |
|
| 1953 | - * @param string $devid the device id |
|
| 1954 | - * @param string $type the state type |
|
| 1955 | - * @param string $key (opt) |
|
| 1956 | - * @param int $counter (opt) |
|
| 1957 | - * |
|
| 1958 | - * @throws StateInvalidException, UnavailableException |
|
| 1959 | - * |
|
| 1960 | - * @return bool |
|
| 1961 | - */ |
|
| 1962 | - private function setStateMessage($state, $devid, $type, $key = false, $counter = false) { |
|
| 1963 | - if (!$this->stateFolder) { |
|
| 1964 | - throw new StateNotFoundException(sprintf("Grommunio->setStateMessage(): Could not locate the state folder for device '%s'", $devid)); |
|
| 1965 | - } |
|
| 1966 | - |
|
| 1967 | - try { |
|
| 1968 | - $stateMessage = $this->getStateMessage($devid, $type, $key, $counter); |
|
| 1969 | - } |
|
| 1970 | - catch (StateNotFoundException $e) { |
|
| 1971 | - // if message is not available, try to create a new one |
|
| 1972 | - $stateMessage = mapi_folder_createmessage($this->stateFolder, MAPI_ASSOCIATED); |
|
| 1973 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->setStateMessage(): mapi_folder_createmessage 0x%08X", mapi_last_hresult())); |
|
| 1974 | - |
|
| 1975 | - $messageName = rtrim((($key !== false) ? $key . "-" : "") . (($type !== "") ? $type : ""), "-"); |
|
| 1976 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->setStateMessage(): creating new state message '%s-%d'", $messageName, is_int($counter) ? $counter : 0)); |
|
| 1977 | - mapi_setprops($stateMessage, [PR_DISPLAY_NAME => $messageName, PR_MESSAGE_CLASS => 'IPM.Note.GrommunioState']); |
|
| 1978 | - } |
|
| 1979 | - if (isset($stateMessage)) { |
|
| 1980 | - $jsonEncodedState = is_object($state) || is_array($state) ? json_encode($state, JSON_INVALID_UTF8_IGNORE | JSON_UNESCAPED_UNICODE) : $state; |
|
| 1981 | - |
|
| 1982 | - $encodedState = base64_encode($jsonEncodedState); |
|
| 1983 | - $encodedStateLength = strlen($encodedState); |
|
| 1984 | - mapi_setprops($stateMessage, [PR_LAST_VERB_EXECUTED => is_int($counter) ? $counter : 0]); |
|
| 1985 | - $stream = mapi_openproperty($stateMessage, PR_BODY, IID_IStream, STGM_DIRECT, MAPI_CREATE | MAPI_MODIFY); |
|
| 1986 | - mapi_stream_setsize($stream, $encodedStateLength); |
|
| 1987 | - mapi_stream_write($stream, $encodedState); |
|
| 1988 | - mapi_stream_commit($stream); |
|
| 1989 | - mapi_savechanges($stateMessage); |
|
| 1990 | - |
|
| 1991 | - return $encodedStateLength; |
|
| 1992 | - } |
|
| 1993 | - |
|
| 1994 | - return false; |
|
| 1995 | - } |
|
| 1996 | - |
|
| 1997 | - /** |
|
| 1998 | - * Returns the restriction for the state folder name. |
|
| 1999 | - * |
|
| 2000 | - * @param string $folderName the state folder name |
|
| 2001 | - * |
|
| 2002 | - * @return array |
|
| 2003 | - */ |
|
| 2004 | - private function getStateFolderRestriction($folderName) { |
|
| 2005 | - return [RES_AND, [ |
|
| 2006 | - [RES_PROPERTY, |
|
| 2007 | - [RELOP => RELOP_EQ, |
|
| 2008 | - ULPROPTAG => PR_DISPLAY_NAME, |
|
| 2009 | - VALUE => $folderName, |
|
| 2010 | - ], |
|
| 2011 | - ], |
|
| 2012 | - [RES_PROPERTY, |
|
| 2013 | - [RELOP => RELOP_EQ, |
|
| 2014 | - ULPROPTAG => PR_ATTR_HIDDEN, |
|
| 2015 | - VALUE => true, |
|
| 2016 | - ], |
|
| 2017 | - ], |
|
| 2018 | - ]]; |
|
| 2019 | - } |
|
| 2020 | - |
|
| 2021 | - /** |
|
| 2022 | - * Returns the restriction for the associated message in the state folder. |
|
| 2023 | - * |
|
| 2024 | - * @param string $messageName the message name |
|
| 2025 | - * @param string $counter counter |
|
| 2026 | - * @param string $thisCounterOnly (opt) if provided, restrict to the exact counter |
|
| 2027 | - * |
|
| 2028 | - * @return array |
|
| 2029 | - */ |
|
| 2030 | - private function getStateMessageRestriction($messageName, $counter, $thisCounterOnly = false) { |
|
| 2031 | - return [RES_AND, [ |
|
| 2032 | - [RES_PROPERTY, |
|
| 2033 | - [RELOP => RELOP_EQ, |
|
| 2034 | - ULPROPTAG => PR_DISPLAY_NAME, |
|
| 2035 | - VALUE => $messageName, |
|
| 2036 | - ], |
|
| 2037 | - ], |
|
| 2038 | - [RES_PROPERTY, |
|
| 2039 | - [RELOP => RELOP_EQ, |
|
| 2040 | - ULPROPTAG => PR_MESSAGE_CLASS, |
|
| 2041 | - VALUE => 'IPM.Note.GrommunioState', |
|
| 2042 | - ], |
|
| 2043 | - ], |
|
| 2044 | - [RES_PROPERTY, |
|
| 2045 | - [RELOP => $thisCounterOnly ? RELOP_EQ : RELOP_LT, |
|
| 2046 | - ULPROPTAG => PR_LAST_VERB_EXECUTED, |
|
| 2047 | - VALUE => $counter, |
|
| 2048 | - ], |
|
| 2049 | - ], |
|
| 2050 | - ]]; |
|
| 2051 | - } |
|
| 2052 | - |
|
| 2053 | - /*---------------------------------------------------------------------------------------------------------- |
|
| 1641 | + /** |
|
| 1642 | + * Gets a hash value indicating the latest dataset of the named |
|
| 1643 | + * state with a specified key and counter. |
|
| 1644 | + * If the state is changed between two calls of this method |
|
| 1645 | + * the returned hash should be different. |
|
| 1646 | + * |
|
| 1647 | + * @param string $devid the device id |
|
| 1648 | + * @param string $type the state type |
|
| 1649 | + * @param string $key (opt) |
|
| 1650 | + * @param string $counter (opt) |
|
| 1651 | + * |
|
| 1652 | + * @throws StateNotFoundException |
|
| 1653 | + * |
|
| 1654 | + * @return string |
|
| 1655 | + */ |
|
| 1656 | + public function GetStateHash($devid, $type, $key = false, $counter = false) { |
|
| 1657 | + try { |
|
| 1658 | + $stateMessage = $this->getStateMessage($devid, $type, $key, $counter); |
|
| 1659 | + $stateMessageProps = mapi_getprops($stateMessage, [PR_LAST_MODIFICATION_TIME]); |
|
| 1660 | + if (isset($stateMessageProps[PR_LAST_MODIFICATION_TIME])) { |
|
| 1661 | + return $stateMessageProps[PR_LAST_MODIFICATION_TIME]; |
|
| 1662 | + } |
|
| 1663 | + } |
|
| 1664 | + catch (StateNotFoundException $e) { |
|
| 1665 | + } |
|
| 1666 | + |
|
| 1667 | + return "0"; |
|
| 1668 | + } |
|
| 1669 | + |
|
| 1670 | + /** |
|
| 1671 | + * Gets a state for a specified key and counter. |
|
| 1672 | + * This method should call IStateMachine->CleanStates() |
|
| 1673 | + * to remove older states (same key, previous counters). |
|
| 1674 | + * |
|
| 1675 | + * @param string $devid the device id |
|
| 1676 | + * @param string $type the state type |
|
| 1677 | + * @param string $key (opt) |
|
| 1678 | + * @param string $counter (opt) |
|
| 1679 | + * @param string $cleanstates (opt) |
|
| 1680 | + * |
|
| 1681 | + * @throws StateNotFoundException, StateInvalidException, UnavailableException |
|
| 1682 | + * |
|
| 1683 | + * @return mixed |
|
| 1684 | + */ |
|
| 1685 | + public function GetState($devid, $type, $key = false, $counter = false, $cleanstates = true) { |
|
| 1686 | + if ($counter && $cleanstates) { |
|
| 1687 | + $this->CleanStates($devid, $type, $key, $counter); |
|
| 1688 | + // also clean Failsave state for previous counter |
|
| 1689 | + if ($key == false) { |
|
| 1690 | + $this->CleanStates($devid, $type, IStateMachine::FAILSAVE, $counter); |
|
| 1691 | + } |
|
| 1692 | + } |
|
| 1693 | + $stateMessage = $this->getStateMessage($devid, $type, $key, $counter); |
|
| 1694 | + $state = base64_decode(MAPIUtils::readPropStream($stateMessage, PR_BODY)); |
|
| 1695 | + |
|
| 1696 | + if ($state && $state[0] === '{') { |
|
| 1697 | + $jsonDec = json_decode($state); |
|
| 1698 | + if (isset($jsonDec->gsSyncStateClass)) { |
|
| 1699 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetState(): top class '%s'", $jsonDec->gsSyncStateClass)); |
|
| 1700 | + $gsObj = new $jsonDec->gsSyncStateClass(); |
|
| 1701 | + $gsObj->jsonDeserialize($jsonDec); |
|
| 1702 | + $gsObj->postUnserialize(); |
|
| 1703 | + } |
|
| 1704 | + } |
|
| 1705 | + |
|
| 1706 | + return isset($gsObj) && is_object($gsObj) ? $gsObj : $state; |
|
| 1707 | + } |
|
| 1708 | + |
|
| 1709 | + /** |
|
| 1710 | + * Writes ta state to for a key and counter. |
|
| 1711 | + * |
|
| 1712 | + * @param mixed $state |
|
| 1713 | + * @param string $devid the device id |
|
| 1714 | + * @param string $type the state type |
|
| 1715 | + * @param string $key (opt) |
|
| 1716 | + * @param int $counter (opt) |
|
| 1717 | + * |
|
| 1718 | + * @throws StateInvalidException, UnavailableException |
|
| 1719 | + * |
|
| 1720 | + * @return bool |
|
| 1721 | + */ |
|
| 1722 | + public function SetState($state, $devid, $type, $key = false, $counter = false) { |
|
| 1723 | + return $this->setStateMessage($state, $devid, $type, $key, $counter); |
|
| 1724 | + } |
|
| 1725 | + |
|
| 1726 | + /** |
|
| 1727 | + * Cleans up all older states. |
|
| 1728 | + * If called with a $counter, all states previous state counter can be removed. |
|
| 1729 | + * If additionally the $thisCounterOnly flag is true, only that specific counter will be removed. |
|
| 1730 | + * If called without $counter, all keys (independently from the counter) can be removed. |
|
| 1731 | + * |
|
| 1732 | + * @param string $devid the device id |
|
| 1733 | + * @param string $type the state type |
|
| 1734 | + * @param string $key |
|
| 1735 | + * @param string $counter (opt) |
|
| 1736 | + * @param string $thisCounterOnly (opt) if provided, the exact counter only will be removed |
|
| 1737 | + * |
|
| 1738 | + * @throws StateInvalidException |
|
| 1739 | + * |
|
| 1740 | + * @return |
|
| 1741 | + */ |
|
| 1742 | + public function CleanStates($devid, $type, $key, $counter = false, $thisCounterOnly = false) { |
|
| 1743 | + if (!$this->stateFolder) { |
|
| 1744 | + $this->getStateFolder($devid); |
|
| 1745 | + if (!$this->stateFolder) { |
|
| 1746 | + throw new StateNotFoundException(sprintf( |
|
| 1747 | + "Grommunio->getStateMessage(): Could not locate the state folder for device '%s'", |
|
| 1748 | + $devid |
|
| 1749 | + )); |
|
| 1750 | + } |
|
| 1751 | + } |
|
| 1752 | + $messageName = rtrim((($key !== false) ? $key . "-" : "") . (($type !== "") ? $type : ""), "-"); |
|
| 1753 | + $restriction = $this->getStateMessageRestriction($messageName, $counter, $thisCounterOnly); |
|
| 1754 | + $stateFolderContents = mapi_folder_getcontentstable($this->stateFolder, MAPI_ASSOCIATED); |
|
| 1755 | + if ($stateFolderContents) { |
|
| 1756 | + mapi_table_restrict($stateFolderContents, $restriction); |
|
| 1757 | + $rowCnt = mapi_table_getrowcount($stateFolderContents); |
|
| 1758 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->CleanStates(): Found %d states to clean (%s)", $rowCnt, $messageName)); |
|
| 1759 | + if ($rowCnt > 0) { |
|
| 1760 | + $rows = mapi_table_queryallrows($stateFolderContents, [PR_ENTRYID]); |
|
| 1761 | + $entryids = []; |
|
| 1762 | + foreach ($rows as $row) { |
|
| 1763 | + $entryids[] = $row[PR_ENTRYID]; |
|
| 1764 | + } |
|
| 1765 | + mapi_folder_deletemessages($this->stateFolder, $entryids, DELETE_HARD_DELETE); |
|
| 1766 | + } |
|
| 1767 | + } |
|
| 1768 | + } |
|
| 1769 | + |
|
| 1770 | + /** |
|
| 1771 | + * Links a user to a device. |
|
| 1772 | + * |
|
| 1773 | + * @param string $username |
|
| 1774 | + * @param string $devid |
|
| 1775 | + * |
|
| 1776 | + * @return bool indicating if the user was added or not (existed already) |
|
| 1777 | + */ |
|
| 1778 | + public function LinkUserDevice($username, $devid) { |
|
| 1779 | + $device = [$devid => time()]; |
|
| 1780 | + $this->setDeviceUserData($this->type, $device, $username, -1, $subkey = -1, $doCas = "merge"); |
|
| 1781 | + |
|
| 1782 | + return false; |
|
| 1783 | + } |
|
| 1784 | + |
|
| 1785 | + /** |
|
| 1786 | + * Unlinks a device from a user. |
|
| 1787 | + * |
|
| 1788 | + * @param string $username |
|
| 1789 | + * @param string $devid |
|
| 1790 | + * |
|
| 1791 | + * @return bool |
|
| 1792 | + */ |
|
| 1793 | + public function UnLinkUserDevice($username, $devid) { |
|
| 1794 | + // TODO: Implement |
|
| 1795 | + return false; |
|
| 1796 | + } |
|
| 1797 | + |
|
| 1798 | + /** |
|
| 1799 | + * Returns the current version of the state files |
|
| 1800 | + * grommunio: This is not relevant atm. IStateMachine::STATEVERSION_02 will match GSync::GetLatestStateVersion(). |
|
| 1801 | + * If it might be required to update states in the future, this could be implemented on a store level, |
|
| 1802 | + * where states are then migrated "on-the-fly" |
|
| 1803 | + * or |
|
| 1804 | + * in a global settings where all states in all stores are migrated once. |
|
| 1805 | + * |
|
| 1806 | + * @return int |
|
| 1807 | + */ |
|
| 1808 | + public function GetStateVersion() { |
|
| 1809 | + return IStateMachine::STATEVERSION_02; |
|
| 1810 | + } |
|
| 1811 | + |
|
| 1812 | + /** |
|
| 1813 | + * Sets the current version of the state files. |
|
| 1814 | + * |
|
| 1815 | + * @param int $version the new supported version |
|
| 1816 | + * |
|
| 1817 | + * @return bool |
|
| 1818 | + */ |
|
| 1819 | + public function SetStateVersion($version) { |
|
| 1820 | + return true; |
|
| 1821 | + } |
|
| 1822 | + |
|
| 1823 | + /** |
|
| 1824 | + * Returns MAPIFolder object which contains the state information. |
|
| 1825 | + * Creates this folder if it is not available yet. |
|
| 1826 | + * |
|
| 1827 | + * @param string $devid the device id |
|
| 1828 | + * |
|
| 1829 | + * @return MAPIFolder |
|
| 1830 | + */ |
|
| 1831 | + private function getStateFolder($devid) { |
|
| 1832 | + // Options request doesn't send device id |
|
| 1833 | + if (strlen($devid) == 0) { |
|
| 1834 | + return false; |
|
| 1835 | + } |
|
| 1836 | + // Try to get the state folder id from redis |
|
| 1837 | + if (!$this->stateFolder) { |
|
| 1838 | + $folderentryid = $this->getDeviceUserData($this->userDeviceData, $devid, $this->mainUser, "statefolder"); |
|
| 1839 | + if ($folderentryid) { |
|
| 1840 | + $this->stateFolder = mapi_msgstore_openentry($this->store, hex2bin($folderentryid)); |
|
| 1841 | + } |
|
| 1842 | + } |
|
| 1843 | + |
|
| 1844 | + // fallback code |
|
| 1845 | + if (!$this->stateFolder) { |
|
| 1846 | + SLog::Write(LOGLEVEL_INFO, sprintf("Grommunio->getStateFolder(): state folder not set. Use fallback")); |
|
| 1847 | + $rootfolder = mapi_msgstore_openentry($this->store); |
|
| 1848 | + $hierarchy = mapi_folder_gethierarchytable($rootfolder, CONVENIENT_DEPTH); |
|
| 1849 | + $restriction = $this->getStateFolderRestriction($devid); |
|
| 1850 | + // restrict the hierarchy to the grommunio-sync search folder only |
|
| 1851 | + mapi_table_restrict($hierarchy, $restriction); |
|
| 1852 | + $rowCnt = mapi_table_getrowcount($hierarchy); |
|
| 1853 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->getStateFolder(): found %d device state folders", $rowCnt)); |
|
| 1854 | + if ($rowCnt == 1) { |
|
| 1855 | + $hierarchyRows = mapi_table_queryrows($hierarchy, [PR_ENTRYID], 0, 1); |
|
| 1856 | + $this->stateFolder = mapi_msgstore_openentry($this->store, $hierarchyRows[0][PR_ENTRYID]); |
|
| 1857 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->getStateFolder(): %s", bin2hex($hierarchyRows[0][PR_ENTRYID]))); |
|
| 1858 | + // put found id in redis |
|
| 1859 | + if ($devid) { |
|
| 1860 | + $this->setDeviceUserData($this->userDeviceData, bin2hex($hierarchyRows[0][PR_ENTRYID]), $devid, $this->mainUser, "statefolder"); |
|
| 1861 | + } |
|
| 1862 | + } |
|
| 1863 | + elseif ($rowCnt == 0) { |
|
| 1864 | + // legacy code: create the hidden state folder and the device subfolder |
|
| 1865 | + // this should happen when the user configures the device (autodiscover or first sync if no autodiscover) |
|
| 1866 | + |
|
| 1867 | + $hierarchy = mapi_folder_gethierarchytable($rootfolder, CONVENIENT_DEPTH); |
|
| 1868 | + $restriction = $this->getStateFolderRestriction(STORE_STATE_FOLDER); |
|
| 1869 | + mapi_table_restrict($hierarchy, $restriction); |
|
| 1870 | + $rowCnt = mapi_table_getrowcount($hierarchy); |
|
| 1871 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->getStateFolder(): found %d store state folders", $rowCnt)); |
|
| 1872 | + if ($rowCnt == 1) { |
|
| 1873 | + $hierarchyRows = mapi_table_queryrows($hierarchy, [PR_ENTRYID], 0, 1); |
|
| 1874 | + $stateFolder = mapi_msgstore_openentry($this->store, $hierarchyRows[0][PR_ENTRYID]); |
|
| 1875 | + } |
|
| 1876 | + elseif ($rowCnt == 0) { |
|
| 1877 | + $stateFolder = mapi_folder_createfolder($rootfolder, STORE_STATE_FOLDER, ""); |
|
| 1878 | + mapi_setprops($stateFolder, [PR_ATTR_HIDDEN => true]); |
|
| 1879 | + } |
|
| 1880 | + |
|
| 1881 | + // TODO: handle this |
|
| 1882 | + |
|
| 1883 | + if (isset($stateFolder) && $stateFolder) { |
|
| 1884 | + $devStateFolder = mapi_folder_createfolder($stateFolder, $devid, ""); |
|
| 1885 | + $devStateFolderProps = mapi_getprops($devStateFolder); |
|
| 1886 | + $this->stateFolder = mapi_msgstore_openentry($this->store, $devStateFolderProps[PR_ENTRYID]); |
|
| 1887 | + mapi_setprops($this->stateFolder, [PR_ATTR_HIDDEN => true]); |
|
| 1888 | + // we don't cache the entryid in redis, because this will happen on the next request anyway |
|
| 1889 | + } |
|
| 1890 | + |
|
| 1891 | + // TODO: unable to create state folder - throw exception |
|
| 1892 | + } |
|
| 1893 | + |
|
| 1894 | + // This case is rather unlikely that there would be several |
|
| 1895 | + // hidden folders having PR_DISPLAY_NAME the same as device id. |
|
| 1896 | + |
|
| 1897 | + // TODO: get the hierarchy table again, get entry id of STORE_STATE_FOLDER |
|
| 1898 | + // and compare it to the parent id of those folders. |
|
| 1899 | + } |
|
| 1900 | + |
|
| 1901 | + return $this->stateFolder; |
|
| 1902 | + } |
|
| 1903 | + |
|
| 1904 | + /** |
|
| 1905 | + * Returns the associated MAPIMessage which contains the state information. |
|
| 1906 | + * |
|
| 1907 | + * @param string $devid the device id |
|
| 1908 | + * @param string $type the state type |
|
| 1909 | + * @param string $key (opt) |
|
| 1910 | + * @param string $counter state counter |
|
| 1911 | + * |
|
| 1912 | + * @throws StateNotFoundException |
|
| 1913 | + * |
|
| 1914 | + * @return MAPIMessage |
|
| 1915 | + */ |
|
| 1916 | + private function getStateMessage($devid, $type, $key, $counter) { |
|
| 1917 | + if (!$this->stateFolder) { |
|
| 1918 | + $this->getStateFolder(Request::GetDeviceID()); |
|
| 1919 | + if (!$this->stateFolder) { |
|
| 1920 | + throw new StateNotFoundException(sprintf( |
|
| 1921 | + "Grommunio->getStateMessage(): Could not locate the state folder for device '%s'", |
|
| 1922 | + $devid |
|
| 1923 | + )); |
|
| 1924 | + } |
|
| 1925 | + } |
|
| 1926 | + $messageName = rtrim((($key !== false) ? $key . "-" : "") . (($type !== "") ? $type : ""), "-"); |
|
| 1927 | + $restriction = $this->getStateMessageRestriction($messageName, $counter, true); |
|
| 1928 | + $stateFolderContents = mapi_folder_getcontentstable($this->stateFolder, MAPI_ASSOCIATED); |
|
| 1929 | + if ($stateFolderContents) { |
|
| 1930 | + mapi_table_restrict($stateFolderContents, $restriction); |
|
| 1931 | + $rowCnt = mapi_table_getrowcount($stateFolderContents); |
|
| 1932 | + if ($rowCnt == 1) { |
|
| 1933 | + $stateFolderRows = mapi_table_queryrows($stateFolderContents, [PR_ENTRYID], 0, 1); |
|
| 1934 | + |
|
| 1935 | + return mapi_msgstore_openentry($this->store, $stateFolderRows[0][PR_ENTRYID]); |
|
| 1936 | + } |
|
| 1937 | + if ($rowCnt > 1) { |
|
| 1938 | + SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->getStateMessage(): Found several (%d) states for '%s'", $rowCnt, $messageName)); |
|
| 1939 | + } |
|
| 1940 | + } |
|
| 1941 | + |
|
| 1942 | + throw new StateNotFoundException(sprintf( |
|
| 1943 | + "Grommunio->getStateMessage(): Could not locate the state message '%s-%s'", |
|
| 1944 | + $messageName, |
|
| 1945 | + Utils::PrintAsString($counter) |
|
| 1946 | + )); |
|
| 1947 | + } |
|
| 1948 | + |
|
| 1949 | + /** |
|
| 1950 | + * Writes ta state to for a key and counter. |
|
| 1951 | + * |
|
| 1952 | + * @param mixed $state |
|
| 1953 | + * @param string $devid the device id |
|
| 1954 | + * @param string $type the state type |
|
| 1955 | + * @param string $key (opt) |
|
| 1956 | + * @param int $counter (opt) |
|
| 1957 | + * |
|
| 1958 | + * @throws StateInvalidException, UnavailableException |
|
| 1959 | + * |
|
| 1960 | + * @return bool |
|
| 1961 | + */ |
|
| 1962 | + private function setStateMessage($state, $devid, $type, $key = false, $counter = false) { |
|
| 1963 | + if (!$this->stateFolder) { |
|
| 1964 | + throw new StateNotFoundException(sprintf("Grommunio->setStateMessage(): Could not locate the state folder for device '%s'", $devid)); |
|
| 1965 | + } |
|
| 1966 | + |
|
| 1967 | + try { |
|
| 1968 | + $stateMessage = $this->getStateMessage($devid, $type, $key, $counter); |
|
| 1969 | + } |
|
| 1970 | + catch (StateNotFoundException $e) { |
|
| 1971 | + // if message is not available, try to create a new one |
|
| 1972 | + $stateMessage = mapi_folder_createmessage($this->stateFolder, MAPI_ASSOCIATED); |
|
| 1973 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->setStateMessage(): mapi_folder_createmessage 0x%08X", mapi_last_hresult())); |
|
| 1974 | + |
|
| 1975 | + $messageName = rtrim((($key !== false) ? $key . "-" : "") . (($type !== "") ? $type : ""), "-"); |
|
| 1976 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->setStateMessage(): creating new state message '%s-%d'", $messageName, is_int($counter) ? $counter : 0)); |
|
| 1977 | + mapi_setprops($stateMessage, [PR_DISPLAY_NAME => $messageName, PR_MESSAGE_CLASS => 'IPM.Note.GrommunioState']); |
|
| 1978 | + } |
|
| 1979 | + if (isset($stateMessage)) { |
|
| 1980 | + $jsonEncodedState = is_object($state) || is_array($state) ? json_encode($state, JSON_INVALID_UTF8_IGNORE | JSON_UNESCAPED_UNICODE) : $state; |
|
| 1981 | + |
|
| 1982 | + $encodedState = base64_encode($jsonEncodedState); |
|
| 1983 | + $encodedStateLength = strlen($encodedState); |
|
| 1984 | + mapi_setprops($stateMessage, [PR_LAST_VERB_EXECUTED => is_int($counter) ? $counter : 0]); |
|
| 1985 | + $stream = mapi_openproperty($stateMessage, PR_BODY, IID_IStream, STGM_DIRECT, MAPI_CREATE | MAPI_MODIFY); |
|
| 1986 | + mapi_stream_setsize($stream, $encodedStateLength); |
|
| 1987 | + mapi_stream_write($stream, $encodedState); |
|
| 1988 | + mapi_stream_commit($stream); |
|
| 1989 | + mapi_savechanges($stateMessage); |
|
| 1990 | + |
|
| 1991 | + return $encodedStateLength; |
|
| 1992 | + } |
|
| 1993 | + |
|
| 1994 | + return false; |
|
| 1995 | + } |
|
| 1996 | + |
|
| 1997 | + /** |
|
| 1998 | + * Returns the restriction for the state folder name. |
|
| 1999 | + * |
|
| 2000 | + * @param string $folderName the state folder name |
|
| 2001 | + * |
|
| 2002 | + * @return array |
|
| 2003 | + */ |
|
| 2004 | + private function getStateFolderRestriction($folderName) { |
|
| 2005 | + return [RES_AND, [ |
|
| 2006 | + [RES_PROPERTY, |
|
| 2007 | + [RELOP => RELOP_EQ, |
|
| 2008 | + ULPROPTAG => PR_DISPLAY_NAME, |
|
| 2009 | + VALUE => $folderName, |
|
| 2010 | + ], |
|
| 2011 | + ], |
|
| 2012 | + [RES_PROPERTY, |
|
| 2013 | + [RELOP => RELOP_EQ, |
|
| 2014 | + ULPROPTAG => PR_ATTR_HIDDEN, |
|
| 2015 | + VALUE => true, |
|
| 2016 | + ], |
|
| 2017 | + ], |
|
| 2018 | + ]]; |
|
| 2019 | + } |
|
| 2020 | + |
|
| 2021 | + /** |
|
| 2022 | + * Returns the restriction for the associated message in the state folder. |
|
| 2023 | + * |
|
| 2024 | + * @param string $messageName the message name |
|
| 2025 | + * @param string $counter counter |
|
| 2026 | + * @param string $thisCounterOnly (opt) if provided, restrict to the exact counter |
|
| 2027 | + * |
|
| 2028 | + * @return array |
|
| 2029 | + */ |
|
| 2030 | + private function getStateMessageRestriction($messageName, $counter, $thisCounterOnly = false) { |
|
| 2031 | + return [RES_AND, [ |
|
| 2032 | + [RES_PROPERTY, |
|
| 2033 | + [RELOP => RELOP_EQ, |
|
| 2034 | + ULPROPTAG => PR_DISPLAY_NAME, |
|
| 2035 | + VALUE => $messageName, |
|
| 2036 | + ], |
|
| 2037 | + ], |
|
| 2038 | + [RES_PROPERTY, |
|
| 2039 | + [RELOP => RELOP_EQ, |
|
| 2040 | + ULPROPTAG => PR_MESSAGE_CLASS, |
|
| 2041 | + VALUE => 'IPM.Note.GrommunioState', |
|
| 2042 | + ], |
|
| 2043 | + ], |
|
| 2044 | + [RES_PROPERTY, |
|
| 2045 | + [RELOP => $thisCounterOnly ? RELOP_EQ : RELOP_LT, |
|
| 2046 | + ULPROPTAG => PR_LAST_VERB_EXECUTED, |
|
| 2047 | + VALUE => $counter, |
|
| 2048 | + ], |
|
| 2049 | + ], |
|
| 2050 | + ]]; |
|
| 2051 | + } |
|
| 2052 | + |
|
| 2053 | + /*---------------------------------------------------------------------------------------------------------- |
|
| 2054 | 2054 | * Private methods |
| 2055 | 2055 | */ |
| 2056 | 2056 | |
| 2057 | - /** |
|
| 2058 | - * Returns a hash representing changes in the hierarchy of the main user. |
|
| 2059 | - * It changes if a folder is added, renamed or deleted. |
|
| 2060 | - * |
|
| 2061 | - * @return string |
|
| 2062 | - */ |
|
| 2063 | - private function getHierarchyHash() { |
|
| 2064 | - $rootfolder = mapi_msgstore_openentry($this->defaultstore); |
|
| 2065 | - $hierarchy = mapi_folder_gethierarchytable($rootfolder, CONVENIENT_DEPTH); |
|
| 2066 | - |
|
| 2067 | - return md5(serialize(mapi_table_queryallrows($hierarchy, [PR_DISPLAY_NAME, PR_PARENT_ENTRYID]))); |
|
| 2068 | - } |
|
| 2069 | - |
|
| 2070 | - /** |
|
| 2071 | - * Advises a store to the changes sink. |
|
| 2072 | - * |
|
| 2073 | - * @param mapistore $store store to be advised |
|
| 2074 | - * |
|
| 2075 | - * @return bool |
|
| 2076 | - */ |
|
| 2077 | - private function adviseStoreToSink($store) { |
|
| 2078 | - // check if we already advised the store |
|
| 2079 | - if (!in_array($store, $this->changesSinkStores)) { |
|
| 2080 | - mapi_msgstore_advise($store, null, fnevObjectModified | fnevObjectCreated | fnevObjectMoved | fnevObjectDeleted, $this->changesSink); |
|
| 2081 | - |
|
| 2082 | - if (mapi_last_hresult()) { |
|
| 2083 | - SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->adviseStoreToSink(): failed to advised store '%s' with code 0x%X. Polling will be performed.", $store, mapi_last_hresult())); |
|
| 2084 | - |
|
| 2085 | - return false; |
|
| 2086 | - } |
|
| 2087 | - |
|
| 2088 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->adviseStoreToSink(): advised store '%s'", $store)); |
|
| 2089 | - $this->changesSinkStores[] = $store; |
|
| 2090 | - } |
|
| 2091 | - |
|
| 2092 | - return true; |
|
| 2093 | - } |
|
| 2094 | - |
|
| 2095 | - /** |
|
| 2096 | - * Open the store marked with PR_DEFAULT_STORE = TRUE |
|
| 2097 | - * if $return_public is set, the public store is opened. |
|
| 2098 | - * |
|
| 2099 | - * @param string $user User which store should be opened |
|
| 2100 | - * |
|
| 2101 | - * @return bool |
|
| 2102 | - */ |
|
| 2103 | - private function openMessageStore($user) { |
|
| 2104 | - // During PING requests the operations store has to be switched constantly |
|
| 2105 | - // the cache prevents the same store opened several times |
|
| 2106 | - if (isset($this->storeCache[$user])) { |
|
| 2107 | - return $this->storeCache[$user]; |
|
| 2108 | - } |
|
| 2109 | - |
|
| 2110 | - $entryid = false; |
|
| 2111 | - $return_public = false; |
|
| 2112 | - |
|
| 2113 | - if (strtoupper($user) == 'SYSTEM') { |
|
| 2114 | - $return_public = true; |
|
| 2115 | - } |
|
| 2116 | - |
|
| 2117 | - // loop through the storestable if authenticated user of public folder |
|
| 2118 | - if ($user == $this->mainUser || $return_public === true) { |
|
| 2119 | - // Find the default store |
|
| 2120 | - $storestables = mapi_getmsgstorestable($this->session); |
|
| 2121 | - $result = mapi_last_hresult(); |
|
| 2122 | - |
|
| 2123 | - if ($result == NOERROR) { |
|
| 2124 | - $rows = mapi_table_queryallrows($storestables, [PR_ENTRYID, PR_DEFAULT_STORE, PR_MDB_PROVIDER]); |
|
| 2125 | - |
|
| 2126 | - foreach ($rows as $row) { |
|
| 2127 | - if (!$return_public && isset($row[PR_DEFAULT_STORE]) && $row[PR_DEFAULT_STORE] == true) { |
|
| 2128 | - $entryid = $row[PR_ENTRYID]; |
|
| 2129 | - |
|
| 2130 | - break; |
|
| 2131 | - } |
|
| 2132 | - if ($return_public && isset($row[PR_MDB_PROVIDER]) && $row[PR_MDB_PROVIDER] == ZARAFA_STORE_PUBLIC_GUID) { |
|
| 2133 | - $entryid = $row[PR_ENTRYID]; |
|
| 2134 | - |
|
| 2135 | - break; |
|
| 2136 | - } |
|
| 2137 | - } |
|
| 2138 | - } |
|
| 2139 | - } |
|
| 2140 | - else { |
|
| 2141 | - $entryid = @mapi_msgstore_createentryid($this->defaultstore, $user); |
|
| 2142 | - } |
|
| 2143 | - |
|
| 2144 | - if ($entryid) { |
|
| 2145 | - $store = @mapi_openmsgstore($this->session, $entryid); |
|
| 2146 | - |
|
| 2147 | - if (!$store) { |
|
| 2148 | - SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->openMessageStore('%s'): Could not open store", $user)); |
|
| 2149 | - |
|
| 2150 | - return false; |
|
| 2151 | - } |
|
| 2152 | - |
|
| 2153 | - // add this store to the cache |
|
| 2154 | - if (!isset($this->storeCache[$user])) { |
|
| 2155 | - $this->storeCache[$user] = $store; |
|
| 2156 | - } |
|
| 2157 | - |
|
| 2158 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->openMessageStore('%s'): Found '%s' store: '%s'", $user, (($return_public) ? 'PUBLIC' : 'DEFAULT'), $store)); |
|
| 2159 | - |
|
| 2160 | - return $store; |
|
| 2161 | - } |
|
| 2162 | - |
|
| 2163 | - SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->openMessageStore('%s'): No store found for this user", $user)); |
|
| 2164 | - |
|
| 2165 | - return false; |
|
| 2166 | - } |
|
| 2167 | - |
|
| 2168 | - /** |
|
| 2169 | - * Checks if the logged in user has secretary permissions on a folder. |
|
| 2170 | - * |
|
| 2171 | - * @param resource $store |
|
| 2172 | - * @param string $folderid |
|
| 2173 | - * @param mixed $entryid |
|
| 2174 | - * |
|
| 2175 | - * @return bool |
|
| 2176 | - */ |
|
| 2177 | - public function HasSecretaryACLs($store, $folderid, $entryid = false) { |
|
| 2178 | - if (!$entryid) { |
|
| 2179 | - $entryid = mapi_msgstore_entryidfromsourcekey($store, hex2bin($folderid)); |
|
| 2180 | - if (!$entryid) { |
|
| 2181 | - SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->HasSecretaryACLs(): error, no entryid resolved for %s on store %s", $folderid, $store)); |
|
| 2182 | - |
|
| 2183 | - return false; |
|
| 2184 | - } |
|
| 2185 | - } |
|
| 2186 | - |
|
| 2187 | - $folder = mapi_msgstore_openentry($store, $entryid); |
|
| 2188 | - if (!$folder) { |
|
| 2189 | - SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->HasSecretaryACLs(): error, could not open folder with entryid %s on store %s", bin2hex($entryid), $store)); |
|
| 2190 | - |
|
| 2191 | - return false; |
|
| 2192 | - } |
|
| 2193 | - |
|
| 2194 | - $props = mapi_getprops($folder, [PR_RIGHTS]); |
|
| 2195 | - if (isset($props[PR_RIGHTS]) && |
|
| 2196 | - ($props[PR_RIGHTS] & ecRightsReadAny) && |
|
| 2197 | - ($props[PR_RIGHTS] & ecRightsCreate) && |
|
| 2198 | - ($props[PR_RIGHTS] & ecRightsEditOwned) && |
|
| 2199 | - ($props[PR_RIGHTS] & ecRightsDeleteOwned) && |
|
| 2200 | - ($props[PR_RIGHTS] & ecRightsEditAny) && |
|
| 2201 | - ($props[PR_RIGHTS] & ecRightsDeleteAny) && |
|
| 2202 | - ($props[PR_RIGHTS] & ecRightsFolderVisible)) { |
|
| 2203 | - return true; |
|
| 2204 | - } |
|
| 2205 | - |
|
| 2206 | - return false; |
|
| 2207 | - } |
|
| 2208 | - |
|
| 2209 | - /** |
|
| 2210 | - * The meta function for out of office settings. |
|
| 2211 | - * |
|
| 2212 | - * @param SyncObject $oof |
|
| 2213 | - */ |
|
| 2214 | - private function settingsOOF(&$oof) { |
|
| 2215 | - // if oof state is set it must be set of oof and get otherwise |
|
| 2216 | - if (isset($oof->oofstate)) { |
|
| 2217 | - $this->settingsOofSet($oof); |
|
| 2218 | - } |
|
| 2219 | - else { |
|
| 2220 | - $this->settingsOofGet($oof); |
|
| 2221 | - } |
|
| 2222 | - } |
|
| 2223 | - |
|
| 2224 | - /** |
|
| 2225 | - * Gets the out of office settings. |
|
| 2226 | - * |
|
| 2227 | - * @param SyncObject $oof |
|
| 2228 | - */ |
|
| 2229 | - private function settingsOofGet(&$oof) { |
|
| 2230 | - $oofprops = mapi_getprops($this->defaultstore, [PR_EC_OUTOFOFFICE, PR_EC_OUTOFOFFICE_MSG, PR_EC_OUTOFOFFICE_SUBJECT, PR_EC_OUTOFOFFICE_FROM, PR_EC_OUTOFOFFICE_UNTIL]); |
|
| 2231 | - $oof->oofstate = SYNC_SETTINGSOOF_DISABLED; |
|
| 2232 | - $oof->Status = SYNC_SETTINGSSTATUS_SUCCESS; |
|
| 2233 | - if ($oofprops != false) { |
|
| 2234 | - $oof->oofstate = isset($oofprops[PR_EC_OUTOFOFFICE]) ? ($oofprops[PR_EC_OUTOFOFFICE] ? SYNC_SETTINGSOOF_GLOBAL : SYNC_SETTINGSOOF_DISABLED) : SYNC_SETTINGSOOF_DISABLED; |
|
| 2235 | - // TODO external and external unknown |
|
| 2236 | - $oofmessage = new SyncOOFMessage(); |
|
| 2237 | - $oofmessage->appliesToInternal = ""; |
|
| 2238 | - $oofmessage->enabled = $oof->oofstate; |
|
| 2239 | - $oofmessage->replymessage = (isset($oofprops[PR_EC_OUTOFOFFICE_MSG])) ? w2u($oofprops[PR_EC_OUTOFOFFICE_MSG]) : ""; |
|
| 2240 | - $oofmessage->bodytype = $oof->bodytype; |
|
| 2241 | - unset($oofmessage->appliesToExternal, $oofmessage->appliesToExternalUnknown); |
|
| 2242 | - $oof->oofmessage[] = $oofmessage; |
|
| 2243 | - |
|
| 2244 | - // check whether time based out of office is set |
|
| 2245 | - if ($oof->oofstate == SYNC_SETTINGSOOF_GLOBAL && isset($oofprops[PR_EC_OUTOFOFFICE_FROM], $oofprops[PR_EC_OUTOFOFFICE_UNTIL])) { |
|
| 2246 | - $now = time(); |
|
| 2247 | - if ($now > $oofprops[PR_EC_OUTOFOFFICE_FROM] && $now > $oofprops[PR_EC_OUTOFOFFICE_UNTIL]) { |
|
| 2248 | - // Out of office is set but the date is in the past. Set the state to disabled. |
|
| 2249 | - // @see https://jira.z-hub.io/browse/ZP-1188 for details |
|
| 2250 | - $oof->oofstate = SYNC_SETTINGSOOF_DISABLED; |
|
| 2251 | - @mapi_setprops($this->defaultstore, [PR_EC_OUTOFOFFICE => false]); |
|
| 2252 | - @mapi_deleteprops($this->defaultstore, [PR_EC_OUTOFOFFICE_FROM, PR_EC_OUTOFOFFICE_UNTIL]); |
|
| 2253 | - SLog::Write(LOGLEVEL_INFO, "Grommunio->settingsOofGet(): Out of office is set but the from and until are in the past. Disabling out of office."); |
|
| 2254 | - } |
|
| 2255 | - elseif ($oofprops[PR_EC_OUTOFOFFICE_FROM] < $oofprops[PR_EC_OUTOFOFFICE_UNTIL]) { |
|
| 2256 | - $oof->oofstate = SYNC_SETTINGSOOF_TIMEBASED; |
|
| 2257 | - $oof->starttime = $oofprops[PR_EC_OUTOFOFFICE_FROM]; |
|
| 2258 | - $oof->endtime = $oofprops[PR_EC_OUTOFOFFICE_UNTIL]; |
|
| 2259 | - } |
|
| 2260 | - else { |
|
| 2261 | - SLog::Write(LOGLEVEL_WARN, sprintf( |
|
| 2262 | - "Grommunio->settingsOofGet(): Time based out of office set but end time ('%s') is before startime ('%s').", |
|
| 2263 | - date("Y-m-d H:i:s", $oofprops[PR_EC_OUTOFOFFICE_FROM]), |
|
| 2264 | - date("Y-m-d H:i:s", $oofprops[PR_EC_OUTOFOFFICE_UNTIL]) |
|
| 2265 | - )); |
|
| 2266 | - $oof->Status = SYNC_SETTINGSSTATUS_PROTOCOLLERROR; |
|
| 2267 | - } |
|
| 2268 | - } |
|
| 2269 | - elseif ($oof->oofstate == SYNC_SETTINGSOOF_GLOBAL && (isset($oofprops[PR_EC_OUTOFOFFICE_FROM]) || isset($oofprops[PR_EC_OUTOFOFFICE_UNTIL]))) { |
|
| 2270 | - SLog::Write(LOGLEVEL_WARN, sprintf( |
|
| 2271 | - "Grommunio->settingsOofGet(): Time based out of office set but either start time ('%s') or end time ('%s') is missing.", |
|
| 2272 | - (isset($oofprops[PR_EC_OUTOFOFFICE_FROM]) ? date("Y-m-d H:i:s", $oofprops[PR_EC_OUTOFOFFICE_FROM]) : 'empty'), |
|
| 2273 | - (isset($oofprops[PR_EC_OUTOFOFFICE_UNTIL]) ? date("Y-m-d H:i:s", $oofprops[PR_EC_OUTOFOFFICE_UNTIL]) : 'empty') |
|
| 2274 | - )); |
|
| 2275 | - $oof->Status = SYNC_SETTINGSSTATUS_PROTOCOLLERROR; |
|
| 2276 | - } |
|
| 2277 | - } |
|
| 2278 | - else { |
|
| 2279 | - SLog::Write(LOGLEVEL_WARN, "Grommunio->Unable to get out of office information"); |
|
| 2280 | - } |
|
| 2281 | - |
|
| 2282 | - // unset body type for oof in order not to stream it |
|
| 2283 | - unset($oof->bodytype); |
|
| 2284 | - } |
|
| 2285 | - |
|
| 2286 | - /** |
|
| 2287 | - * Sets the out of office settings. |
|
| 2288 | - * |
|
| 2289 | - * @param SyncObject $oof |
|
| 2290 | - */ |
|
| 2291 | - private function settingsOofSet(&$oof) { |
|
| 2292 | - $oof->Status = SYNC_SETTINGSSTATUS_SUCCESS; |
|
| 2293 | - $props = []; |
|
| 2294 | - if ($oof->oofstate == SYNC_SETTINGSOOF_GLOBAL || $oof->oofstate == SYNC_SETTINGSOOF_TIMEBASED) { |
|
| 2295 | - $props[PR_EC_OUTOFOFFICE] = true; |
|
| 2296 | - foreach ($oof->oofmessage as $oofmessage) { |
|
| 2297 | - if (isset($oofmessage->appliesToInternal)) { |
|
| 2298 | - $props[PR_EC_OUTOFOFFICE_MSG] = isset($oofmessage->replymessage) ? u2w($oofmessage->replymessage) : ""; |
|
| 2299 | - $props[PR_EC_OUTOFOFFICE_SUBJECT] = "Out of office"; |
|
| 2300 | - } |
|
| 2301 | - } |
|
| 2302 | - if ($oof->oofstate == SYNC_SETTINGSOOF_TIMEBASED) { |
|
| 2303 | - if (isset($oof->starttime, $oof->endtime)) { |
|
| 2304 | - $props[PR_EC_OUTOFOFFICE_FROM] = $oof->starttime; |
|
| 2305 | - $props[PR_EC_OUTOFOFFICE_UNTIL] = $oof->endtime; |
|
| 2306 | - } |
|
| 2307 | - elseif (isset($oof->starttime) || isset($oof->endtime)) { |
|
| 2308 | - $oof->Status = SYNC_SETTINGSSTATUS_PROTOCOLLERROR; |
|
| 2309 | - } |
|
| 2310 | - } |
|
| 2311 | - else { |
|
| 2312 | - $deleteProps = [PR_EC_OUTOFOFFICE_FROM, PR_EC_OUTOFOFFICE_UNTIL]; |
|
| 2313 | - } |
|
| 2314 | - } |
|
| 2315 | - elseif ($oof->oofstate == SYNC_SETTINGSOOF_DISABLED) { |
|
| 2316 | - $props[PR_EC_OUTOFOFFICE] = false; |
|
| 2317 | - $deleteProps = [PR_EC_OUTOFOFFICE_FROM, PR_EC_OUTOFOFFICE_UNTIL]; |
|
| 2318 | - } |
|
| 2319 | - |
|
| 2320 | - if (!empty($props)) { |
|
| 2321 | - @mapi_setprops($this->defaultstore, $props); |
|
| 2322 | - $result = mapi_last_hresult(); |
|
| 2323 | - if ($result != NOERROR) { |
|
| 2324 | - SLog::Write(LOGLEVEL_ERROR, sprintf("Grommunio->settingsOofSet(): Setting oof information failed (%X)", $result)); |
|
| 2325 | - |
|
| 2326 | - return false; |
|
| 2327 | - } |
|
| 2328 | - } |
|
| 2329 | - |
|
| 2330 | - if (!empty($deleteProps)) { |
|
| 2331 | - @mapi_deleteprops($this->defaultstore, $deleteProps); |
|
| 2332 | - } |
|
| 2333 | - |
|
| 2334 | - return true; |
|
| 2335 | - } |
|
| 2336 | - |
|
| 2337 | - /** |
|
| 2338 | - * Gets the user's email address from server. |
|
| 2339 | - * |
|
| 2340 | - * @param SyncObject $userinformation |
|
| 2341 | - */ |
|
| 2342 | - private function settingsUserInformation(&$userinformation) { |
|
| 2343 | - if (!isset($this->defaultstore) || !isset($this->mainUser)) { |
|
| 2344 | - SLog::Write(LOGLEVEL_ERROR, "Grommunio->settingsUserInformation(): The store or user are not available for getting user information"); |
|
| 2345 | - |
|
| 2346 | - return false; |
|
| 2347 | - } |
|
| 2348 | - $user = nsp_getuserinfo($this->mainUser); |
|
| 2349 | - if ($user != false) { |
|
| 2350 | - $userinformation->Status = SYNC_SETTINGSSTATUS_USERINFO_SUCCESS; |
|
| 2351 | - if (Request::GetProtocolVersion() >= 14.1) { |
|
| 2352 | - $account = new SyncAccount(); |
|
| 2353 | - $emailaddresses = new SyncEmailAddresses(); |
|
| 2354 | - $emailaddresses->smtpaddress[] = $user["primary_email"]; |
|
| 2355 | - $emailaddresses->primarysmtpaddress = $user["primary_email"]; |
|
| 2356 | - $account->emailaddresses = $emailaddresses; |
|
| 2357 | - $userinformation->accounts[] = $account; |
|
| 2358 | - } |
|
| 2359 | - else { |
|
| 2360 | - $userinformation->emailaddresses[] = $user["primary_email"]; |
|
| 2361 | - } |
|
| 2362 | - |
|
| 2363 | - return true; |
|
| 2364 | - } |
|
| 2365 | - SLog::Write(LOGLEVEL_ERROR, sprintf("Grommunio->settingsUserInformation(): Getting user information failed: nsp_getuserinfo(%X)", mapi_last_hresult())); |
|
| 2366 | - |
|
| 2367 | - return false; |
|
| 2368 | - } |
|
| 2369 | - |
|
| 2370 | - /** |
|
| 2371 | - * Gets the rights management templates from the server. |
|
| 2372 | - * |
|
| 2373 | - * @param SyncObject $rmTemplates |
|
| 2374 | - */ |
|
| 2375 | - private function settingsRightsManagementTemplates(&$rmTemplates) { |
|
| 2376 | - /* Currently there is no information rights management feature in |
|
| 2057 | + /** |
|
| 2058 | + * Returns a hash representing changes in the hierarchy of the main user. |
|
| 2059 | + * It changes if a folder is added, renamed or deleted. |
|
| 2060 | + * |
|
| 2061 | + * @return string |
|
| 2062 | + */ |
|
| 2063 | + private function getHierarchyHash() { |
|
| 2064 | + $rootfolder = mapi_msgstore_openentry($this->defaultstore); |
|
| 2065 | + $hierarchy = mapi_folder_gethierarchytable($rootfolder, CONVENIENT_DEPTH); |
|
| 2066 | + |
|
| 2067 | + return md5(serialize(mapi_table_queryallrows($hierarchy, [PR_DISPLAY_NAME, PR_PARENT_ENTRYID]))); |
|
| 2068 | + } |
|
| 2069 | + |
|
| 2070 | + /** |
|
| 2071 | + * Advises a store to the changes sink. |
|
| 2072 | + * |
|
| 2073 | + * @param mapistore $store store to be advised |
|
| 2074 | + * |
|
| 2075 | + * @return bool |
|
| 2076 | + */ |
|
| 2077 | + private function adviseStoreToSink($store) { |
|
| 2078 | + // check if we already advised the store |
|
| 2079 | + if (!in_array($store, $this->changesSinkStores)) { |
|
| 2080 | + mapi_msgstore_advise($store, null, fnevObjectModified | fnevObjectCreated | fnevObjectMoved | fnevObjectDeleted, $this->changesSink); |
|
| 2081 | + |
|
| 2082 | + if (mapi_last_hresult()) { |
|
| 2083 | + SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->adviseStoreToSink(): failed to advised store '%s' with code 0x%X. Polling will be performed.", $store, mapi_last_hresult())); |
|
| 2084 | + |
|
| 2085 | + return false; |
|
| 2086 | + } |
|
| 2087 | + |
|
| 2088 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->adviseStoreToSink(): advised store '%s'", $store)); |
|
| 2089 | + $this->changesSinkStores[] = $store; |
|
| 2090 | + } |
|
| 2091 | + |
|
| 2092 | + return true; |
|
| 2093 | + } |
|
| 2094 | + |
|
| 2095 | + /** |
|
| 2096 | + * Open the store marked with PR_DEFAULT_STORE = TRUE |
|
| 2097 | + * if $return_public is set, the public store is opened. |
|
| 2098 | + * |
|
| 2099 | + * @param string $user User which store should be opened |
|
| 2100 | + * |
|
| 2101 | + * @return bool |
|
| 2102 | + */ |
|
| 2103 | + private function openMessageStore($user) { |
|
| 2104 | + // During PING requests the operations store has to be switched constantly |
|
| 2105 | + // the cache prevents the same store opened several times |
|
| 2106 | + if (isset($this->storeCache[$user])) { |
|
| 2107 | + return $this->storeCache[$user]; |
|
| 2108 | + } |
|
| 2109 | + |
|
| 2110 | + $entryid = false; |
|
| 2111 | + $return_public = false; |
|
| 2112 | + |
|
| 2113 | + if (strtoupper($user) == 'SYSTEM') { |
|
| 2114 | + $return_public = true; |
|
| 2115 | + } |
|
| 2116 | + |
|
| 2117 | + // loop through the storestable if authenticated user of public folder |
|
| 2118 | + if ($user == $this->mainUser || $return_public === true) { |
|
| 2119 | + // Find the default store |
|
| 2120 | + $storestables = mapi_getmsgstorestable($this->session); |
|
| 2121 | + $result = mapi_last_hresult(); |
|
| 2122 | + |
|
| 2123 | + if ($result == NOERROR) { |
|
| 2124 | + $rows = mapi_table_queryallrows($storestables, [PR_ENTRYID, PR_DEFAULT_STORE, PR_MDB_PROVIDER]); |
|
| 2125 | + |
|
| 2126 | + foreach ($rows as $row) { |
|
| 2127 | + if (!$return_public && isset($row[PR_DEFAULT_STORE]) && $row[PR_DEFAULT_STORE] == true) { |
|
| 2128 | + $entryid = $row[PR_ENTRYID]; |
|
| 2129 | + |
|
| 2130 | + break; |
|
| 2131 | + } |
|
| 2132 | + if ($return_public && isset($row[PR_MDB_PROVIDER]) && $row[PR_MDB_PROVIDER] == ZARAFA_STORE_PUBLIC_GUID) { |
|
| 2133 | + $entryid = $row[PR_ENTRYID]; |
|
| 2134 | + |
|
| 2135 | + break; |
|
| 2136 | + } |
|
| 2137 | + } |
|
| 2138 | + } |
|
| 2139 | + } |
|
| 2140 | + else { |
|
| 2141 | + $entryid = @mapi_msgstore_createentryid($this->defaultstore, $user); |
|
| 2142 | + } |
|
| 2143 | + |
|
| 2144 | + if ($entryid) { |
|
| 2145 | + $store = @mapi_openmsgstore($this->session, $entryid); |
|
| 2146 | + |
|
| 2147 | + if (!$store) { |
|
| 2148 | + SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->openMessageStore('%s'): Could not open store", $user)); |
|
| 2149 | + |
|
| 2150 | + return false; |
|
| 2151 | + } |
|
| 2152 | + |
|
| 2153 | + // add this store to the cache |
|
| 2154 | + if (!isset($this->storeCache[$user])) { |
|
| 2155 | + $this->storeCache[$user] = $store; |
|
| 2156 | + } |
|
| 2157 | + |
|
| 2158 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->openMessageStore('%s'): Found '%s' store: '%s'", $user, (($return_public) ? 'PUBLIC' : 'DEFAULT'), $store)); |
|
| 2159 | + |
|
| 2160 | + return $store; |
|
| 2161 | + } |
|
| 2162 | + |
|
| 2163 | + SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->openMessageStore('%s'): No store found for this user", $user)); |
|
| 2164 | + |
|
| 2165 | + return false; |
|
| 2166 | + } |
|
| 2167 | + |
|
| 2168 | + /** |
|
| 2169 | + * Checks if the logged in user has secretary permissions on a folder. |
|
| 2170 | + * |
|
| 2171 | + * @param resource $store |
|
| 2172 | + * @param string $folderid |
|
| 2173 | + * @param mixed $entryid |
|
| 2174 | + * |
|
| 2175 | + * @return bool |
|
| 2176 | + */ |
|
| 2177 | + public function HasSecretaryACLs($store, $folderid, $entryid = false) { |
|
| 2178 | + if (!$entryid) { |
|
| 2179 | + $entryid = mapi_msgstore_entryidfromsourcekey($store, hex2bin($folderid)); |
|
| 2180 | + if (!$entryid) { |
|
| 2181 | + SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->HasSecretaryACLs(): error, no entryid resolved for %s on store %s", $folderid, $store)); |
|
| 2182 | + |
|
| 2183 | + return false; |
|
| 2184 | + } |
|
| 2185 | + } |
|
| 2186 | + |
|
| 2187 | + $folder = mapi_msgstore_openentry($store, $entryid); |
|
| 2188 | + if (!$folder) { |
|
| 2189 | + SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->HasSecretaryACLs(): error, could not open folder with entryid %s on store %s", bin2hex($entryid), $store)); |
|
| 2190 | + |
|
| 2191 | + return false; |
|
| 2192 | + } |
|
| 2193 | + |
|
| 2194 | + $props = mapi_getprops($folder, [PR_RIGHTS]); |
|
| 2195 | + if (isset($props[PR_RIGHTS]) && |
|
| 2196 | + ($props[PR_RIGHTS] & ecRightsReadAny) && |
|
| 2197 | + ($props[PR_RIGHTS] & ecRightsCreate) && |
|
| 2198 | + ($props[PR_RIGHTS] & ecRightsEditOwned) && |
|
| 2199 | + ($props[PR_RIGHTS] & ecRightsDeleteOwned) && |
|
| 2200 | + ($props[PR_RIGHTS] & ecRightsEditAny) && |
|
| 2201 | + ($props[PR_RIGHTS] & ecRightsDeleteAny) && |
|
| 2202 | + ($props[PR_RIGHTS] & ecRightsFolderVisible)) { |
|
| 2203 | + return true; |
|
| 2204 | + } |
|
| 2205 | + |
|
| 2206 | + return false; |
|
| 2207 | + } |
|
| 2208 | + |
|
| 2209 | + /** |
|
| 2210 | + * The meta function for out of office settings. |
|
| 2211 | + * |
|
| 2212 | + * @param SyncObject $oof |
|
| 2213 | + */ |
|
| 2214 | + private function settingsOOF(&$oof) { |
|
| 2215 | + // if oof state is set it must be set of oof and get otherwise |
|
| 2216 | + if (isset($oof->oofstate)) { |
|
| 2217 | + $this->settingsOofSet($oof); |
|
| 2218 | + } |
|
| 2219 | + else { |
|
| 2220 | + $this->settingsOofGet($oof); |
|
| 2221 | + } |
|
| 2222 | + } |
|
| 2223 | + |
|
| 2224 | + /** |
|
| 2225 | + * Gets the out of office settings. |
|
| 2226 | + * |
|
| 2227 | + * @param SyncObject $oof |
|
| 2228 | + */ |
|
| 2229 | + private function settingsOofGet(&$oof) { |
|
| 2230 | + $oofprops = mapi_getprops($this->defaultstore, [PR_EC_OUTOFOFFICE, PR_EC_OUTOFOFFICE_MSG, PR_EC_OUTOFOFFICE_SUBJECT, PR_EC_OUTOFOFFICE_FROM, PR_EC_OUTOFOFFICE_UNTIL]); |
|
| 2231 | + $oof->oofstate = SYNC_SETTINGSOOF_DISABLED; |
|
| 2232 | + $oof->Status = SYNC_SETTINGSSTATUS_SUCCESS; |
|
| 2233 | + if ($oofprops != false) { |
|
| 2234 | + $oof->oofstate = isset($oofprops[PR_EC_OUTOFOFFICE]) ? ($oofprops[PR_EC_OUTOFOFFICE] ? SYNC_SETTINGSOOF_GLOBAL : SYNC_SETTINGSOOF_DISABLED) : SYNC_SETTINGSOOF_DISABLED; |
|
| 2235 | + // TODO external and external unknown |
|
| 2236 | + $oofmessage = new SyncOOFMessage(); |
|
| 2237 | + $oofmessage->appliesToInternal = ""; |
|
| 2238 | + $oofmessage->enabled = $oof->oofstate; |
|
| 2239 | + $oofmessage->replymessage = (isset($oofprops[PR_EC_OUTOFOFFICE_MSG])) ? w2u($oofprops[PR_EC_OUTOFOFFICE_MSG]) : ""; |
|
| 2240 | + $oofmessage->bodytype = $oof->bodytype; |
|
| 2241 | + unset($oofmessage->appliesToExternal, $oofmessage->appliesToExternalUnknown); |
|
| 2242 | + $oof->oofmessage[] = $oofmessage; |
|
| 2243 | + |
|
| 2244 | + // check whether time based out of office is set |
|
| 2245 | + if ($oof->oofstate == SYNC_SETTINGSOOF_GLOBAL && isset($oofprops[PR_EC_OUTOFOFFICE_FROM], $oofprops[PR_EC_OUTOFOFFICE_UNTIL])) { |
|
| 2246 | + $now = time(); |
|
| 2247 | + if ($now > $oofprops[PR_EC_OUTOFOFFICE_FROM] && $now > $oofprops[PR_EC_OUTOFOFFICE_UNTIL]) { |
|
| 2248 | + // Out of office is set but the date is in the past. Set the state to disabled. |
|
| 2249 | + // @see https://jira.z-hub.io/browse/ZP-1188 for details |
|
| 2250 | + $oof->oofstate = SYNC_SETTINGSOOF_DISABLED; |
|
| 2251 | + @mapi_setprops($this->defaultstore, [PR_EC_OUTOFOFFICE => false]); |
|
| 2252 | + @mapi_deleteprops($this->defaultstore, [PR_EC_OUTOFOFFICE_FROM, PR_EC_OUTOFOFFICE_UNTIL]); |
|
| 2253 | + SLog::Write(LOGLEVEL_INFO, "Grommunio->settingsOofGet(): Out of office is set but the from and until are in the past. Disabling out of office."); |
|
| 2254 | + } |
|
| 2255 | + elseif ($oofprops[PR_EC_OUTOFOFFICE_FROM] < $oofprops[PR_EC_OUTOFOFFICE_UNTIL]) { |
|
| 2256 | + $oof->oofstate = SYNC_SETTINGSOOF_TIMEBASED; |
|
| 2257 | + $oof->starttime = $oofprops[PR_EC_OUTOFOFFICE_FROM]; |
|
| 2258 | + $oof->endtime = $oofprops[PR_EC_OUTOFOFFICE_UNTIL]; |
|
| 2259 | + } |
|
| 2260 | + else { |
|
| 2261 | + SLog::Write(LOGLEVEL_WARN, sprintf( |
|
| 2262 | + "Grommunio->settingsOofGet(): Time based out of office set but end time ('%s') is before startime ('%s').", |
|
| 2263 | + date("Y-m-d H:i:s", $oofprops[PR_EC_OUTOFOFFICE_FROM]), |
|
| 2264 | + date("Y-m-d H:i:s", $oofprops[PR_EC_OUTOFOFFICE_UNTIL]) |
|
| 2265 | + )); |
|
| 2266 | + $oof->Status = SYNC_SETTINGSSTATUS_PROTOCOLLERROR; |
|
| 2267 | + } |
|
| 2268 | + } |
|
| 2269 | + elseif ($oof->oofstate == SYNC_SETTINGSOOF_GLOBAL && (isset($oofprops[PR_EC_OUTOFOFFICE_FROM]) || isset($oofprops[PR_EC_OUTOFOFFICE_UNTIL]))) { |
|
| 2270 | + SLog::Write(LOGLEVEL_WARN, sprintf( |
|
| 2271 | + "Grommunio->settingsOofGet(): Time based out of office set but either start time ('%s') or end time ('%s') is missing.", |
|
| 2272 | + (isset($oofprops[PR_EC_OUTOFOFFICE_FROM]) ? date("Y-m-d H:i:s", $oofprops[PR_EC_OUTOFOFFICE_FROM]) : 'empty'), |
|
| 2273 | + (isset($oofprops[PR_EC_OUTOFOFFICE_UNTIL]) ? date("Y-m-d H:i:s", $oofprops[PR_EC_OUTOFOFFICE_UNTIL]) : 'empty') |
|
| 2274 | + )); |
|
| 2275 | + $oof->Status = SYNC_SETTINGSSTATUS_PROTOCOLLERROR; |
|
| 2276 | + } |
|
| 2277 | + } |
|
| 2278 | + else { |
|
| 2279 | + SLog::Write(LOGLEVEL_WARN, "Grommunio->Unable to get out of office information"); |
|
| 2280 | + } |
|
| 2281 | + |
|
| 2282 | + // unset body type for oof in order not to stream it |
|
| 2283 | + unset($oof->bodytype); |
|
| 2284 | + } |
|
| 2285 | + |
|
| 2286 | + /** |
|
| 2287 | + * Sets the out of office settings. |
|
| 2288 | + * |
|
| 2289 | + * @param SyncObject $oof |
|
| 2290 | + */ |
|
| 2291 | + private function settingsOofSet(&$oof) { |
|
| 2292 | + $oof->Status = SYNC_SETTINGSSTATUS_SUCCESS; |
|
| 2293 | + $props = []; |
|
| 2294 | + if ($oof->oofstate == SYNC_SETTINGSOOF_GLOBAL || $oof->oofstate == SYNC_SETTINGSOOF_TIMEBASED) { |
|
| 2295 | + $props[PR_EC_OUTOFOFFICE] = true; |
|
| 2296 | + foreach ($oof->oofmessage as $oofmessage) { |
|
| 2297 | + if (isset($oofmessage->appliesToInternal)) { |
|
| 2298 | + $props[PR_EC_OUTOFOFFICE_MSG] = isset($oofmessage->replymessage) ? u2w($oofmessage->replymessage) : ""; |
|
| 2299 | + $props[PR_EC_OUTOFOFFICE_SUBJECT] = "Out of office"; |
|
| 2300 | + } |
|
| 2301 | + } |
|
| 2302 | + if ($oof->oofstate == SYNC_SETTINGSOOF_TIMEBASED) { |
|
| 2303 | + if (isset($oof->starttime, $oof->endtime)) { |
|
| 2304 | + $props[PR_EC_OUTOFOFFICE_FROM] = $oof->starttime; |
|
| 2305 | + $props[PR_EC_OUTOFOFFICE_UNTIL] = $oof->endtime; |
|
| 2306 | + } |
|
| 2307 | + elseif (isset($oof->starttime) || isset($oof->endtime)) { |
|
| 2308 | + $oof->Status = SYNC_SETTINGSSTATUS_PROTOCOLLERROR; |
|
| 2309 | + } |
|
| 2310 | + } |
|
| 2311 | + else { |
|
| 2312 | + $deleteProps = [PR_EC_OUTOFOFFICE_FROM, PR_EC_OUTOFOFFICE_UNTIL]; |
|
| 2313 | + } |
|
| 2314 | + } |
|
| 2315 | + elseif ($oof->oofstate == SYNC_SETTINGSOOF_DISABLED) { |
|
| 2316 | + $props[PR_EC_OUTOFOFFICE] = false; |
|
| 2317 | + $deleteProps = [PR_EC_OUTOFOFFICE_FROM, PR_EC_OUTOFOFFICE_UNTIL]; |
|
| 2318 | + } |
|
| 2319 | + |
|
| 2320 | + if (!empty($props)) { |
|
| 2321 | + @mapi_setprops($this->defaultstore, $props); |
|
| 2322 | + $result = mapi_last_hresult(); |
|
| 2323 | + if ($result != NOERROR) { |
|
| 2324 | + SLog::Write(LOGLEVEL_ERROR, sprintf("Grommunio->settingsOofSet(): Setting oof information failed (%X)", $result)); |
|
| 2325 | + |
|
| 2326 | + return false; |
|
| 2327 | + } |
|
| 2328 | + } |
|
| 2329 | + |
|
| 2330 | + if (!empty($deleteProps)) { |
|
| 2331 | + @mapi_deleteprops($this->defaultstore, $deleteProps); |
|
| 2332 | + } |
|
| 2333 | + |
|
| 2334 | + return true; |
|
| 2335 | + } |
|
| 2336 | + |
|
| 2337 | + /** |
|
| 2338 | + * Gets the user's email address from server. |
|
| 2339 | + * |
|
| 2340 | + * @param SyncObject $userinformation |
|
| 2341 | + */ |
|
| 2342 | + private function settingsUserInformation(&$userinformation) { |
|
| 2343 | + if (!isset($this->defaultstore) || !isset($this->mainUser)) { |
|
| 2344 | + SLog::Write(LOGLEVEL_ERROR, "Grommunio->settingsUserInformation(): The store or user are not available for getting user information"); |
|
| 2345 | + |
|
| 2346 | + return false; |
|
| 2347 | + } |
|
| 2348 | + $user = nsp_getuserinfo($this->mainUser); |
|
| 2349 | + if ($user != false) { |
|
| 2350 | + $userinformation->Status = SYNC_SETTINGSSTATUS_USERINFO_SUCCESS; |
|
| 2351 | + if (Request::GetProtocolVersion() >= 14.1) { |
|
| 2352 | + $account = new SyncAccount(); |
|
| 2353 | + $emailaddresses = new SyncEmailAddresses(); |
|
| 2354 | + $emailaddresses->smtpaddress[] = $user["primary_email"]; |
|
| 2355 | + $emailaddresses->primarysmtpaddress = $user["primary_email"]; |
|
| 2356 | + $account->emailaddresses = $emailaddresses; |
|
| 2357 | + $userinformation->accounts[] = $account; |
|
| 2358 | + } |
|
| 2359 | + else { |
|
| 2360 | + $userinformation->emailaddresses[] = $user["primary_email"]; |
|
| 2361 | + } |
|
| 2362 | + |
|
| 2363 | + return true; |
|
| 2364 | + } |
|
| 2365 | + SLog::Write(LOGLEVEL_ERROR, sprintf("Grommunio->settingsUserInformation(): Getting user information failed: nsp_getuserinfo(%X)", mapi_last_hresult())); |
|
| 2366 | + |
|
| 2367 | + return false; |
|
| 2368 | + } |
|
| 2369 | + |
|
| 2370 | + /** |
|
| 2371 | + * Gets the rights management templates from the server. |
|
| 2372 | + * |
|
| 2373 | + * @param SyncObject $rmTemplates |
|
| 2374 | + */ |
|
| 2375 | + private function settingsRightsManagementTemplates(&$rmTemplates) { |
|
| 2376 | + /* Currently there is no information rights management feature in |
|
| 2377 | 2377 | * the grommunio backend, so just return the status and empty |
| 2378 | 2378 | * SyncRightsManagementTemplates tag. |
| 2379 | 2379 | * Once it's available, it would be something like: |
@@ -2384,588 +2384,588 @@ discard block |
||
| 2384 | 2384 | $rmTemplate->description = "What does the template do. E.g. it disables forward and reply."; |
| 2385 | 2385 | $rmTemplates->rmtemplates[] = $rmTemplate; |
| 2386 | 2386 | */ |
| 2387 | - $rmTemplates->Status = SYNC_COMMONSTATUS_IRMFEATUREDISABLED; |
|
| 2388 | - $rmTemplates->rmtemplates = []; |
|
| 2389 | - } |
|
| 2390 | - |
|
| 2391 | - /** |
|
| 2392 | - * Sets the importance and priority of a message from a RFC822 message headers. |
|
| 2393 | - * |
|
| 2394 | - * @param int $xPriority |
|
| 2395 | - * @param array $mapiprops |
|
| 2396 | - * @param mixed $sendMailProps |
|
| 2397 | - */ |
|
| 2398 | - private function getImportanceAndPriority($xPriority, &$mapiprops, $sendMailProps) { |
|
| 2399 | - switch ($xPriority) { |
|
| 2400 | - case 1: |
|
| 2401 | - case 2: |
|
| 2402 | - $priority = PRIO_URGENT; |
|
| 2403 | - $importance = IMPORTANCE_HIGH; |
|
| 2404 | - break; |
|
| 2405 | - |
|
| 2406 | - case 4: |
|
| 2407 | - case 5: |
|
| 2408 | - $priority = PRIO_NONURGENT; |
|
| 2409 | - $importance = IMPORTANCE_LOW; |
|
| 2410 | - break; |
|
| 2411 | - |
|
| 2412 | - case 3: |
|
| 2413 | - default: |
|
| 2414 | - $priority = PRIO_NORMAL; |
|
| 2415 | - $importance = IMPORTANCE_NORMAL; |
|
| 2416 | - break; |
|
| 2417 | - } |
|
| 2418 | - $mapiprops[$sendMailProps["importance"]] = $importance; |
|
| 2419 | - $mapiprops[$sendMailProps["priority"]] = $priority; |
|
| 2420 | - } |
|
| 2421 | - |
|
| 2422 | - /** |
|
| 2423 | - * Copies attachments from one message to another. |
|
| 2424 | - * |
|
| 2425 | - * @param MAPIMessage $toMessage |
|
| 2426 | - * @param MAPIMessage $fromMessage |
|
| 2427 | - */ |
|
| 2428 | - private function copyAttachments(&$toMessage, $fromMessage) { |
|
| 2429 | - $attachtable = mapi_message_getattachmenttable($fromMessage); |
|
| 2430 | - $rows = mapi_table_queryallrows($attachtable, [PR_ATTACH_NUM]); |
|
| 2431 | - |
|
| 2432 | - foreach ($rows as $row) { |
|
| 2433 | - if (isset($row[PR_ATTACH_NUM])) { |
|
| 2434 | - $attach = mapi_message_openattach($fromMessage, $row[PR_ATTACH_NUM]); |
|
| 2435 | - $newattach = mapi_message_createattach($toMessage); |
|
| 2436 | - mapi_copyto($attach, [], [], $newattach, 0); |
|
| 2437 | - mapi_savechanges($newattach); |
|
| 2438 | - } |
|
| 2439 | - } |
|
| 2440 | - } |
|
| 2441 | - |
|
| 2442 | - /** |
|
| 2443 | - * Function will create a search folder in FINDER_ROOT folder |
|
| 2444 | - * if folder exists then it will open it. |
|
| 2445 | - * |
|
| 2446 | - * @see createSearchFolder($store, $openIfExists = true) function in the webaccess |
|
| 2447 | - * |
|
| 2448 | - * @return mapiFolderObject $folder created search folder |
|
| 2449 | - */ |
|
| 2450 | - private function getSearchFolder() { |
|
| 2451 | - // create new or open existing search folder |
|
| 2452 | - $searchFolderRoot = $this->getSearchFoldersRoot($this->store); |
|
| 2453 | - if ($searchFolderRoot === false) { |
|
| 2454 | - // error in finding search root folder |
|
| 2455 | - // or store doesn't support search folders |
|
| 2456 | - return false; |
|
| 2457 | - } |
|
| 2458 | - |
|
| 2459 | - $searchFolder = $this->createSearchFolder($searchFolderRoot); |
|
| 2460 | - |
|
| 2461 | - if ($searchFolder !== false && mapi_last_hresult() == NOERROR) { |
|
| 2462 | - return $searchFolder; |
|
| 2463 | - } |
|
| 2464 | - |
|
| 2465 | - return false; |
|
| 2466 | - } |
|
| 2467 | - |
|
| 2468 | - /** |
|
| 2469 | - * Function will open FINDER_ROOT folder in root container |
|
| 2470 | - * public folder's don't have FINDER_ROOT folder. |
|
| 2471 | - * |
|
| 2472 | - * @see getSearchFoldersRoot($store) function in the webaccess |
|
| 2473 | - * |
|
| 2474 | - * @return mapiFolderObject root folder for search folders |
|
| 2475 | - */ |
|
| 2476 | - private function getSearchFoldersRoot() { |
|
| 2477 | - // check if we can create search folders |
|
| 2478 | - $storeProps = mapi_getprops($this->store, [PR_STORE_SUPPORT_MASK, PR_FINDER_ENTRYID]); |
|
| 2479 | - if (($storeProps[PR_STORE_SUPPORT_MASK] & STORE_SEARCH_OK) != STORE_SEARCH_OK) { |
|
| 2480 | - SLog::Write(LOGLEVEL_WARN, "Grommunio->getSearchFoldersRoot(): Store doesn't support search folders. Public store doesn't have FINDER_ROOT folder"); |
|
| 2481 | - |
|
| 2482 | - return false; |
|
| 2483 | - } |
|
| 2484 | - |
|
| 2485 | - // open search folders root |
|
| 2486 | - $searchRootFolder = mapi_msgstore_openentry($this->store, $storeProps[PR_FINDER_ENTRYID]); |
|
| 2487 | - if (mapi_last_hresult() != NOERROR) { |
|
| 2488 | - SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->getSearchFoldersRoot(): Unable to open search folder (0x%X)", mapi_last_hresult())); |
|
| 2489 | - |
|
| 2490 | - return false; |
|
| 2491 | - } |
|
| 2492 | - |
|
| 2493 | - return $searchRootFolder; |
|
| 2494 | - } |
|
| 2495 | - |
|
| 2496 | - /** |
|
| 2497 | - * Creates a search folder if it not exists or opens an existing one |
|
| 2498 | - * and returns it. |
|
| 2499 | - * |
|
| 2500 | - * @param mapiFolderObject $searchFolderRoot |
|
| 2501 | - * |
|
| 2502 | - * @return mapiFolderObject |
|
| 2503 | - */ |
|
| 2504 | - private function createSearchFolder($searchFolderRoot) { |
|
| 2505 | - $folderName = "grommunio-sync Search Folder " . @getmypid(); |
|
| 2506 | - $searchFolders = mapi_folder_gethierarchytable($searchFolderRoot); |
|
| 2507 | - $restriction = [ |
|
| 2508 | - RES_CONTENT, |
|
| 2509 | - [ |
|
| 2510 | - FUZZYLEVEL => FL_PREFIX, |
|
| 2511 | - ULPROPTAG => PR_DISPLAY_NAME, |
|
| 2512 | - VALUE => [PR_DISPLAY_NAME => $folderName], |
|
| 2513 | - ], |
|
| 2514 | - ]; |
|
| 2515 | - // restrict the hierarchy to the grommunio-sync search folder only |
|
| 2516 | - mapi_table_restrict($searchFolders, $restriction); |
|
| 2517 | - if (mapi_table_getrowcount($searchFolders)) { |
|
| 2518 | - $searchFolder = mapi_table_queryrows($searchFolders, [PR_ENTRYID], 0, 1); |
|
| 2519 | - |
|
| 2520 | - return mapi_msgstore_openentry($this->store, $searchFolder[0][PR_ENTRYID]); |
|
| 2521 | - } |
|
| 2522 | - |
|
| 2523 | - return mapi_folder_createfolder($searchFolderRoot, $folderName, null, 0, FOLDER_SEARCH); |
|
| 2524 | - } |
|
| 2525 | - |
|
| 2526 | - /** |
|
| 2527 | - * Creates a search restriction. |
|
| 2528 | - * |
|
| 2529 | - * @param ContentParameter $cpo |
|
| 2530 | - * |
|
| 2531 | - * @return array |
|
| 2532 | - */ |
|
| 2533 | - private function getSearchRestriction($cpo) { |
|
| 2534 | - $searchText = $cpo->GetSearchFreeText(); |
|
| 2535 | - |
|
| 2536 | - $searchGreater = strtotime($cpo->GetSearchValueGreater()); |
|
| 2537 | - $searchLess = strtotime($cpo->GetSearchValueLess()); |
|
| 2538 | - |
|
| 2539 | - if (version_compare(phpversion(), '5.3.4') < 0) { |
|
| 2540 | - SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->getSearchRestriction(): Your system's PHP version (%s) might not correctly process unicode strings. Search containing such characters might not return correct results. It is recommended to update to at least PHP 5.3.4. See ZP-541 for more information.", phpversion())); |
|
| 2541 | - } |
|
| 2542 | - // split the search on whitespache and look for every word |
|
| 2543 | - $searchText = preg_split("/\\W+/u", $searchText); |
|
| 2544 | - $searchProps = [PR_BODY, PR_SUBJECT, PR_DISPLAY_TO, PR_DISPLAY_CC, PR_SENDER_NAME, PR_SENDER_EMAIL_ADDRESS, PR_SENT_REPRESENTING_NAME, PR_SENT_REPRESENTING_EMAIL_ADDRESS]; |
|
| 2545 | - $resAnd = []; |
|
| 2546 | - foreach ($searchText as $term) { |
|
| 2547 | - $resOr = []; |
|
| 2548 | - |
|
| 2549 | - foreach ($searchProps as $property) { |
|
| 2550 | - array_push( |
|
| 2551 | - $resOr, |
|
| 2552 | - [RES_CONTENT, |
|
| 2553 | - [ |
|
| 2554 | - FUZZYLEVEL => FL_SUBSTRING | FL_IGNORECASE, |
|
| 2555 | - ULPROPTAG => $property, |
|
| 2556 | - VALUE => u2w($term), |
|
| 2557 | - ], |
|
| 2558 | - ] |
|
| 2559 | - ); |
|
| 2560 | - } |
|
| 2561 | - array_push($resAnd, [RES_OR, $resOr]); |
|
| 2562 | - } |
|
| 2563 | - |
|
| 2564 | - // add time range restrictions |
|
| 2565 | - if ($searchGreater) { |
|
| 2566 | - array_push($resAnd, [RES_PROPERTY, [RELOP => RELOP_GE, ULPROPTAG => PR_MESSAGE_DELIVERY_TIME, VALUE => [PR_MESSAGE_DELIVERY_TIME => $searchGreater]]]); // RES_AND; |
|
| 2567 | - } |
|
| 2568 | - if ($searchLess) { |
|
| 2569 | - array_push($resAnd, [RES_PROPERTY, [RELOP => RELOP_LE, ULPROPTAG => PR_MESSAGE_DELIVERY_TIME, VALUE => [PR_MESSAGE_DELIVERY_TIME => $searchLess]]]); |
|
| 2570 | - } |
|
| 2571 | - |
|
| 2572 | - return [RES_AND, $resAnd]; |
|
| 2573 | - } |
|
| 2574 | - |
|
| 2575 | - /** |
|
| 2576 | - * Resolve recipient based on his email address. |
|
| 2577 | - * |
|
| 2578 | - * @param string $to |
|
| 2579 | - * @param int $maxAmbiguousRecipients |
|
| 2580 | - * @param bool $expandDistlist |
|
| 2581 | - * |
|
| 2582 | - * @return bool|SyncResolveRecipient |
|
| 2583 | - */ |
|
| 2584 | - private function resolveRecipient($to, $maxAmbiguousRecipients, $expandDistlist = true) { |
|
| 2585 | - $recipient = $this->resolveRecipientGAL($to, $maxAmbiguousRecipients, $expandDistlist); |
|
| 2586 | - |
|
| 2587 | - if ($recipient !== false) { |
|
| 2588 | - return $recipient; |
|
| 2589 | - } |
|
| 2590 | - |
|
| 2591 | - $recipient = $this->resolveRecipientContact($to, $maxAmbiguousRecipients); |
|
| 2592 | - |
|
| 2593 | - if ($recipient !== false) { |
|
| 2594 | - return $recipient; |
|
| 2595 | - } |
|
| 2596 | - |
|
| 2597 | - return false; |
|
| 2598 | - } |
|
| 2599 | - |
|
| 2600 | - /** |
|
| 2601 | - * Resolves recipient from the GAL and gets his certificates. |
|
| 2602 | - * |
|
| 2603 | - * @param string $to |
|
| 2604 | - * @param int $maxAmbiguousRecipients |
|
| 2605 | - * @param bool $expandDistlist |
|
| 2606 | - * |
|
| 2607 | - * @return array|bool |
|
| 2608 | - */ |
|
| 2609 | - private function resolveRecipientGAL($to, $maxAmbiguousRecipients, $expandDistlist = true) { |
|
| 2610 | - SLog::Write(LOGLEVEL_WBXML, sprintf("Grommunio->resolveRecipientGAL(): Resolving recipient '%s' in GAL", $to)); |
|
| 2611 | - $addrbook = $this->getAddressbook(); |
|
| 2612 | - // FIXME: create a function to get the adressbook contentstable |
|
| 2613 | - $ab_entryid = mapi_ab_getdefaultdir($addrbook); |
|
| 2614 | - if ($ab_entryid) { |
|
| 2615 | - $ab_dir = mapi_ab_openentry($addrbook, $ab_entryid); |
|
| 2616 | - } |
|
| 2617 | - if ($ab_dir) { |
|
| 2618 | - $table = mapi_folder_getcontentstable($ab_dir); |
|
| 2619 | - } |
|
| 2620 | - |
|
| 2621 | - if (!$table) { |
|
| 2622 | - SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->resolveRecipientGAL(): Unable to open addressbook:0x%X", mapi_last_hresult())); |
|
| 2623 | - |
|
| 2624 | - return false; |
|
| 2625 | - } |
|
| 2626 | - |
|
| 2627 | - $restriction = MAPIUtils::GetSearchRestriction(u2w($to)); |
|
| 2628 | - mapi_table_restrict($table, $restriction); |
|
| 2629 | - |
|
| 2630 | - $querycnt = mapi_table_getrowcount($table); |
|
| 2631 | - if ($querycnt > 0) { |
|
| 2632 | - $recipientGal = []; |
|
| 2633 | - $rowsToQuery = $maxAmbiguousRecipients; |
|
| 2634 | - // some devices request 0 ambiguous recipients |
|
| 2635 | - if ($querycnt == 1 && $maxAmbiguousRecipients == 0) { |
|
| 2636 | - $rowsToQuery = 1; |
|
| 2637 | - } |
|
| 2638 | - elseif ($querycnt > 1 && $maxAmbiguousRecipients == 0) { |
|
| 2639 | - SLog::Write(LOGLEVEL_INFO, sprintf("Grommunio->resolveRecipientGAL(): GAL search found %d recipients but the device hasn't requested ambiguous recipients", $querycnt)); |
|
| 2640 | - |
|
| 2641 | - return $recipientGal; |
|
| 2642 | - } |
|
| 2643 | - elseif ($querycnt > 1 && $maxAmbiguousRecipients == 1) { |
|
| 2644 | - $rowsToQuery = $querycnt; |
|
| 2645 | - } |
|
| 2646 | - // get the certificate every time because caching the certificate is less expensive than opening addressbook entry again |
|
| 2647 | - $abentries = mapi_table_queryrows($table, [PR_ENTRYID, PR_DISPLAY_NAME, PR_EMS_AB_TAGGED_X509_CERT, PR_OBJECT_TYPE, PR_SMTP_ADDRESS], 0, $rowsToQuery); |
|
| 2648 | - for ($i = 0, $nrEntries = count($abentries); $i < $nrEntries; ++$i) { |
|
| 2649 | - if (strcasecmp($abentries[$i][PR_SMTP_ADDRESS], $to) !== 0 && $maxAmbiguousRecipients == 1) { |
|
| 2650 | - SLog::Write(LOGLEVEL_INFO, sprintf("Grommunio->resolveRecipientGAL(): maxAmbiguousRecipients is 1 and found non-matching user (to '%s' found: '%s')", $to, $abentries[$i][PR_SMTP_ADDRESS])); |
|
| 2651 | - |
|
| 2652 | - continue; |
|
| 2653 | - } |
|
| 2654 | - if ($abentries[$i][PR_OBJECT_TYPE] == MAPI_DISTLIST) { |
|
| 2655 | - // check whether to expand dist list |
|
| 2656 | - if ($expandDistlist) { |
|
| 2657 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->resolveRecipientGAL(): '%s' is a dist list. Expand it to members.", $to)); |
|
| 2658 | - $distList = mapi_ab_openentry($addrbook, $abentries[$i][PR_ENTRYID]); |
|
| 2659 | - $distListContent = mapi_folder_getcontentstable($distList); |
|
| 2660 | - $distListMembers = mapi_table_queryallrows($distListContent, [PR_ENTRYID, PR_DISPLAY_NAME, PR_EMS_AB_TAGGED_X509_CERT]); |
|
| 2661 | - for ($j = 0, $nrDistListMembers = mapi_table_getrowcount($distListContent); $j < $nrDistListMembers; ++$j) { |
|
| 2662 | - SLog::Write(LOGLEVEL_WBXML, sprintf("Grommunio->resolveRecipientGAL(): distlist's '%s' member: '%s'", $to, $distListMembers[$j][PR_DISPLAY_NAME])); |
|
| 2663 | - $recipientGal[] = $this->createResolveRecipient(SYNC_RESOLVERECIPIENTS_TYPE_GAL, $to, $distListMembers[$j], $nrDistListMembers); |
|
| 2664 | - } |
|
| 2665 | - } |
|
| 2666 | - else { |
|
| 2667 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->resolveRecipientGAL(): '%s' is a dist list, but return it as is.", $to)); |
|
| 2668 | - $recipientGal[] = $this->createResolveRecipient(SYNC_RESOLVERECIPIENTS_TYPE_GAL, $abentries[$i][PR_SMTP_ADDRESS], $abentries[$i]); |
|
| 2669 | - } |
|
| 2670 | - } |
|
| 2671 | - elseif ($abentries[$i][PR_OBJECT_TYPE] == MAPI_MAILUSER) { |
|
| 2672 | - $recipientGal[] = $this->createResolveRecipient(SYNC_RESOLVERECIPIENTS_TYPE_GAL, $abentries[$i][PR_SMTP_ADDRESS], $abentries[$i]); |
|
| 2673 | - } |
|
| 2674 | - } |
|
| 2675 | - |
|
| 2676 | - SLog::Write(LOGLEVEL_WBXML, "Grommunio->resolveRecipientGAL(): Found a recipient in GAL"); |
|
| 2677 | - |
|
| 2678 | - return $recipientGal; |
|
| 2679 | - } |
|
| 2680 | - |
|
| 2681 | - SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->resolveRecipientGAL(): No recipient found for: '%s' in GAL", $to)); |
|
| 2682 | - |
|
| 2683 | - return SYNC_RESOLVERECIPSSTATUS_RESPONSE_UNRESOLVEDRECIP; |
|
| 2684 | - |
|
| 2685 | - return false; |
|
| 2686 | - } |
|
| 2687 | - |
|
| 2688 | - /** |
|
| 2689 | - * Resolves recipient from the contact list and gets his certificates. |
|
| 2690 | - * |
|
| 2691 | - * @param string $to |
|
| 2692 | - * @param int $maxAmbiguousRecipients |
|
| 2693 | - * |
|
| 2694 | - * @return array|bool |
|
| 2695 | - */ |
|
| 2696 | - private function resolveRecipientContact($to, $maxAmbiguousRecipients) { |
|
| 2697 | - SLog::Write(LOGLEVEL_WBXML, sprintf("Grommunio->resolveRecipientContact(): Resolving recipient '%s' in user's contacts", $to)); |
|
| 2698 | - // go through all contact folders of the user and |
|
| 2699 | - // check if there's a contact with the given email address |
|
| 2700 | - $root = mapi_msgstore_openentry($this->defaultstore); |
|
| 2701 | - if (!$root) { |
|
| 2702 | - SLog::Write(LOGLEVEL_ERROR, sprintf("Grommunio->resolveRecipientContact(): Unable to open default store: 0x%X", mapi_last_hresult())); |
|
| 2703 | - } |
|
| 2704 | - $rootprops = mapi_getprops($root, [PR_IPM_CONTACT_ENTRYID]); |
|
| 2705 | - $contacts = $this->getContactsFromFolder($this->defaultstore, $rootprops[PR_IPM_CONTACT_ENTRYID], $to); |
|
| 2706 | - $recipients = []; |
|
| 2707 | - |
|
| 2708 | - if ($contacts !== false) { |
|
| 2709 | - SLog::Write(LOGLEVEL_WBXML, sprintf("Grommunio->resolveRecipientContact(): Found %d contacts in main contacts folder.", count($contacts))); |
|
| 2710 | - // create resolve recipient object |
|
| 2711 | - foreach ($contacts as $contact) { |
|
| 2712 | - $recipients[] = $this->createResolveRecipient(SYNC_RESOLVERECIPIENTS_TYPE_CONTACT, $to, $contact); |
|
| 2713 | - } |
|
| 2714 | - } |
|
| 2715 | - |
|
| 2716 | - $contactfolder = mapi_msgstore_openentry($this->defaultstore, $rootprops[PR_IPM_CONTACT_ENTRYID]); |
|
| 2717 | - $subfolders = MAPIUtils::GetSubfoldersForType($contactfolder, "IPF.Contact"); |
|
| 2718 | - if ($subfolders !== false) { |
|
| 2719 | - foreach ($subfolders as $folder) { |
|
| 2720 | - $contacts = $this->getContactsFromFolder($this->defaultstore, $folder[PR_ENTRYID], $to); |
|
| 2721 | - if ($contacts !== false) { |
|
| 2722 | - SLog::Write(LOGLEVEL_WBXML, sprintf("Grommunio->resolveRecipientContact(): Found %d contacts in contacts' subfolder.", count($contacts))); |
|
| 2723 | - foreach ($contacts as $contact) { |
|
| 2724 | - $recipients[] = $this->createResolveRecipient(SYNC_RESOLVERECIPIENTS_TYPE_CONTACT, $to, $contact); |
|
| 2725 | - } |
|
| 2726 | - } |
|
| 2727 | - } |
|
| 2728 | - } |
|
| 2729 | - |
|
| 2730 | - // search contacts in public folders |
|
| 2731 | - $storestables = mapi_getmsgstorestable($this->session); |
|
| 2732 | - $result = mapi_last_hresult(); |
|
| 2733 | - |
|
| 2734 | - if ($result == NOERROR) { |
|
| 2735 | - $rows = mapi_table_queryallrows($storestables, [PR_ENTRYID, PR_DEFAULT_STORE, PR_MDB_PROVIDER]); |
|
| 2736 | - foreach ($rows as $row) { |
|
| 2737 | - if (isset($row[PR_MDB_PROVIDER]) && $row[PR_MDB_PROVIDER] == ZARAFA_STORE_PUBLIC_GUID) { |
|
| 2738 | - // TODO refactor public store |
|
| 2739 | - $publicstore = mapi_openmsgstore($this->session, $row[PR_ENTRYID]); |
|
| 2740 | - $publicfolder = mapi_msgstore_openentry($publicstore); |
|
| 2741 | - |
|
| 2742 | - $subfolders = MAPIUtils::GetSubfoldersForType($publicfolder, "IPF.Contact"); |
|
| 2743 | - if ($subfolders !== false) { |
|
| 2744 | - foreach ($subfolders as $folder) { |
|
| 2745 | - $contacts = $this->getContactsFromFolder($publicstore, $folder[PR_ENTRYID], $to); |
|
| 2746 | - if ($contacts !== false) { |
|
| 2747 | - SLog::Write(LOGLEVEL_WBXML, sprintf("Grommunio->resolveRecipientContact(): Found %d contacts in public contacts folder.", count($contacts))); |
|
| 2748 | - foreach ($contacts as $contact) { |
|
| 2749 | - $recipients[] = $this->createResolveRecipient(SYNC_RESOLVERECIPIENTS_TYPE_CONTACT, $to, $contact); |
|
| 2750 | - } |
|
| 2751 | - } |
|
| 2752 | - } |
|
| 2753 | - } |
|
| 2754 | - |
|
| 2755 | - break; |
|
| 2756 | - } |
|
| 2757 | - } |
|
| 2758 | - } |
|
| 2759 | - else { |
|
| 2760 | - SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->resolveRecipientContact(): Unable to open public store: 0x%X", $result)); |
|
| 2761 | - } |
|
| 2762 | - |
|
| 2763 | - if (empty($recipients)) { |
|
| 2764 | - $contactProperties = []; |
|
| 2765 | - $contactProperties[PR_DISPLAY_NAME] = $to; |
|
| 2766 | - $contactProperties[PR_USER_X509_CERTIFICATE] = false; |
|
| 2767 | - |
|
| 2768 | - $recipients[] = $this->createResolveRecipient(SYNC_RESOLVERECIPIENTS_TYPE_CONTACT, $to, $contactProperties); |
|
| 2769 | - } |
|
| 2770 | - |
|
| 2771 | - return $recipients; |
|
| 2772 | - } |
|
| 2773 | - |
|
| 2774 | - /** |
|
| 2775 | - * Creates SyncResolveRecipientsCertificates object for ResolveRecipients. |
|
| 2776 | - * |
|
| 2777 | - * @param binary $certificates |
|
| 2778 | - * @param int $recipientCount |
|
| 2779 | - * |
|
| 2780 | - * @return SyncResolveRecipientsCertificates |
|
| 2781 | - */ |
|
| 2782 | - private function getCertificates($certificates, $recipientCount = 0) { |
|
| 2783 | - $cert = new SyncResolveRecipientsCertificates(); |
|
| 2784 | - if ($certificates === false) { |
|
| 2785 | - $cert->status = SYNC_RESOLVERECIPSSTATUS_CERTIFICATES_NOVALIDCERT; |
|
| 2786 | - |
|
| 2787 | - return $cert; |
|
| 2788 | - } |
|
| 2789 | - $cert->status = SYNC_RESOLVERECIPSSTATUS_SUCCESS; |
|
| 2790 | - $cert->certificatecount = count($certificates); |
|
| 2791 | - $cert->recipientcount = $recipientCount; |
|
| 2792 | - $cert->certificate = []; |
|
| 2793 | - foreach ($certificates as $certificate) { |
|
| 2794 | - $cert->certificate[] = base64_encode($certificate); |
|
| 2795 | - } |
|
| 2796 | - |
|
| 2797 | - return $cert; |
|
| 2798 | - } |
|
| 2799 | - |
|
| 2800 | - /** |
|
| 2801 | - * Creates SyncResolveRecipient object for ResolveRecipientsResponse. |
|
| 2802 | - * |
|
| 2803 | - * @param int $type |
|
| 2804 | - * @param string $email |
|
| 2805 | - * @param array $recipientProperties |
|
| 2806 | - * @param int $recipientCount |
|
| 2807 | - * |
|
| 2808 | - * @return SyncResolveRecipient |
|
| 2809 | - */ |
|
| 2810 | - private function createResolveRecipient($type, $email, $recipientProperties, $recipientCount = 0) { |
|
| 2811 | - $recipient = new SyncResolveRecipient(); |
|
| 2812 | - $recipient->type = $type; |
|
| 2813 | - $recipient->displayname = u2w($recipientProperties[PR_DISPLAY_NAME]); |
|
| 2814 | - $recipient->emailaddress = $email; |
|
| 2815 | - |
|
| 2816 | - if ($type == SYNC_RESOLVERECIPIENTS_TYPE_GAL) { |
|
| 2817 | - $certificateProp = PR_EMS_AB_TAGGED_X509_CERT; |
|
| 2818 | - } |
|
| 2819 | - elseif ($type == SYNC_RESOLVERECIPIENTS_TYPE_CONTACT) { |
|
| 2820 | - $certificateProp = PR_USER_X509_CERTIFICATE; |
|
| 2821 | - } |
|
| 2822 | - else { |
|
| 2823 | - $certificateProp = null; |
|
| 2824 | - } |
|
| 2825 | - |
|
| 2826 | - if (isset($recipientProperties[$certificateProp]) && is_array($recipientProperties[$certificateProp]) && !empty($recipientProperties[$certificateProp])) { |
|
| 2827 | - $certificates = $this->getCertificates($recipientProperties[$certificateProp], $recipientCount); |
|
| 2828 | - } |
|
| 2829 | - else { |
|
| 2830 | - $certificates = $this->getCertificates(false); |
|
| 2831 | - SLog::Write(LOGLEVEL_INFO, sprintf("Grommunio->createResolveRecipient(): No certificate found for '%s' (requested email address: '%s')", $recipientProperties[PR_DISPLAY_NAME], $email)); |
|
| 2832 | - } |
|
| 2833 | - $recipient->certificates = $certificates; |
|
| 2834 | - |
|
| 2835 | - if (isset($recipientProperties[PR_ENTRYID])) { |
|
| 2836 | - $recipient->id = $recipientProperties[PR_ENTRYID]; |
|
| 2837 | - } |
|
| 2838 | - |
|
| 2839 | - return $recipient; |
|
| 2840 | - } |
|
| 2841 | - |
|
| 2842 | - /** |
|
| 2843 | - * Gets the availability of a user for the given time window. |
|
| 2844 | - * |
|
| 2845 | - * @param string $to |
|
| 2846 | - * @param SyncResolveRecipient $resolveRecipient |
|
| 2847 | - * @param SyncResolveRecipientsOptions $resolveRecipientsOptions |
|
| 2848 | - * |
|
| 2849 | - * @return SyncResolveRecipientsAvailability |
|
| 2850 | - */ |
|
| 2851 | - private function getAvailability($to, $resolveRecipient, $resolveRecipientsOptions) { |
|
| 2852 | - $availability = new SyncResolveRecipientsAvailability(); |
|
| 2853 | - $availability->status = SYNC_RESOLVERECIPSSTATUS_AVAILABILITY_SUCCESS; |
|
| 2854 | - |
|
| 2855 | - if (!isset($resolveRecipient->id)) { |
|
| 2856 | - // TODO this shouldn't happen but try to get the recipient in such a case |
|
| 2857 | - } |
|
| 2858 | - |
|
| 2859 | - $start = strtotime($resolveRecipientsOptions->availability->starttime); |
|
| 2860 | - $end = strtotime($resolveRecipientsOptions->availability->endtime); |
|
| 2861 | - // Each digit in the MergedFreeBusy indicates the free/busy status for the user for every 30 minute interval. |
|
| 2862 | - $timeslots = intval(ceil(($end - $start) / self::HALFHOURSECONDS)); |
|
| 2863 | - |
|
| 2864 | - if ($timeslots > self::MAXFREEBUSYSLOTS) { |
|
| 2865 | - throw new StatusException("Grommunio->getAvailability(): the requested free busy range is too large.", SYNC_RESOLVERECIPSSTATUS_PROTOCOLERROR); |
|
| 2866 | - } |
|
| 2867 | - |
|
| 2868 | - $mergedFreeBusy = str_pad(fbNoData, $timeslots, fbNoData); |
|
| 2869 | - |
|
| 2870 | - $retval = mapi_getuseravailability($this->session, $resolveRecipient->id, $start, $end); |
|
| 2871 | - SLog::Write(LOGLEVEL_INFO, sprintf("Grommunio->getAvailability(): free busy '%s'", print_r($retval, 1))); |
|
| 2872 | - |
|
| 2873 | - if (!empty($retval)) { |
|
| 2874 | - $freebusy = json_decode($retval, true); |
|
| 2875 | - // freebusy is available, assume that the user is free |
|
| 2876 | - $mergedFreeBusy = str_pad(fbFree, $timeslots, fbFree); |
|
| 2877 | - foreach ($freebusy['events'] as $event) { |
|
| 2878 | - // calculate which timeslot of mergedFreeBusy should be replaced. |
|
| 2879 | - $startSlot = intval(floor(($event['StartTime'] - $start) / self::HALFHOURSECONDS)); |
|
| 2880 | - $endSlot = intval(floor(($event['EndTime'] - $start) / self::HALFHOURSECONDS)); |
|
| 2881 | - // if event started at a multiple of half an hour from requested freebusy time and |
|
| 2882 | - // its duration is also a multiple of half an hour |
|
| 2883 | - // then it's necessary to reduce endSlot by one |
|
| 2884 | - if ((($event['StartTime'] - $start) % self::HALFHOURSECONDS == 0) && (($event['EndTime'] - $event['StartTime']) % self::HALFHOURSECONDS == 0)) { |
|
| 2885 | - --$endSlot; |
|
| 2886 | - } |
|
| 2887 | - $fbType = Utils::GetFbStatusFromType($event['BusyType']); |
|
| 2888 | - for ($i = $startSlot; $i <= $endSlot && $i < $timeslots; ++$i) { |
|
| 2889 | - // only set the new slot's free busy status if it's higher than the current one |
|
| 2890 | - if ($fbType > $mergedFreeBusy[$i]) { |
|
| 2891 | - $mergedFreeBusy[$i] = $fbType; |
|
| 2892 | - } |
|
| 2893 | - } |
|
| 2894 | - } |
|
| 2895 | - } |
|
| 2896 | - $availability->mergedfreebusy = $mergedFreeBusy; |
|
| 2897 | - |
|
| 2898 | - return $availability; |
|
| 2899 | - } |
|
| 2900 | - |
|
| 2901 | - /** |
|
| 2902 | - * Returns contacts matching given email address from a folder. |
|
| 2903 | - * |
|
| 2904 | - * @param MAPIStore $store |
|
| 2905 | - * @param binary $folderEntryid |
|
| 2906 | - * @param string $email |
|
| 2907 | - * |
|
| 2908 | - * @return array|bool |
|
| 2909 | - */ |
|
| 2910 | - private function getContactsFromFolder($store, $folderEntryid, $email) { |
|
| 2911 | - $folder = mapi_msgstore_openentry($store, $folderEntryid); |
|
| 2912 | - $folderContent = mapi_folder_getcontentstable($folder); |
|
| 2913 | - mapi_table_restrict($folderContent, MAPIUtils::GetEmailAddressRestriction($store, $email)); |
|
| 2914 | - // TODO max limit |
|
| 2915 | - if (mapi_table_getrowcount($folderContent) > 0) { |
|
| 2916 | - return mapi_table_queryallrows($folderContent, [PR_DISPLAY_NAME, PR_USER_X509_CERTIFICATE, PR_ENTRYID]); |
|
| 2917 | - } |
|
| 2918 | - |
|
| 2919 | - return false; |
|
| 2920 | - } |
|
| 2921 | - |
|
| 2922 | - /** |
|
| 2923 | - * Get MAPI addressbook object. |
|
| 2924 | - * |
|
| 2925 | - * @return MAPIAddressbook object to be used with mapi_ab_* or false on failure |
|
| 2926 | - */ |
|
| 2927 | - private function getAddressbook() { |
|
| 2928 | - if (isset($this->addressbook) && $this->addressbook) { |
|
| 2929 | - return $this->addressbook; |
|
| 2930 | - } |
|
| 2931 | - $this->addressbook = mapi_openaddressbook($this->session); |
|
| 2932 | - $result = mapi_last_hresult(); |
|
| 2933 | - if ($result && $this->addressbook === false) { |
|
| 2934 | - SLog::Write(LOGLEVEL_ERROR, sprintf("Grommunio->getAddressbook error opening addressbook 0x%X", $result)); |
|
| 2935 | - |
|
| 2936 | - return false; |
|
| 2937 | - } |
|
| 2938 | - |
|
| 2939 | - return $this->addressbook; |
|
| 2940 | - } |
|
| 2941 | - |
|
| 2942 | - /** |
|
| 2943 | - * Checks if the user is not disabled for grommunio-sync. |
|
| 2944 | - * |
|
| 2945 | - * @throws FatalException if user is disabled for grommunio-sync |
|
| 2946 | - * |
|
| 2947 | - * @return bool |
|
| 2948 | - */ |
|
| 2949 | - private function isGSyncEnabled() { |
|
| 2950 | - $addressbook = $this->getAddressbook(); |
|
| 2951 | - // this check needs to be performed on the store of the main (authenticated) user |
|
| 2952 | - $store = $this->storeCache[$this->mainUser]; |
|
| 2953 | - $userEntryid = mapi_getprops($store, [PR_MAILBOX_OWNER_ENTRYID]); |
|
| 2954 | - $mailuser = mapi_ab_openentry($addressbook, $userEntryid[PR_MAILBOX_OWNER_ENTRYID]); |
|
| 2955 | - $enabledFeatures = mapi_getprops($mailuser, [PR_EC_DISABLED_FEATURES]); |
|
| 2956 | - if (isset($enabledFeatures[PR_EC_DISABLED_FEATURES]) && is_array($enabledFeatures[PR_EC_DISABLED_FEATURES])) { |
|
| 2957 | - $mobileDisabled = in_array(self::MOBILE_ENABLED, $enabledFeatures[PR_EC_DISABLED_FEATURES]); |
|
| 2958 | - $deviceId = Request::GetDeviceID(); |
|
| 2959 | - // Checks for deviceId present in zarafaDisabledFeatures LDAP array attribute. Check is performed case insensitive. |
|
| 2960 | - $deviceIdDisabled = (($deviceId !== null) && in_array($deviceId, array_map('strtolower', $enabledFeatures[PR_EC_DISABLED_FEATURES]))) ? true : false; |
|
| 2961 | - if ($mobileDisabled) { |
|
| 2962 | - throw new FatalException("User is disabled for grommunio-sync."); |
|
| 2963 | - } |
|
| 2964 | - if ($deviceIdDisabled) { |
|
| 2965 | - throw new FatalException(sprintf("User has deviceId %s disabled for usage with grommunio-sync.", $deviceId)); |
|
| 2966 | - } |
|
| 2967 | - } |
|
| 2968 | - |
|
| 2969 | - return true; |
|
| 2970 | - } |
|
| 2387 | + $rmTemplates->Status = SYNC_COMMONSTATUS_IRMFEATUREDISABLED; |
|
| 2388 | + $rmTemplates->rmtemplates = []; |
|
| 2389 | + } |
|
| 2390 | + |
|
| 2391 | + /** |
|
| 2392 | + * Sets the importance and priority of a message from a RFC822 message headers. |
|
| 2393 | + * |
|
| 2394 | + * @param int $xPriority |
|
| 2395 | + * @param array $mapiprops |
|
| 2396 | + * @param mixed $sendMailProps |
|
| 2397 | + */ |
|
| 2398 | + private function getImportanceAndPriority($xPriority, &$mapiprops, $sendMailProps) { |
|
| 2399 | + switch ($xPriority) { |
|
| 2400 | + case 1: |
|
| 2401 | + case 2: |
|
| 2402 | + $priority = PRIO_URGENT; |
|
| 2403 | + $importance = IMPORTANCE_HIGH; |
|
| 2404 | + break; |
|
| 2405 | + |
|
| 2406 | + case 4: |
|
| 2407 | + case 5: |
|
| 2408 | + $priority = PRIO_NONURGENT; |
|
| 2409 | + $importance = IMPORTANCE_LOW; |
|
| 2410 | + break; |
|
| 2411 | + |
|
| 2412 | + case 3: |
|
| 2413 | + default: |
|
| 2414 | + $priority = PRIO_NORMAL; |
|
| 2415 | + $importance = IMPORTANCE_NORMAL; |
|
| 2416 | + break; |
|
| 2417 | + } |
|
| 2418 | + $mapiprops[$sendMailProps["importance"]] = $importance; |
|
| 2419 | + $mapiprops[$sendMailProps["priority"]] = $priority; |
|
| 2420 | + } |
|
| 2421 | + |
|
| 2422 | + /** |
|
| 2423 | + * Copies attachments from one message to another. |
|
| 2424 | + * |
|
| 2425 | + * @param MAPIMessage $toMessage |
|
| 2426 | + * @param MAPIMessage $fromMessage |
|
| 2427 | + */ |
|
| 2428 | + private function copyAttachments(&$toMessage, $fromMessage) { |
|
| 2429 | + $attachtable = mapi_message_getattachmenttable($fromMessage); |
|
| 2430 | + $rows = mapi_table_queryallrows($attachtable, [PR_ATTACH_NUM]); |
|
| 2431 | + |
|
| 2432 | + foreach ($rows as $row) { |
|
| 2433 | + if (isset($row[PR_ATTACH_NUM])) { |
|
| 2434 | + $attach = mapi_message_openattach($fromMessage, $row[PR_ATTACH_NUM]); |
|
| 2435 | + $newattach = mapi_message_createattach($toMessage); |
|
| 2436 | + mapi_copyto($attach, [], [], $newattach, 0); |
|
| 2437 | + mapi_savechanges($newattach); |
|
| 2438 | + } |
|
| 2439 | + } |
|
| 2440 | + } |
|
| 2441 | + |
|
| 2442 | + /** |
|
| 2443 | + * Function will create a search folder in FINDER_ROOT folder |
|
| 2444 | + * if folder exists then it will open it. |
|
| 2445 | + * |
|
| 2446 | + * @see createSearchFolder($store, $openIfExists = true) function in the webaccess |
|
| 2447 | + * |
|
| 2448 | + * @return mapiFolderObject $folder created search folder |
|
| 2449 | + */ |
|
| 2450 | + private function getSearchFolder() { |
|
| 2451 | + // create new or open existing search folder |
|
| 2452 | + $searchFolderRoot = $this->getSearchFoldersRoot($this->store); |
|
| 2453 | + if ($searchFolderRoot === false) { |
|
| 2454 | + // error in finding search root folder |
|
| 2455 | + // or store doesn't support search folders |
|
| 2456 | + return false; |
|
| 2457 | + } |
|
| 2458 | + |
|
| 2459 | + $searchFolder = $this->createSearchFolder($searchFolderRoot); |
|
| 2460 | + |
|
| 2461 | + if ($searchFolder !== false && mapi_last_hresult() == NOERROR) { |
|
| 2462 | + return $searchFolder; |
|
| 2463 | + } |
|
| 2464 | + |
|
| 2465 | + return false; |
|
| 2466 | + } |
|
| 2467 | + |
|
| 2468 | + /** |
|
| 2469 | + * Function will open FINDER_ROOT folder in root container |
|
| 2470 | + * public folder's don't have FINDER_ROOT folder. |
|
| 2471 | + * |
|
| 2472 | + * @see getSearchFoldersRoot($store) function in the webaccess |
|
| 2473 | + * |
|
| 2474 | + * @return mapiFolderObject root folder for search folders |
|
| 2475 | + */ |
|
| 2476 | + private function getSearchFoldersRoot() { |
|
| 2477 | + // check if we can create search folders |
|
| 2478 | + $storeProps = mapi_getprops($this->store, [PR_STORE_SUPPORT_MASK, PR_FINDER_ENTRYID]); |
|
| 2479 | + if (($storeProps[PR_STORE_SUPPORT_MASK] & STORE_SEARCH_OK) != STORE_SEARCH_OK) { |
|
| 2480 | + SLog::Write(LOGLEVEL_WARN, "Grommunio->getSearchFoldersRoot(): Store doesn't support search folders. Public store doesn't have FINDER_ROOT folder"); |
|
| 2481 | + |
|
| 2482 | + return false; |
|
| 2483 | + } |
|
| 2484 | + |
|
| 2485 | + // open search folders root |
|
| 2486 | + $searchRootFolder = mapi_msgstore_openentry($this->store, $storeProps[PR_FINDER_ENTRYID]); |
|
| 2487 | + if (mapi_last_hresult() != NOERROR) { |
|
| 2488 | + SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->getSearchFoldersRoot(): Unable to open search folder (0x%X)", mapi_last_hresult())); |
|
| 2489 | + |
|
| 2490 | + return false; |
|
| 2491 | + } |
|
| 2492 | + |
|
| 2493 | + return $searchRootFolder; |
|
| 2494 | + } |
|
| 2495 | + |
|
| 2496 | + /** |
|
| 2497 | + * Creates a search folder if it not exists or opens an existing one |
|
| 2498 | + * and returns it. |
|
| 2499 | + * |
|
| 2500 | + * @param mapiFolderObject $searchFolderRoot |
|
| 2501 | + * |
|
| 2502 | + * @return mapiFolderObject |
|
| 2503 | + */ |
|
| 2504 | + private function createSearchFolder($searchFolderRoot) { |
|
| 2505 | + $folderName = "grommunio-sync Search Folder " . @getmypid(); |
|
| 2506 | + $searchFolders = mapi_folder_gethierarchytable($searchFolderRoot); |
|
| 2507 | + $restriction = [ |
|
| 2508 | + RES_CONTENT, |
|
| 2509 | + [ |
|
| 2510 | + FUZZYLEVEL => FL_PREFIX, |
|
| 2511 | + ULPROPTAG => PR_DISPLAY_NAME, |
|
| 2512 | + VALUE => [PR_DISPLAY_NAME => $folderName], |
|
| 2513 | + ], |
|
| 2514 | + ]; |
|
| 2515 | + // restrict the hierarchy to the grommunio-sync search folder only |
|
| 2516 | + mapi_table_restrict($searchFolders, $restriction); |
|
| 2517 | + if (mapi_table_getrowcount($searchFolders)) { |
|
| 2518 | + $searchFolder = mapi_table_queryrows($searchFolders, [PR_ENTRYID], 0, 1); |
|
| 2519 | + |
|
| 2520 | + return mapi_msgstore_openentry($this->store, $searchFolder[0][PR_ENTRYID]); |
|
| 2521 | + } |
|
| 2522 | + |
|
| 2523 | + return mapi_folder_createfolder($searchFolderRoot, $folderName, null, 0, FOLDER_SEARCH); |
|
| 2524 | + } |
|
| 2525 | + |
|
| 2526 | + /** |
|
| 2527 | + * Creates a search restriction. |
|
| 2528 | + * |
|
| 2529 | + * @param ContentParameter $cpo |
|
| 2530 | + * |
|
| 2531 | + * @return array |
|
| 2532 | + */ |
|
| 2533 | + private function getSearchRestriction($cpo) { |
|
| 2534 | + $searchText = $cpo->GetSearchFreeText(); |
|
| 2535 | + |
|
| 2536 | + $searchGreater = strtotime($cpo->GetSearchValueGreater()); |
|
| 2537 | + $searchLess = strtotime($cpo->GetSearchValueLess()); |
|
| 2538 | + |
|
| 2539 | + if (version_compare(phpversion(), '5.3.4') < 0) { |
|
| 2540 | + SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->getSearchRestriction(): Your system's PHP version (%s) might not correctly process unicode strings. Search containing such characters might not return correct results. It is recommended to update to at least PHP 5.3.4. See ZP-541 for more information.", phpversion())); |
|
| 2541 | + } |
|
| 2542 | + // split the search on whitespache and look for every word |
|
| 2543 | + $searchText = preg_split("/\\W+/u", $searchText); |
|
| 2544 | + $searchProps = [PR_BODY, PR_SUBJECT, PR_DISPLAY_TO, PR_DISPLAY_CC, PR_SENDER_NAME, PR_SENDER_EMAIL_ADDRESS, PR_SENT_REPRESENTING_NAME, PR_SENT_REPRESENTING_EMAIL_ADDRESS]; |
|
| 2545 | + $resAnd = []; |
|
| 2546 | + foreach ($searchText as $term) { |
|
| 2547 | + $resOr = []; |
|
| 2548 | + |
|
| 2549 | + foreach ($searchProps as $property) { |
|
| 2550 | + array_push( |
|
| 2551 | + $resOr, |
|
| 2552 | + [RES_CONTENT, |
|
| 2553 | + [ |
|
| 2554 | + FUZZYLEVEL => FL_SUBSTRING | FL_IGNORECASE, |
|
| 2555 | + ULPROPTAG => $property, |
|
| 2556 | + VALUE => u2w($term), |
|
| 2557 | + ], |
|
| 2558 | + ] |
|
| 2559 | + ); |
|
| 2560 | + } |
|
| 2561 | + array_push($resAnd, [RES_OR, $resOr]); |
|
| 2562 | + } |
|
| 2563 | + |
|
| 2564 | + // add time range restrictions |
|
| 2565 | + if ($searchGreater) { |
|
| 2566 | + array_push($resAnd, [RES_PROPERTY, [RELOP => RELOP_GE, ULPROPTAG => PR_MESSAGE_DELIVERY_TIME, VALUE => [PR_MESSAGE_DELIVERY_TIME => $searchGreater]]]); // RES_AND; |
|
| 2567 | + } |
|
| 2568 | + if ($searchLess) { |
|
| 2569 | + array_push($resAnd, [RES_PROPERTY, [RELOP => RELOP_LE, ULPROPTAG => PR_MESSAGE_DELIVERY_TIME, VALUE => [PR_MESSAGE_DELIVERY_TIME => $searchLess]]]); |
|
| 2570 | + } |
|
| 2571 | + |
|
| 2572 | + return [RES_AND, $resAnd]; |
|
| 2573 | + } |
|
| 2574 | + |
|
| 2575 | + /** |
|
| 2576 | + * Resolve recipient based on his email address. |
|
| 2577 | + * |
|
| 2578 | + * @param string $to |
|
| 2579 | + * @param int $maxAmbiguousRecipients |
|
| 2580 | + * @param bool $expandDistlist |
|
| 2581 | + * |
|
| 2582 | + * @return bool|SyncResolveRecipient |
|
| 2583 | + */ |
|
| 2584 | + private function resolveRecipient($to, $maxAmbiguousRecipients, $expandDistlist = true) { |
|
| 2585 | + $recipient = $this->resolveRecipientGAL($to, $maxAmbiguousRecipients, $expandDistlist); |
|
| 2586 | + |
|
| 2587 | + if ($recipient !== false) { |
|
| 2588 | + return $recipient; |
|
| 2589 | + } |
|
| 2590 | + |
|
| 2591 | + $recipient = $this->resolveRecipientContact($to, $maxAmbiguousRecipients); |
|
| 2592 | + |
|
| 2593 | + if ($recipient !== false) { |
|
| 2594 | + return $recipient; |
|
| 2595 | + } |
|
| 2596 | + |
|
| 2597 | + return false; |
|
| 2598 | + } |
|
| 2599 | + |
|
| 2600 | + /** |
|
| 2601 | + * Resolves recipient from the GAL and gets his certificates. |
|
| 2602 | + * |
|
| 2603 | + * @param string $to |
|
| 2604 | + * @param int $maxAmbiguousRecipients |
|
| 2605 | + * @param bool $expandDistlist |
|
| 2606 | + * |
|
| 2607 | + * @return array|bool |
|
| 2608 | + */ |
|
| 2609 | + private function resolveRecipientGAL($to, $maxAmbiguousRecipients, $expandDistlist = true) { |
|
| 2610 | + SLog::Write(LOGLEVEL_WBXML, sprintf("Grommunio->resolveRecipientGAL(): Resolving recipient '%s' in GAL", $to)); |
|
| 2611 | + $addrbook = $this->getAddressbook(); |
|
| 2612 | + // FIXME: create a function to get the adressbook contentstable |
|
| 2613 | + $ab_entryid = mapi_ab_getdefaultdir($addrbook); |
|
| 2614 | + if ($ab_entryid) { |
|
| 2615 | + $ab_dir = mapi_ab_openentry($addrbook, $ab_entryid); |
|
| 2616 | + } |
|
| 2617 | + if ($ab_dir) { |
|
| 2618 | + $table = mapi_folder_getcontentstable($ab_dir); |
|
| 2619 | + } |
|
| 2620 | + |
|
| 2621 | + if (!$table) { |
|
| 2622 | + SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->resolveRecipientGAL(): Unable to open addressbook:0x%X", mapi_last_hresult())); |
|
| 2623 | + |
|
| 2624 | + return false; |
|
| 2625 | + } |
|
| 2626 | + |
|
| 2627 | + $restriction = MAPIUtils::GetSearchRestriction(u2w($to)); |
|
| 2628 | + mapi_table_restrict($table, $restriction); |
|
| 2629 | + |
|
| 2630 | + $querycnt = mapi_table_getrowcount($table); |
|
| 2631 | + if ($querycnt > 0) { |
|
| 2632 | + $recipientGal = []; |
|
| 2633 | + $rowsToQuery = $maxAmbiguousRecipients; |
|
| 2634 | + // some devices request 0 ambiguous recipients |
|
| 2635 | + if ($querycnt == 1 && $maxAmbiguousRecipients == 0) { |
|
| 2636 | + $rowsToQuery = 1; |
|
| 2637 | + } |
|
| 2638 | + elseif ($querycnt > 1 && $maxAmbiguousRecipients == 0) { |
|
| 2639 | + SLog::Write(LOGLEVEL_INFO, sprintf("Grommunio->resolveRecipientGAL(): GAL search found %d recipients but the device hasn't requested ambiguous recipients", $querycnt)); |
|
| 2640 | + |
|
| 2641 | + return $recipientGal; |
|
| 2642 | + } |
|
| 2643 | + elseif ($querycnt > 1 && $maxAmbiguousRecipients == 1) { |
|
| 2644 | + $rowsToQuery = $querycnt; |
|
| 2645 | + } |
|
| 2646 | + // get the certificate every time because caching the certificate is less expensive than opening addressbook entry again |
|
| 2647 | + $abentries = mapi_table_queryrows($table, [PR_ENTRYID, PR_DISPLAY_NAME, PR_EMS_AB_TAGGED_X509_CERT, PR_OBJECT_TYPE, PR_SMTP_ADDRESS], 0, $rowsToQuery); |
|
| 2648 | + for ($i = 0, $nrEntries = count($abentries); $i < $nrEntries; ++$i) { |
|
| 2649 | + if (strcasecmp($abentries[$i][PR_SMTP_ADDRESS], $to) !== 0 && $maxAmbiguousRecipients == 1) { |
|
| 2650 | + SLog::Write(LOGLEVEL_INFO, sprintf("Grommunio->resolveRecipientGAL(): maxAmbiguousRecipients is 1 and found non-matching user (to '%s' found: '%s')", $to, $abentries[$i][PR_SMTP_ADDRESS])); |
|
| 2651 | + |
|
| 2652 | + continue; |
|
| 2653 | + } |
|
| 2654 | + if ($abentries[$i][PR_OBJECT_TYPE] == MAPI_DISTLIST) { |
|
| 2655 | + // check whether to expand dist list |
|
| 2656 | + if ($expandDistlist) { |
|
| 2657 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->resolveRecipientGAL(): '%s' is a dist list. Expand it to members.", $to)); |
|
| 2658 | + $distList = mapi_ab_openentry($addrbook, $abentries[$i][PR_ENTRYID]); |
|
| 2659 | + $distListContent = mapi_folder_getcontentstable($distList); |
|
| 2660 | + $distListMembers = mapi_table_queryallrows($distListContent, [PR_ENTRYID, PR_DISPLAY_NAME, PR_EMS_AB_TAGGED_X509_CERT]); |
|
| 2661 | + for ($j = 0, $nrDistListMembers = mapi_table_getrowcount($distListContent); $j < $nrDistListMembers; ++$j) { |
|
| 2662 | + SLog::Write(LOGLEVEL_WBXML, sprintf("Grommunio->resolveRecipientGAL(): distlist's '%s' member: '%s'", $to, $distListMembers[$j][PR_DISPLAY_NAME])); |
|
| 2663 | + $recipientGal[] = $this->createResolveRecipient(SYNC_RESOLVERECIPIENTS_TYPE_GAL, $to, $distListMembers[$j], $nrDistListMembers); |
|
| 2664 | + } |
|
| 2665 | + } |
|
| 2666 | + else { |
|
| 2667 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->resolveRecipientGAL(): '%s' is a dist list, but return it as is.", $to)); |
|
| 2668 | + $recipientGal[] = $this->createResolveRecipient(SYNC_RESOLVERECIPIENTS_TYPE_GAL, $abentries[$i][PR_SMTP_ADDRESS], $abentries[$i]); |
|
| 2669 | + } |
|
| 2670 | + } |
|
| 2671 | + elseif ($abentries[$i][PR_OBJECT_TYPE] == MAPI_MAILUSER) { |
|
| 2672 | + $recipientGal[] = $this->createResolveRecipient(SYNC_RESOLVERECIPIENTS_TYPE_GAL, $abentries[$i][PR_SMTP_ADDRESS], $abentries[$i]); |
|
| 2673 | + } |
|
| 2674 | + } |
|
| 2675 | + |
|
| 2676 | + SLog::Write(LOGLEVEL_WBXML, "Grommunio->resolveRecipientGAL(): Found a recipient in GAL"); |
|
| 2677 | + |
|
| 2678 | + return $recipientGal; |
|
| 2679 | + } |
|
| 2680 | + |
|
| 2681 | + SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->resolveRecipientGAL(): No recipient found for: '%s' in GAL", $to)); |
|
| 2682 | + |
|
| 2683 | + return SYNC_RESOLVERECIPSSTATUS_RESPONSE_UNRESOLVEDRECIP; |
|
| 2684 | + |
|
| 2685 | + return false; |
|
| 2686 | + } |
|
| 2687 | + |
|
| 2688 | + /** |
|
| 2689 | + * Resolves recipient from the contact list and gets his certificates. |
|
| 2690 | + * |
|
| 2691 | + * @param string $to |
|
| 2692 | + * @param int $maxAmbiguousRecipients |
|
| 2693 | + * |
|
| 2694 | + * @return array|bool |
|
| 2695 | + */ |
|
| 2696 | + private function resolveRecipientContact($to, $maxAmbiguousRecipients) { |
|
| 2697 | + SLog::Write(LOGLEVEL_WBXML, sprintf("Grommunio->resolveRecipientContact(): Resolving recipient '%s' in user's contacts", $to)); |
|
| 2698 | + // go through all contact folders of the user and |
|
| 2699 | + // check if there's a contact with the given email address |
|
| 2700 | + $root = mapi_msgstore_openentry($this->defaultstore); |
|
| 2701 | + if (!$root) { |
|
| 2702 | + SLog::Write(LOGLEVEL_ERROR, sprintf("Grommunio->resolveRecipientContact(): Unable to open default store: 0x%X", mapi_last_hresult())); |
|
| 2703 | + } |
|
| 2704 | + $rootprops = mapi_getprops($root, [PR_IPM_CONTACT_ENTRYID]); |
|
| 2705 | + $contacts = $this->getContactsFromFolder($this->defaultstore, $rootprops[PR_IPM_CONTACT_ENTRYID], $to); |
|
| 2706 | + $recipients = []; |
|
| 2707 | + |
|
| 2708 | + if ($contacts !== false) { |
|
| 2709 | + SLog::Write(LOGLEVEL_WBXML, sprintf("Grommunio->resolveRecipientContact(): Found %d contacts in main contacts folder.", count($contacts))); |
|
| 2710 | + // create resolve recipient object |
|
| 2711 | + foreach ($contacts as $contact) { |
|
| 2712 | + $recipients[] = $this->createResolveRecipient(SYNC_RESOLVERECIPIENTS_TYPE_CONTACT, $to, $contact); |
|
| 2713 | + } |
|
| 2714 | + } |
|
| 2715 | + |
|
| 2716 | + $contactfolder = mapi_msgstore_openentry($this->defaultstore, $rootprops[PR_IPM_CONTACT_ENTRYID]); |
|
| 2717 | + $subfolders = MAPIUtils::GetSubfoldersForType($contactfolder, "IPF.Contact"); |
|
| 2718 | + if ($subfolders !== false) { |
|
| 2719 | + foreach ($subfolders as $folder) { |
|
| 2720 | + $contacts = $this->getContactsFromFolder($this->defaultstore, $folder[PR_ENTRYID], $to); |
|
| 2721 | + if ($contacts !== false) { |
|
| 2722 | + SLog::Write(LOGLEVEL_WBXML, sprintf("Grommunio->resolveRecipientContact(): Found %d contacts in contacts' subfolder.", count($contacts))); |
|
| 2723 | + foreach ($contacts as $contact) { |
|
| 2724 | + $recipients[] = $this->createResolveRecipient(SYNC_RESOLVERECIPIENTS_TYPE_CONTACT, $to, $contact); |
|
| 2725 | + } |
|
| 2726 | + } |
|
| 2727 | + } |
|
| 2728 | + } |
|
| 2729 | + |
|
| 2730 | + // search contacts in public folders |
|
| 2731 | + $storestables = mapi_getmsgstorestable($this->session); |
|
| 2732 | + $result = mapi_last_hresult(); |
|
| 2733 | + |
|
| 2734 | + if ($result == NOERROR) { |
|
| 2735 | + $rows = mapi_table_queryallrows($storestables, [PR_ENTRYID, PR_DEFAULT_STORE, PR_MDB_PROVIDER]); |
|
| 2736 | + foreach ($rows as $row) { |
|
| 2737 | + if (isset($row[PR_MDB_PROVIDER]) && $row[PR_MDB_PROVIDER] == ZARAFA_STORE_PUBLIC_GUID) { |
|
| 2738 | + // TODO refactor public store |
|
| 2739 | + $publicstore = mapi_openmsgstore($this->session, $row[PR_ENTRYID]); |
|
| 2740 | + $publicfolder = mapi_msgstore_openentry($publicstore); |
|
| 2741 | + |
|
| 2742 | + $subfolders = MAPIUtils::GetSubfoldersForType($publicfolder, "IPF.Contact"); |
|
| 2743 | + if ($subfolders !== false) { |
|
| 2744 | + foreach ($subfolders as $folder) { |
|
| 2745 | + $contacts = $this->getContactsFromFolder($publicstore, $folder[PR_ENTRYID], $to); |
|
| 2746 | + if ($contacts !== false) { |
|
| 2747 | + SLog::Write(LOGLEVEL_WBXML, sprintf("Grommunio->resolveRecipientContact(): Found %d contacts in public contacts folder.", count($contacts))); |
|
| 2748 | + foreach ($contacts as $contact) { |
|
| 2749 | + $recipients[] = $this->createResolveRecipient(SYNC_RESOLVERECIPIENTS_TYPE_CONTACT, $to, $contact); |
|
| 2750 | + } |
|
| 2751 | + } |
|
| 2752 | + } |
|
| 2753 | + } |
|
| 2754 | + |
|
| 2755 | + break; |
|
| 2756 | + } |
|
| 2757 | + } |
|
| 2758 | + } |
|
| 2759 | + else { |
|
| 2760 | + SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->resolveRecipientContact(): Unable to open public store: 0x%X", $result)); |
|
| 2761 | + } |
|
| 2762 | + |
|
| 2763 | + if (empty($recipients)) { |
|
| 2764 | + $contactProperties = []; |
|
| 2765 | + $contactProperties[PR_DISPLAY_NAME] = $to; |
|
| 2766 | + $contactProperties[PR_USER_X509_CERTIFICATE] = false; |
|
| 2767 | + |
|
| 2768 | + $recipients[] = $this->createResolveRecipient(SYNC_RESOLVERECIPIENTS_TYPE_CONTACT, $to, $contactProperties); |
|
| 2769 | + } |
|
| 2770 | + |
|
| 2771 | + return $recipients; |
|
| 2772 | + } |
|
| 2773 | + |
|
| 2774 | + /** |
|
| 2775 | + * Creates SyncResolveRecipientsCertificates object for ResolveRecipients. |
|
| 2776 | + * |
|
| 2777 | + * @param binary $certificates |
|
| 2778 | + * @param int $recipientCount |
|
| 2779 | + * |
|
| 2780 | + * @return SyncResolveRecipientsCertificates |
|
| 2781 | + */ |
|
| 2782 | + private function getCertificates($certificates, $recipientCount = 0) { |
|
| 2783 | + $cert = new SyncResolveRecipientsCertificates(); |
|
| 2784 | + if ($certificates === false) { |
|
| 2785 | + $cert->status = SYNC_RESOLVERECIPSSTATUS_CERTIFICATES_NOVALIDCERT; |
|
| 2786 | + |
|
| 2787 | + return $cert; |
|
| 2788 | + } |
|
| 2789 | + $cert->status = SYNC_RESOLVERECIPSSTATUS_SUCCESS; |
|
| 2790 | + $cert->certificatecount = count($certificates); |
|
| 2791 | + $cert->recipientcount = $recipientCount; |
|
| 2792 | + $cert->certificate = []; |
|
| 2793 | + foreach ($certificates as $certificate) { |
|
| 2794 | + $cert->certificate[] = base64_encode($certificate); |
|
| 2795 | + } |
|
| 2796 | + |
|
| 2797 | + return $cert; |
|
| 2798 | + } |
|
| 2799 | + |
|
| 2800 | + /** |
|
| 2801 | + * Creates SyncResolveRecipient object for ResolveRecipientsResponse. |
|
| 2802 | + * |
|
| 2803 | + * @param int $type |
|
| 2804 | + * @param string $email |
|
| 2805 | + * @param array $recipientProperties |
|
| 2806 | + * @param int $recipientCount |
|
| 2807 | + * |
|
| 2808 | + * @return SyncResolveRecipient |
|
| 2809 | + */ |
|
| 2810 | + private function createResolveRecipient($type, $email, $recipientProperties, $recipientCount = 0) { |
|
| 2811 | + $recipient = new SyncResolveRecipient(); |
|
| 2812 | + $recipient->type = $type; |
|
| 2813 | + $recipient->displayname = u2w($recipientProperties[PR_DISPLAY_NAME]); |
|
| 2814 | + $recipient->emailaddress = $email; |
|
| 2815 | + |
|
| 2816 | + if ($type == SYNC_RESOLVERECIPIENTS_TYPE_GAL) { |
|
| 2817 | + $certificateProp = PR_EMS_AB_TAGGED_X509_CERT; |
|
| 2818 | + } |
|
| 2819 | + elseif ($type == SYNC_RESOLVERECIPIENTS_TYPE_CONTACT) { |
|
| 2820 | + $certificateProp = PR_USER_X509_CERTIFICATE; |
|
| 2821 | + } |
|
| 2822 | + else { |
|
| 2823 | + $certificateProp = null; |
|
| 2824 | + } |
|
| 2825 | + |
|
| 2826 | + if (isset($recipientProperties[$certificateProp]) && is_array($recipientProperties[$certificateProp]) && !empty($recipientProperties[$certificateProp])) { |
|
| 2827 | + $certificates = $this->getCertificates($recipientProperties[$certificateProp], $recipientCount); |
|
| 2828 | + } |
|
| 2829 | + else { |
|
| 2830 | + $certificates = $this->getCertificates(false); |
|
| 2831 | + SLog::Write(LOGLEVEL_INFO, sprintf("Grommunio->createResolveRecipient(): No certificate found for '%s' (requested email address: '%s')", $recipientProperties[PR_DISPLAY_NAME], $email)); |
|
| 2832 | + } |
|
| 2833 | + $recipient->certificates = $certificates; |
|
| 2834 | + |
|
| 2835 | + if (isset($recipientProperties[PR_ENTRYID])) { |
|
| 2836 | + $recipient->id = $recipientProperties[PR_ENTRYID]; |
|
| 2837 | + } |
|
| 2838 | + |
|
| 2839 | + return $recipient; |
|
| 2840 | + } |
|
| 2841 | + |
|
| 2842 | + /** |
|
| 2843 | + * Gets the availability of a user for the given time window. |
|
| 2844 | + * |
|
| 2845 | + * @param string $to |
|
| 2846 | + * @param SyncResolveRecipient $resolveRecipient |
|
| 2847 | + * @param SyncResolveRecipientsOptions $resolveRecipientsOptions |
|
| 2848 | + * |
|
| 2849 | + * @return SyncResolveRecipientsAvailability |
|
| 2850 | + */ |
|
| 2851 | + private function getAvailability($to, $resolveRecipient, $resolveRecipientsOptions) { |
|
| 2852 | + $availability = new SyncResolveRecipientsAvailability(); |
|
| 2853 | + $availability->status = SYNC_RESOLVERECIPSSTATUS_AVAILABILITY_SUCCESS; |
|
| 2854 | + |
|
| 2855 | + if (!isset($resolveRecipient->id)) { |
|
| 2856 | + // TODO this shouldn't happen but try to get the recipient in such a case |
|
| 2857 | + } |
|
| 2858 | + |
|
| 2859 | + $start = strtotime($resolveRecipientsOptions->availability->starttime); |
|
| 2860 | + $end = strtotime($resolveRecipientsOptions->availability->endtime); |
|
| 2861 | + // Each digit in the MergedFreeBusy indicates the free/busy status for the user for every 30 minute interval. |
|
| 2862 | + $timeslots = intval(ceil(($end - $start) / self::HALFHOURSECONDS)); |
|
| 2863 | + |
|
| 2864 | + if ($timeslots > self::MAXFREEBUSYSLOTS) { |
|
| 2865 | + throw new StatusException("Grommunio->getAvailability(): the requested free busy range is too large.", SYNC_RESOLVERECIPSSTATUS_PROTOCOLERROR); |
|
| 2866 | + } |
|
| 2867 | + |
|
| 2868 | + $mergedFreeBusy = str_pad(fbNoData, $timeslots, fbNoData); |
|
| 2869 | + |
|
| 2870 | + $retval = mapi_getuseravailability($this->session, $resolveRecipient->id, $start, $end); |
|
| 2871 | + SLog::Write(LOGLEVEL_INFO, sprintf("Grommunio->getAvailability(): free busy '%s'", print_r($retval, 1))); |
|
| 2872 | + |
|
| 2873 | + if (!empty($retval)) { |
|
| 2874 | + $freebusy = json_decode($retval, true); |
|
| 2875 | + // freebusy is available, assume that the user is free |
|
| 2876 | + $mergedFreeBusy = str_pad(fbFree, $timeslots, fbFree); |
|
| 2877 | + foreach ($freebusy['events'] as $event) { |
|
| 2878 | + // calculate which timeslot of mergedFreeBusy should be replaced. |
|
| 2879 | + $startSlot = intval(floor(($event['StartTime'] - $start) / self::HALFHOURSECONDS)); |
|
| 2880 | + $endSlot = intval(floor(($event['EndTime'] - $start) / self::HALFHOURSECONDS)); |
|
| 2881 | + // if event started at a multiple of half an hour from requested freebusy time and |
|
| 2882 | + // its duration is also a multiple of half an hour |
|
| 2883 | + // then it's necessary to reduce endSlot by one |
|
| 2884 | + if ((($event['StartTime'] - $start) % self::HALFHOURSECONDS == 0) && (($event['EndTime'] - $event['StartTime']) % self::HALFHOURSECONDS == 0)) { |
|
| 2885 | + --$endSlot; |
|
| 2886 | + } |
|
| 2887 | + $fbType = Utils::GetFbStatusFromType($event['BusyType']); |
|
| 2888 | + for ($i = $startSlot; $i <= $endSlot && $i < $timeslots; ++$i) { |
|
| 2889 | + // only set the new slot's free busy status if it's higher than the current one |
|
| 2890 | + if ($fbType > $mergedFreeBusy[$i]) { |
|
| 2891 | + $mergedFreeBusy[$i] = $fbType; |
|
| 2892 | + } |
|
| 2893 | + } |
|
| 2894 | + } |
|
| 2895 | + } |
|
| 2896 | + $availability->mergedfreebusy = $mergedFreeBusy; |
|
| 2897 | + |
|
| 2898 | + return $availability; |
|
| 2899 | + } |
|
| 2900 | + |
|
| 2901 | + /** |
|
| 2902 | + * Returns contacts matching given email address from a folder. |
|
| 2903 | + * |
|
| 2904 | + * @param MAPIStore $store |
|
| 2905 | + * @param binary $folderEntryid |
|
| 2906 | + * @param string $email |
|
| 2907 | + * |
|
| 2908 | + * @return array|bool |
|
| 2909 | + */ |
|
| 2910 | + private function getContactsFromFolder($store, $folderEntryid, $email) { |
|
| 2911 | + $folder = mapi_msgstore_openentry($store, $folderEntryid); |
|
| 2912 | + $folderContent = mapi_folder_getcontentstable($folder); |
|
| 2913 | + mapi_table_restrict($folderContent, MAPIUtils::GetEmailAddressRestriction($store, $email)); |
|
| 2914 | + // TODO max limit |
|
| 2915 | + if (mapi_table_getrowcount($folderContent) > 0) { |
|
| 2916 | + return mapi_table_queryallrows($folderContent, [PR_DISPLAY_NAME, PR_USER_X509_CERTIFICATE, PR_ENTRYID]); |
|
| 2917 | + } |
|
| 2918 | + |
|
| 2919 | + return false; |
|
| 2920 | + } |
|
| 2921 | + |
|
| 2922 | + /** |
|
| 2923 | + * Get MAPI addressbook object. |
|
| 2924 | + * |
|
| 2925 | + * @return MAPIAddressbook object to be used with mapi_ab_* or false on failure |
|
| 2926 | + */ |
|
| 2927 | + private function getAddressbook() { |
|
| 2928 | + if (isset($this->addressbook) && $this->addressbook) { |
|
| 2929 | + return $this->addressbook; |
|
| 2930 | + } |
|
| 2931 | + $this->addressbook = mapi_openaddressbook($this->session); |
|
| 2932 | + $result = mapi_last_hresult(); |
|
| 2933 | + if ($result && $this->addressbook === false) { |
|
| 2934 | + SLog::Write(LOGLEVEL_ERROR, sprintf("Grommunio->getAddressbook error opening addressbook 0x%X", $result)); |
|
| 2935 | + |
|
| 2936 | + return false; |
|
| 2937 | + } |
|
| 2938 | + |
|
| 2939 | + return $this->addressbook; |
|
| 2940 | + } |
|
| 2941 | + |
|
| 2942 | + /** |
|
| 2943 | + * Checks if the user is not disabled for grommunio-sync. |
|
| 2944 | + * |
|
| 2945 | + * @throws FatalException if user is disabled for grommunio-sync |
|
| 2946 | + * |
|
| 2947 | + * @return bool |
|
| 2948 | + */ |
|
| 2949 | + private function isGSyncEnabled() { |
|
| 2950 | + $addressbook = $this->getAddressbook(); |
|
| 2951 | + // this check needs to be performed on the store of the main (authenticated) user |
|
| 2952 | + $store = $this->storeCache[$this->mainUser]; |
|
| 2953 | + $userEntryid = mapi_getprops($store, [PR_MAILBOX_OWNER_ENTRYID]); |
|
| 2954 | + $mailuser = mapi_ab_openentry($addressbook, $userEntryid[PR_MAILBOX_OWNER_ENTRYID]); |
|
| 2955 | + $enabledFeatures = mapi_getprops($mailuser, [PR_EC_DISABLED_FEATURES]); |
|
| 2956 | + if (isset($enabledFeatures[PR_EC_DISABLED_FEATURES]) && is_array($enabledFeatures[PR_EC_DISABLED_FEATURES])) { |
|
| 2957 | + $mobileDisabled = in_array(self::MOBILE_ENABLED, $enabledFeatures[PR_EC_DISABLED_FEATURES]); |
|
| 2958 | + $deviceId = Request::GetDeviceID(); |
|
| 2959 | + // Checks for deviceId present in zarafaDisabledFeatures LDAP array attribute. Check is performed case insensitive. |
|
| 2960 | + $deviceIdDisabled = (($deviceId !== null) && in_array($deviceId, array_map('strtolower', $enabledFeatures[PR_EC_DISABLED_FEATURES]))) ? true : false; |
|
| 2961 | + if ($mobileDisabled) { |
|
| 2962 | + throw new FatalException("User is disabled for grommunio-sync."); |
|
| 2963 | + } |
|
| 2964 | + if ($deviceIdDisabled) { |
|
| 2965 | + throw new FatalException(sprintf("User has deviceId %s disabled for usage with grommunio-sync.", $deviceId)); |
|
| 2966 | + } |
|
| 2967 | + } |
|
| 2968 | + |
|
| 2969 | + return true; |
|
| 2970 | + } |
|
| 2971 | 2971 | } |
@@ -142,7 +142,7 @@ discard block |
||
| 142 | 142 | if (function_exists('mapi_feature') && mapi_feature('LOGONFLAGS')) { |
| 143 | 143 | // send grommunio-sync version and user agent to ZCP - ZP-589 |
| 144 | 144 | if (Utils::CheckMapiExtVersion('7.2.0')) { |
| 145 | - $gsync_version = 'Grommunio-Sync_' . @constant('GROMMUNIOSYNC_VERSION'); |
|
| 145 | + $gsync_version = 'Grommunio-Sync_'.@constant('GROMMUNIOSYNC_VERSION'); |
|
| 146 | 146 | $user_agent = ($deviceId) ? GSync::GetDeviceManager()->GetUserAgent() : "unknown"; |
| 147 | 147 | $this->session = @mapi_logon_zarafa($this->mainUser, $pass, MAPI_SERVER, null, null, 0, $gsync_version, $user_agent); |
| 148 | 148 | } |
@@ -195,7 +195,7 @@ discard block |
||
| 195 | 195 | $this->store = $this->defaultstore; |
| 196 | 196 | $this->storeName = $defaultUser; |
| 197 | 197 | |
| 198 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->Logon(): User '%s' is authenticated%s", $this->mainUser, ($this->impersonateUser ? " impersonating '" . $this->impersonateUser . "'" : ''))); |
|
| 198 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->Logon(): User '%s' is authenticated%s", $this->mainUser, ($this->impersonateUser ? " impersonating '".$this->impersonateUser."'" : ''))); |
|
| 199 | 199 | |
| 200 | 200 | $this->isGSyncEnabled(); |
| 201 | 201 | |
@@ -589,7 +589,7 @@ discard block |
||
| 589 | 589 | $fwbody = w2u($fwbody); |
| 590 | 590 | } |
| 591 | 591 | |
| 592 | - $mapiprops[$sendMailProps["body"]] = $body . "\r\n\r\n" . $fwbody; |
|
| 592 | + $mapiprops[$sendMailProps["body"]] = $body."\r\n\r\n".$fwbody; |
|
| 593 | 593 | } |
| 594 | 594 | |
| 595 | 595 | if (strlen($bodyHtml) > 0) { |
@@ -605,7 +605,7 @@ discard block |
||
| 605 | 605 | $fwbodyHtml = w2u($fwbodyHtml); |
| 606 | 606 | } |
| 607 | 607 | |
| 608 | - $mapiprops[$sendMailProps["html"]] = $bodyHtml . "<br><br>" . $fwbodyHtml; |
|
| 608 | + $mapiprops[$sendMailProps["html"]] = $bodyHtml."<br><br>".$fwbodyHtml; |
|
| 609 | 609 | } |
| 610 | 610 | } |
| 611 | 611 | } |
@@ -969,11 +969,11 @@ discard block |
||
| 969 | 969 | if ($calFolderId) { |
| 970 | 970 | $shortFolderId = GSync::GetDeviceManager()->GetFolderIdForBackendId($calFolderId); |
| 971 | 971 | if ($calFolderId != $shortFolderId) { |
| 972 | - $prefix = $shortFolderId . ':'; |
|
| 972 | + $prefix = $shortFolderId.':'; |
|
| 973 | 973 | } |
| 974 | 974 | } |
| 975 | 975 | |
| 976 | - return $prefix . $calendarid; |
|
| 976 | + return $prefix.$calendarid; |
|
| 977 | 977 | } |
| 978 | 978 | |
| 979 | 979 | /** |
@@ -1321,7 +1321,7 @@ discard block |
||
| 1321 | 1321 | } |
| 1322 | 1322 | } |
| 1323 | 1323 | $nrResults = count($items); |
| 1324 | - $items['range'] = ($nrResults > 0) ? $rangestart . '-' . ($nrResults - 1) : '0-0'; |
|
| 1324 | + $items['range'] = ($nrResults > 0) ? $rangestart.'-'.($nrResults - 1) : '0-0'; |
|
| 1325 | 1325 | $items['searchtotal'] = $nrResults; |
| 1326 | 1326 | |
| 1327 | 1327 | return $items; |
@@ -1372,8 +1372,7 @@ discard block |
||
| 1372 | 1372 | |
| 1373 | 1373 | // if the search range is set limit the result to it, otherwise return all found messages |
| 1374 | 1374 | $rows = (is_array($searchRange) && isset($searchRange[0], $searchRange[1])) ? |
| 1375 | - mapi_table_queryrows($table, [PR_ENTRYID], $searchRange[0], $searchRange[1] - $searchRange[0] + 1) : |
|
| 1376 | - mapi_table_queryrows($table, [PR_ENTRYID], 0, SEARCH_MAXRESULTS); |
|
| 1375 | + mapi_table_queryrows($table, [PR_ENTRYID], $searchRange[0], $searchRange[1] - $searchRange[0] + 1) : mapi_table_queryrows($table, [PR_ENTRYID], 0, SEARCH_MAXRESULTS); |
|
| 1377 | 1376 | |
| 1378 | 1377 | $cnt = count($rows); |
| 1379 | 1378 | $items['searchtotal'] = $cnt; |
@@ -1423,7 +1422,7 @@ discard block |
||
| 1423 | 1422 | [ |
| 1424 | 1423 | FUZZYLEVEL => FL_PREFIX, |
| 1425 | 1424 | ULPROPTAG => PR_DISPLAY_NAME, |
| 1426 | - VALUE => [PR_DISPLAY_NAME => "grommunio-sync Search Folder " . $pid], |
|
| 1425 | + VALUE => [PR_DISPLAY_NAME => "grommunio-sync Search Folder ".$pid], |
|
| 1427 | 1426 | ], |
| 1428 | 1427 | ], |
| 1429 | 1428 | TBL_BATCH |
@@ -1567,7 +1566,7 @@ discard block |
||
| 1567 | 1566 | $content_unread = isset($folder[PR_CONTENT_UNREAD]) ? $folder[PR_CONTENT_UNREAD] : -1; |
| 1568 | 1567 | $content_deleted = isset($folder[PR_DELETED_MSG_COUNT]) ? $folder[PR_DELETED_MSG_COUNT] : -1; |
| 1569 | 1568 | |
| 1570 | - $this->folderStatCache[$user][bin2hex($folder[PR_SOURCE_KEY])] = $commit_time . "/" . $content_count . "/" . $content_unread . "/" . $content_deleted; |
|
| 1569 | + $this->folderStatCache[$user][bin2hex($folder[PR_SOURCE_KEY])] = $commit_time."/".$content_count."/".$content_unread."/".$content_deleted; |
|
| 1571 | 1570 | } |
| 1572 | 1571 | SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetFolderStat() fetched status information of %d folders for store '%s'", count($this->folderStatCache[$user]), $user)); |
| 1573 | 1572 | } |
@@ -1749,7 +1748,7 @@ discard block |
||
| 1749 | 1748 | )); |
| 1750 | 1749 | } |
| 1751 | 1750 | } |
| 1752 | - $messageName = rtrim((($key !== false) ? $key . "-" : "") . (($type !== "") ? $type : ""), "-"); |
|
| 1751 | + $messageName = rtrim((($key !== false) ? $key."-" : "").(($type !== "") ? $type : ""), "-"); |
|
| 1753 | 1752 | $restriction = $this->getStateMessageRestriction($messageName, $counter, $thisCounterOnly); |
| 1754 | 1753 | $stateFolderContents = mapi_folder_getcontentstable($this->stateFolder, MAPI_ASSOCIATED); |
| 1755 | 1754 | if ($stateFolderContents) { |
@@ -1923,7 +1922,7 @@ discard block |
||
| 1923 | 1922 | )); |
| 1924 | 1923 | } |
| 1925 | 1924 | } |
| 1926 | - $messageName = rtrim((($key !== false) ? $key . "-" : "") . (($type !== "") ? $type : ""), "-"); |
|
| 1925 | + $messageName = rtrim((($key !== false) ? $key."-" : "").(($type !== "") ? $type : ""), "-"); |
|
| 1927 | 1926 | $restriction = $this->getStateMessageRestriction($messageName, $counter, true); |
| 1928 | 1927 | $stateFolderContents = mapi_folder_getcontentstable($this->stateFolder, MAPI_ASSOCIATED); |
| 1929 | 1928 | if ($stateFolderContents) { |
@@ -1972,17 +1971,17 @@ discard block |
||
| 1972 | 1971 | $stateMessage = mapi_folder_createmessage($this->stateFolder, MAPI_ASSOCIATED); |
| 1973 | 1972 | SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->setStateMessage(): mapi_folder_createmessage 0x%08X", mapi_last_hresult())); |
| 1974 | 1973 | |
| 1975 | - $messageName = rtrim((($key !== false) ? $key . "-" : "") . (($type !== "") ? $type : ""), "-"); |
|
| 1974 | + $messageName = rtrim((($key !== false) ? $key."-" : "").(($type !== "") ? $type : ""), "-"); |
|
| 1976 | 1975 | SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->setStateMessage(): creating new state message '%s-%d'", $messageName, is_int($counter) ? $counter : 0)); |
| 1977 | 1976 | mapi_setprops($stateMessage, [PR_DISPLAY_NAME => $messageName, PR_MESSAGE_CLASS => 'IPM.Note.GrommunioState']); |
| 1978 | 1977 | } |
| 1979 | 1978 | if (isset($stateMessage)) { |
| 1980 | - $jsonEncodedState = is_object($state) || is_array($state) ? json_encode($state, JSON_INVALID_UTF8_IGNORE | JSON_UNESCAPED_UNICODE) : $state; |
|
| 1979 | + $jsonEncodedState = is_object($state) || is_array($state) ? json_encode($state, JSON_INVALID_UTF8_IGNORE|JSON_UNESCAPED_UNICODE) : $state; |
|
| 1981 | 1980 | |
| 1982 | 1981 | $encodedState = base64_encode($jsonEncodedState); |
| 1983 | 1982 | $encodedStateLength = strlen($encodedState); |
| 1984 | 1983 | mapi_setprops($stateMessage, [PR_LAST_VERB_EXECUTED => is_int($counter) ? $counter : 0]); |
| 1985 | - $stream = mapi_openproperty($stateMessage, PR_BODY, IID_IStream, STGM_DIRECT, MAPI_CREATE | MAPI_MODIFY); |
|
| 1984 | + $stream = mapi_openproperty($stateMessage, PR_BODY, IID_IStream, STGM_DIRECT, MAPI_CREATE|MAPI_MODIFY); |
|
| 1986 | 1985 | mapi_stream_setsize($stream, $encodedStateLength); |
| 1987 | 1986 | mapi_stream_write($stream, $encodedState); |
| 1988 | 1987 | mapi_stream_commit($stream); |
@@ -2077,7 +2076,7 @@ discard block |
||
| 2077 | 2076 | private function adviseStoreToSink($store) { |
| 2078 | 2077 | // check if we already advised the store |
| 2079 | 2078 | if (!in_array($store, $this->changesSinkStores)) { |
| 2080 | - mapi_msgstore_advise($store, null, fnevObjectModified | fnevObjectCreated | fnevObjectMoved | fnevObjectDeleted, $this->changesSink); |
|
| 2079 | + mapi_msgstore_advise($store, null, fnevObjectModified|fnevObjectCreated|fnevObjectMoved|fnevObjectDeleted, $this->changesSink); |
|
| 2081 | 2080 | |
| 2082 | 2081 | if (mapi_last_hresult()) { |
| 2083 | 2082 | SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->adviseStoreToSink(): failed to advised store '%s' with code 0x%X. Polling will be performed.", $store, mapi_last_hresult())); |
@@ -2502,7 +2501,7 @@ discard block |
||
| 2502 | 2501 | * @return mapiFolderObject |
| 2503 | 2502 | */ |
| 2504 | 2503 | private function createSearchFolder($searchFolderRoot) { |
| 2505 | - $folderName = "grommunio-sync Search Folder " . @getmypid(); |
|
| 2504 | + $folderName = "grommunio-sync Search Folder ".@getmypid(); |
|
| 2506 | 2505 | $searchFolders = mapi_folder_gethierarchytable($searchFolderRoot); |
| 2507 | 2506 | $restriction = [ |
| 2508 | 2507 | RES_CONTENT, |
@@ -2551,7 +2550,7 @@ discard block |
||
| 2551 | 2550 | $resOr, |
| 2552 | 2551 | [RES_CONTENT, |
| 2553 | 2552 | [ |
| 2554 | - FUZZYLEVEL => FL_SUBSTRING | FL_IGNORECASE, |
|
| 2553 | + FUZZYLEVEL => FL_SUBSTRING|FL_IGNORECASE, |
|
| 2555 | 2554 | ULPROPTAG => $property, |
| 2556 | 2555 | VALUE => u2w($term), |
| 2557 | 2556 | ], |
@@ -130,8 +130,7 @@ discard block |
||
| 130 | 130 | if ($this->impersonateUser !== false) { |
| 131 | 131 | SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->Logon(): Impersonation active - authenticating: '%s' - impersonating '%s'", $this->mainUser, $this->impersonateUser)); |
| 132 | 132 | $defaultUser = $this->impersonateUser; |
| 133 | - } |
|
| 134 | - else { |
|
| 133 | + } else { |
|
| 135 | 134 | $defaultUser = $this->mainUser; |
| 136 | 135 | } |
| 137 | 136 | |
@@ -145,8 +144,7 @@ discard block |
||
| 145 | 144 | $gsync_version = 'Grommunio-Sync_' . @constant('GROMMUNIOSYNC_VERSION'); |
| 146 | 145 | $user_agent = ($deviceId) ? GSync::GetDeviceManager()->GetUserAgent() : "unknown"; |
| 147 | 146 | $this->session = @mapi_logon_zarafa($this->mainUser, $pass, MAPI_SERVER, null, null, 0, $gsync_version, $user_agent); |
| 148 | - } |
|
| 149 | - else { |
|
| 147 | + } else { |
|
| 150 | 148 | $this->session = @mapi_logon_zarafa($this->mainUser, $pass, MAPI_SERVER, null, null, 0); |
| 151 | 149 | } |
| 152 | 150 | $this->notifications = true; |
@@ -163,8 +161,7 @@ discard block |
||
| 163 | 161 | throw new ServiceUnavailableException("Error connecting to KC (login)"); |
| 164 | 162 | } |
| 165 | 163 | } |
| 166 | - } |
|
| 167 | - catch (MAPIException $ex) { |
|
| 164 | + } catch (MAPIException $ex) { |
|
| 168 | 165 | throw new AuthenticationRequiredException($ex->getDisplayMessage()); |
| 169 | 166 | } |
| 170 | 167 | |
@@ -266,8 +263,7 @@ discard block |
||
| 266 | 263 | $storeProps = mapi_getprops($userstore, [PR_IPM_SUBTREE_ENTRYID]); |
| 267 | 264 | $rights = $this->HasSecretaryACLs($userstore, '', $storeProps[PR_IPM_SUBTREE_ENTRYID]); |
| 268 | 265 | SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->Setup(): Checking for secretary ACLs on root folder of impersonated store '%s': '%s'", $user, Utils::PrintAsString($rights))); |
| 269 | - } |
|
| 270 | - else { |
|
| 266 | + } else { |
|
| 271 | 267 | $zarafauserinfo = @nsp_getuserinfo($this->mainUser); |
| 272 | 268 | $rights = (isset($zarafauserinfo['admin']) && $zarafauserinfo['admin']) ? true : false; |
| 273 | 269 | SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->Setup(): Checking for admin ACLs on store '%s': '%s'", $user, Utils::PrintAsString($rights))); |
@@ -330,8 +326,7 @@ discard block |
||
| 330 | 326 | // for SYSTEM user open the public folders |
| 331 | 327 | if (strtoupper($this->storeName) == "SYSTEM") { |
| 332 | 328 | $rootfolder = mapi_msgstore_openentry($this->store, $storeProps[PR_IPM_PUBLIC_FOLDERS_ENTRYID]); |
| 333 | - } |
|
| 334 | - else { |
|
| 329 | + } else { |
|
| 335 | 330 | $rootfolder = mapi_msgstore_openentry($this->store); |
| 336 | 331 | } |
| 337 | 332 | |
@@ -354,8 +349,7 @@ discard block |
||
| 354 | 349 | $folder = $mapiprovider->GetFolder($row); |
| 355 | 350 | if ($folder) { |
| 356 | 351 | $folders[] = $folder; |
| 357 | - } |
|
| 358 | - else { |
|
| 352 | + } else { |
|
| 359 | 353 | SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->GetHierarchy(): ignoring folder '%s' as MAPIProvider->GetFolder() did not return a SyncFolder object", (isset($row[PR_DISPLAY_NAME]) ? $row[PR_DISPLAY_NAME] : "unknown"))); |
| 360 | 354 | } |
| 361 | 355 | } |
@@ -547,8 +541,7 @@ discard block |
||
| 547 | 541 | PR_ICON_INDEX => 262, |
| 548 | 542 | PR_LAST_VERB_EXECUTED => 104, |
| 549 | 543 | ]; |
| 550 | - } |
|
| 551 | - elseif ($sm->replyflag) { |
|
| 544 | + } elseif ($sm->replyflag) { |
|
| 552 | 545 | $updateProps = [ |
| 553 | 546 | PR_ICON_INDEX => 261, |
| 554 | 547 | PR_LAST_VERB_EXECUTED => 102, |
@@ -608,8 +601,7 @@ discard block |
||
| 608 | 601 | $mapiprops[$sendMailProps["html"]] = $bodyHtml . "<br><br>" . $fwbodyHtml; |
| 609 | 602 | } |
| 610 | 603 | } |
| 611 | - } |
|
| 612 | - else { |
|
| 604 | + } else { |
|
| 613 | 605 | // no fwmessage could be opened and we need it because we do not replace mime |
| 614 | 606 | if (!isset($sm->replacemime) || $sm->replacemime == false) { |
| 615 | 607 | throw new StatusException(sprintf("Grommunio->SendMail(): Could not open message id '%s' in folder id '%s' to be replied/forwarded: 0x%X", $sm->source->itemid, $sm->source->folderid, mapi_last_hresult()), SYNC_COMMONSTATUS_ITEMNOTFOUND); |
@@ -657,8 +649,7 @@ discard block |
||
| 657 | 649 | if (!$folderid) { |
| 658 | 650 | $entryid = hex2bin($id); |
| 659 | 651 | $sk = $id; |
| 660 | - } |
|
| 661 | - else { |
|
| 652 | + } else { |
|
| 662 | 653 | // id might be in the new longid format, so we have to split it here |
| 663 | 654 | list($fsk, $sk) = Utils::SplitMessageId($id); |
| 664 | 655 | // get the entry id of the message |
@@ -751,8 +742,7 @@ discard block |
||
| 751 | 742 | $stream = mapi_inetmapi_imtoinet($this->session, $addrbook, $embMessage, ['use_tnef' => -1]); |
| 752 | 743 | // set the default contenttype for this kind of messages |
| 753 | 744 | $attachment->contenttype = "message/rfc822"; |
| 754 | - } |
|
| 755 | - else { |
|
| 745 | + } else { |
|
| 756 | 746 | $stream = mapi_openproperty($attach, PR_ATTACH_DATA_BIN, IID_IStream, 0, 0); |
| 757 | 747 | } |
| 758 | 748 | |
@@ -764,8 +754,7 @@ discard block |
||
| 764 | 754 | $attachment->data = MAPIStreamWrapper::Open($stream); |
| 765 | 755 | if (isset($attprops[PR_ATTACH_MIME_TAG])) { |
| 766 | 756 | $attachment->contenttype = $attprops[PR_ATTACH_MIME_TAG]; |
| 767 | - } |
|
| 768 | - elseif (isset($attprops[PR_ATTACH_MIME_TAG_W])) { |
|
| 757 | + } elseif (isset($attprops[PR_ATTACH_MIME_TAG_W])) { |
|
| 769 | 758 | $attachment->contenttype = $attprops[PR_ATTACH_MIME_TAG_W]; |
| 770 | 759 | } |
| 771 | 760 | // TODO default contenttype |
@@ -1164,8 +1153,7 @@ discard block |
||
| 1164 | 1153 | } |
| 1165 | 1154 | ++$response->recipientcount; |
| 1166 | 1155 | $response->recipient[] = $entry; |
| 1167 | - } |
|
| 1168 | - elseif (is_int($recipient)) { |
|
| 1156 | + } elseif (is_int($recipient)) { |
|
| 1169 | 1157 | $response->status = $recipient; |
| 1170 | 1158 | } |
| 1171 | 1159 | } |
@@ -1660,8 +1648,7 @@ discard block |
||
| 1660 | 1648 | if (isset($stateMessageProps[PR_LAST_MODIFICATION_TIME])) { |
| 1661 | 1649 | return $stateMessageProps[PR_LAST_MODIFICATION_TIME]; |
| 1662 | 1650 | } |
| 1663 | - } |
|
| 1664 | - catch (StateNotFoundException $e) { |
|
| 1651 | + } catch (StateNotFoundException $e) { |
|
| 1665 | 1652 | } |
| 1666 | 1653 | |
| 1667 | 1654 | return "0"; |
@@ -1859,8 +1846,7 @@ discard block |
||
| 1859 | 1846 | if ($devid) { |
| 1860 | 1847 | $this->setDeviceUserData($this->userDeviceData, bin2hex($hierarchyRows[0][PR_ENTRYID]), $devid, $this->mainUser, "statefolder"); |
| 1861 | 1848 | } |
| 1862 | - } |
|
| 1863 | - elseif ($rowCnt == 0) { |
|
| 1849 | + } elseif ($rowCnt == 0) { |
|
| 1864 | 1850 | // legacy code: create the hidden state folder and the device subfolder |
| 1865 | 1851 | // this should happen when the user configures the device (autodiscover or first sync if no autodiscover) |
| 1866 | 1852 | |
@@ -1872,8 +1858,7 @@ discard block |
||
| 1872 | 1858 | if ($rowCnt == 1) { |
| 1873 | 1859 | $hierarchyRows = mapi_table_queryrows($hierarchy, [PR_ENTRYID], 0, 1); |
| 1874 | 1860 | $stateFolder = mapi_msgstore_openentry($this->store, $hierarchyRows[0][PR_ENTRYID]); |
| 1875 | - } |
|
| 1876 | - elseif ($rowCnt == 0) { |
|
| 1861 | + } elseif ($rowCnt == 0) { |
|
| 1877 | 1862 | $stateFolder = mapi_folder_createfolder($rootfolder, STORE_STATE_FOLDER, ""); |
| 1878 | 1863 | mapi_setprops($stateFolder, [PR_ATTR_HIDDEN => true]); |
| 1879 | 1864 | } |
@@ -1966,8 +1951,7 @@ discard block |
||
| 1966 | 1951 | |
| 1967 | 1952 | try { |
| 1968 | 1953 | $stateMessage = $this->getStateMessage($devid, $type, $key, $counter); |
| 1969 | - } |
|
| 1970 | - catch (StateNotFoundException $e) { |
|
| 1954 | + } catch (StateNotFoundException $e) { |
|
| 1971 | 1955 | // if message is not available, try to create a new one |
| 1972 | 1956 | $stateMessage = mapi_folder_createmessage($this->stateFolder, MAPI_ASSOCIATED); |
| 1973 | 1957 | SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->setStateMessage(): mapi_folder_createmessage 0x%08X", mapi_last_hresult())); |
@@ -2136,8 +2120,7 @@ discard block |
||
| 2136 | 2120 | } |
| 2137 | 2121 | } |
| 2138 | 2122 | } |
| 2139 | - } |
|
| 2140 | - else { |
|
| 2123 | + } else { |
|
| 2141 | 2124 | $entryid = @mapi_msgstore_createentryid($this->defaultstore, $user); |
| 2142 | 2125 | } |
| 2143 | 2126 | |
@@ -2215,8 +2198,7 @@ discard block |
||
| 2215 | 2198 | // if oof state is set it must be set of oof and get otherwise |
| 2216 | 2199 | if (isset($oof->oofstate)) { |
| 2217 | 2200 | $this->settingsOofSet($oof); |
| 2218 | - } |
|
| 2219 | - else { |
|
| 2201 | + } else { |
|
| 2220 | 2202 | $this->settingsOofGet($oof); |
| 2221 | 2203 | } |
| 2222 | 2204 | } |
@@ -2251,13 +2233,11 @@ discard block |
||
| 2251 | 2233 | @mapi_setprops($this->defaultstore, [PR_EC_OUTOFOFFICE => false]); |
| 2252 | 2234 | @mapi_deleteprops($this->defaultstore, [PR_EC_OUTOFOFFICE_FROM, PR_EC_OUTOFOFFICE_UNTIL]); |
| 2253 | 2235 | SLog::Write(LOGLEVEL_INFO, "Grommunio->settingsOofGet(): Out of office is set but the from and until are in the past. Disabling out of office."); |
| 2254 | - } |
|
| 2255 | - elseif ($oofprops[PR_EC_OUTOFOFFICE_FROM] < $oofprops[PR_EC_OUTOFOFFICE_UNTIL]) { |
|
| 2236 | + } elseif ($oofprops[PR_EC_OUTOFOFFICE_FROM] < $oofprops[PR_EC_OUTOFOFFICE_UNTIL]) { |
|
| 2256 | 2237 | $oof->oofstate = SYNC_SETTINGSOOF_TIMEBASED; |
| 2257 | 2238 | $oof->starttime = $oofprops[PR_EC_OUTOFOFFICE_FROM]; |
| 2258 | 2239 | $oof->endtime = $oofprops[PR_EC_OUTOFOFFICE_UNTIL]; |
| 2259 | - } |
|
| 2260 | - else { |
|
| 2240 | + } else { |
|
| 2261 | 2241 | SLog::Write(LOGLEVEL_WARN, sprintf( |
| 2262 | 2242 | "Grommunio->settingsOofGet(): Time based out of office set but end time ('%s') is before startime ('%s').", |
| 2263 | 2243 | date("Y-m-d H:i:s", $oofprops[PR_EC_OUTOFOFFICE_FROM]), |
@@ -2265,8 +2245,7 @@ discard block |
||
| 2265 | 2245 | )); |
| 2266 | 2246 | $oof->Status = SYNC_SETTINGSSTATUS_PROTOCOLLERROR; |
| 2267 | 2247 | } |
| 2268 | - } |
|
| 2269 | - elseif ($oof->oofstate == SYNC_SETTINGSOOF_GLOBAL && (isset($oofprops[PR_EC_OUTOFOFFICE_FROM]) || isset($oofprops[PR_EC_OUTOFOFFICE_UNTIL]))) { |
|
| 2248 | + } elseif ($oof->oofstate == SYNC_SETTINGSOOF_GLOBAL && (isset($oofprops[PR_EC_OUTOFOFFICE_FROM]) || isset($oofprops[PR_EC_OUTOFOFFICE_UNTIL]))) { |
|
| 2270 | 2249 | SLog::Write(LOGLEVEL_WARN, sprintf( |
| 2271 | 2250 | "Grommunio->settingsOofGet(): Time based out of office set but either start time ('%s') or end time ('%s') is missing.", |
| 2272 | 2251 | (isset($oofprops[PR_EC_OUTOFOFFICE_FROM]) ? date("Y-m-d H:i:s", $oofprops[PR_EC_OUTOFOFFICE_FROM]) : 'empty'), |
@@ -2274,8 +2253,7 @@ discard block |
||
| 2274 | 2253 | )); |
| 2275 | 2254 | $oof->Status = SYNC_SETTINGSSTATUS_PROTOCOLLERROR; |
| 2276 | 2255 | } |
| 2277 | - } |
|
| 2278 | - else { |
|
| 2256 | + } else { |
|
| 2279 | 2257 | SLog::Write(LOGLEVEL_WARN, "Grommunio->Unable to get out of office information"); |
| 2280 | 2258 | } |
| 2281 | 2259 | |
@@ -2303,16 +2281,13 @@ discard block |
||
| 2303 | 2281 | if (isset($oof->starttime, $oof->endtime)) { |
| 2304 | 2282 | $props[PR_EC_OUTOFOFFICE_FROM] = $oof->starttime; |
| 2305 | 2283 | $props[PR_EC_OUTOFOFFICE_UNTIL] = $oof->endtime; |
| 2306 | - } |
|
| 2307 | - elseif (isset($oof->starttime) || isset($oof->endtime)) { |
|
| 2284 | + } elseif (isset($oof->starttime) || isset($oof->endtime)) { |
|
| 2308 | 2285 | $oof->Status = SYNC_SETTINGSSTATUS_PROTOCOLLERROR; |
| 2309 | 2286 | } |
| 2310 | - } |
|
| 2311 | - else { |
|
| 2287 | + } else { |
|
| 2312 | 2288 | $deleteProps = [PR_EC_OUTOFOFFICE_FROM, PR_EC_OUTOFOFFICE_UNTIL]; |
| 2313 | 2289 | } |
| 2314 | - } |
|
| 2315 | - elseif ($oof->oofstate == SYNC_SETTINGSOOF_DISABLED) { |
|
| 2290 | + } elseif ($oof->oofstate == SYNC_SETTINGSOOF_DISABLED) { |
|
| 2316 | 2291 | $props[PR_EC_OUTOFOFFICE] = false; |
| 2317 | 2292 | $deleteProps = [PR_EC_OUTOFOFFICE_FROM, PR_EC_OUTOFOFFICE_UNTIL]; |
| 2318 | 2293 | } |
@@ -2355,8 +2330,7 @@ discard block |
||
| 2355 | 2330 | $emailaddresses->primarysmtpaddress = $user["primary_email"]; |
| 2356 | 2331 | $account->emailaddresses = $emailaddresses; |
| 2357 | 2332 | $userinformation->accounts[] = $account; |
| 2358 | - } |
|
| 2359 | - else { |
|
| 2333 | + } else { |
|
| 2360 | 2334 | $userinformation->emailaddresses[] = $user["primary_email"]; |
| 2361 | 2335 | } |
| 2362 | 2336 | |
@@ -2634,13 +2608,11 @@ discard block |
||
| 2634 | 2608 | // some devices request 0 ambiguous recipients |
| 2635 | 2609 | if ($querycnt == 1 && $maxAmbiguousRecipients == 0) { |
| 2636 | 2610 | $rowsToQuery = 1; |
| 2637 | - } |
|
| 2638 | - elseif ($querycnt > 1 && $maxAmbiguousRecipients == 0) { |
|
| 2611 | + } elseif ($querycnt > 1 && $maxAmbiguousRecipients == 0) { |
|
| 2639 | 2612 | SLog::Write(LOGLEVEL_INFO, sprintf("Grommunio->resolveRecipientGAL(): GAL search found %d recipients but the device hasn't requested ambiguous recipients", $querycnt)); |
| 2640 | 2613 | |
| 2641 | 2614 | return $recipientGal; |
| 2642 | - } |
|
| 2643 | - elseif ($querycnt > 1 && $maxAmbiguousRecipients == 1) { |
|
| 2615 | + } elseif ($querycnt > 1 && $maxAmbiguousRecipients == 1) { |
|
| 2644 | 2616 | $rowsToQuery = $querycnt; |
| 2645 | 2617 | } |
| 2646 | 2618 | // get the certificate every time because caching the certificate is less expensive than opening addressbook entry again |
@@ -2662,13 +2634,11 @@ discard block |
||
| 2662 | 2634 | SLog::Write(LOGLEVEL_WBXML, sprintf("Grommunio->resolveRecipientGAL(): distlist's '%s' member: '%s'", $to, $distListMembers[$j][PR_DISPLAY_NAME])); |
| 2663 | 2635 | $recipientGal[] = $this->createResolveRecipient(SYNC_RESOLVERECIPIENTS_TYPE_GAL, $to, $distListMembers[$j], $nrDistListMembers); |
| 2664 | 2636 | } |
| 2665 | - } |
|
| 2666 | - else { |
|
| 2637 | + } else { |
|
| 2667 | 2638 | SLog::Write(LOGLEVEL_DEBUG, sprintf("Grommunio->resolveRecipientGAL(): '%s' is a dist list, but return it as is.", $to)); |
| 2668 | 2639 | $recipientGal[] = $this->createResolveRecipient(SYNC_RESOLVERECIPIENTS_TYPE_GAL, $abentries[$i][PR_SMTP_ADDRESS], $abentries[$i]); |
| 2669 | 2640 | } |
| 2670 | - } |
|
| 2671 | - elseif ($abentries[$i][PR_OBJECT_TYPE] == MAPI_MAILUSER) { |
|
| 2641 | + } elseif ($abentries[$i][PR_OBJECT_TYPE] == MAPI_MAILUSER) { |
|
| 2672 | 2642 | $recipientGal[] = $this->createResolveRecipient(SYNC_RESOLVERECIPIENTS_TYPE_GAL, $abentries[$i][PR_SMTP_ADDRESS], $abentries[$i]); |
| 2673 | 2643 | } |
| 2674 | 2644 | } |
@@ -2755,8 +2725,7 @@ discard block |
||
| 2755 | 2725 | break; |
| 2756 | 2726 | } |
| 2757 | 2727 | } |
| 2758 | - } |
|
| 2759 | - else { |
|
| 2728 | + } else { |
|
| 2760 | 2729 | SLog::Write(LOGLEVEL_WARN, sprintf("Grommunio->resolveRecipientContact(): Unable to open public store: 0x%X", $result)); |
| 2761 | 2730 | } |
| 2762 | 2731 | |
@@ -2815,18 +2784,15 @@ discard block |
||
| 2815 | 2784 | |
| 2816 | 2785 | if ($type == SYNC_RESOLVERECIPIENTS_TYPE_GAL) { |
| 2817 | 2786 | $certificateProp = PR_EMS_AB_TAGGED_X509_CERT; |
| 2818 | - } |
|
| 2819 | - elseif ($type == SYNC_RESOLVERECIPIENTS_TYPE_CONTACT) { |
|
| 2787 | + } elseif ($type == SYNC_RESOLVERECIPIENTS_TYPE_CONTACT) { |
|
| 2820 | 2788 | $certificateProp = PR_USER_X509_CERTIFICATE; |
| 2821 | - } |
|
| 2822 | - else { |
|
| 2789 | + } else { |
|
| 2823 | 2790 | $certificateProp = null; |
| 2824 | 2791 | } |
| 2825 | 2792 | |
| 2826 | 2793 | if (isset($recipientProperties[$certificateProp]) && is_array($recipientProperties[$certificateProp]) && !empty($recipientProperties[$certificateProp])) { |
| 2827 | 2794 | $certificates = $this->getCertificates($recipientProperties[$certificateProp], $recipientCount); |
| 2828 | - } |
|
| 2829 | - else { |
|
| 2795 | + } else { |
|
| 2830 | 2796 | $certificates = $this->getCertificates(false); |
| 2831 | 2797 | SLog::Write(LOGLEVEL_INFO, sprintf("Grommunio->createResolveRecipient(): No certificate found for '%s' (requested email address: '%s')", $recipientProperties[PR_DISPLAY_NAME], $email)); |
| 2832 | 2798 | } |
@@ -9,171 +9,171 @@ |
||
| 9 | 9 | */ |
| 10 | 10 | |
| 11 | 11 | class MAPIStreamWrapper { |
| 12 | - public const PROTOCOL = "mapistream"; |
|
| 13 | - |
|
| 14 | - private $mapistream; |
|
| 15 | - private $position; |
|
| 16 | - private $streamlength; |
|
| 17 | - private $toTruncate; |
|
| 18 | - private $truncateHtmlSafe; |
|
| 19 | - private $context; |
|
| 20 | - |
|
| 21 | - /** |
|
| 22 | - * Opens the stream |
|
| 23 | - * The mapistream reference is passed over the context. |
|
| 24 | - * |
|
| 25 | - * @param string $path Specifies the URL that was passed to the original function |
|
| 26 | - * @param string $mode The mode used to open the file, as detailed for fopen() |
|
| 27 | - * @param int $options Holds additional flags set by the streams API |
|
| 28 | - * @param string $opened_path if the path is opened successfully, and STREAM_USE_PATH is set in options, |
|
| 29 | - * opened_path should be set to the full path of the file/resource that was actually opened |
|
| 30 | - * |
|
| 31 | - * @return bool |
|
| 32 | - */ |
|
| 33 | - public function stream_open($path, $mode, $options, &$opened_path) { |
|
| 34 | - $contextOptions = stream_context_get_options($this->context); |
|
| 35 | - if (!isset($contextOptions[self::PROTOCOL]['stream'])) { |
|
| 36 | - return false; |
|
| 37 | - } |
|
| 38 | - |
|
| 39 | - $this->position = 0; |
|
| 40 | - $this->toTruncate = false; |
|
| 41 | - $this->truncateHtmlSafe = (isset($contextOptions[self::PROTOCOL]['truncatehtmlsafe'])) ? $contextOptions[self::PROTOCOL]['truncatehtmlsafe'] : false; |
|
| 42 | - |
|
| 43 | - // this is our stream! |
|
| 44 | - $this->mapistream = $contextOptions[self::PROTOCOL]['stream']; |
|
| 45 | - |
|
| 46 | - // get the data length from mapi |
|
| 47 | - if ($this->mapistream) { |
|
| 48 | - $stat = mapi_stream_stat($this->mapistream); |
|
| 49 | - $this->streamlength = $stat["cb"]; |
|
| 50 | - } |
|
| 51 | - else { |
|
| 52 | - $this->streamlength = 0; |
|
| 53 | - } |
|
| 54 | - |
|
| 55 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIStreamWrapper::stream_open(): initialized mapistream: %s - streamlength: %d - HTML-safe-truncate: %s", $this->mapistream, $this->streamlength, Utils::PrintAsString($this->truncateHtmlSafe))); |
|
| 56 | - |
|
| 57 | - return true; |
|
| 58 | - } |
|
| 59 | - |
|
| 60 | - /** |
|
| 61 | - * Reads from stream. |
|
| 62 | - * |
|
| 63 | - * @param int $len amount of bytes to be read |
|
| 64 | - * |
|
| 65 | - * @return string |
|
| 66 | - */ |
|
| 67 | - public function stream_read($len) { |
|
| 68 | - $len = ($this->position + $len > $this->streamlength) ? ($this->streamlength - $this->position) : $len; |
|
| 69 | - |
|
| 70 | - // read 4 additional bytes from the stream so we can always truncate correctly |
|
| 71 | - if ($this->toTruncate && $this->position + $len >= $this->streamlength) { |
|
| 72 | - $len += 4; |
|
| 73 | - } |
|
| 74 | - if ($this->mapistream) { |
|
| 75 | - $data = mapi_stream_read($this->mapistream, $len); |
|
| 76 | - } |
|
| 77 | - else { |
|
| 78 | - $data = ""; |
|
| 79 | - } |
|
| 80 | - $this->position += strlen($data); |
|
| 81 | - |
|
| 82 | - // we need to truncate UTF8 compatible if ftruncate() was called |
|
| 83 | - if ($this->toTruncate && $this->position >= $this->streamlength) { |
|
| 84 | - $data = Utils::Utf8_truncate($data, $this->streamlength, $this->truncateHtmlSafe); |
|
| 85 | - } |
|
| 86 | - |
|
| 87 | - return $data; |
|
| 88 | - } |
|
| 89 | - |
|
| 90 | - /** |
|
| 91 | - * Stream "seek" functionality. |
|
| 92 | - * |
|
| 93 | - * @param int $offset |
|
| 94 | - * @param int $whence |
|
| 95 | - * |
|
| 96 | - * @return bool |
|
| 97 | - */ |
|
| 98 | - public function stream_seek($offset, $whence = SEEK_SET) { |
|
| 99 | - switch ($whence) { |
|
| 100 | - case SEEK_SET: |
|
| 101 | - $mapiWhence = STREAM_SEEK_SET; |
|
| 102 | - break; |
|
| 103 | - |
|
| 104 | - case SEEK_END: |
|
| 105 | - $mapiWhence = STREAM_SEEK_END; |
|
| 106 | - break; |
|
| 107 | - |
|
| 108 | - default: |
|
| 109 | - $mapiWhence = STREAM_SEEK_CUR; |
|
| 110 | - } |
|
| 111 | - |
|
| 112 | - return mapi_stream_seek($this->mapistream, $offset, $mapiWhence); |
|
| 113 | - } |
|
| 114 | - |
|
| 115 | - /** |
|
| 116 | - * Returns the current position on stream. |
|
| 117 | - * |
|
| 118 | - * @return int |
|
| 119 | - */ |
|
| 120 | - public function stream_tell() { |
|
| 121 | - return $this->position; |
|
| 122 | - } |
|
| 123 | - |
|
| 124 | - /** |
|
| 125 | - * Indicates if 'end of file' is reached. |
|
| 126 | - * |
|
| 127 | - * @return bool |
|
| 128 | - */ |
|
| 129 | - public function stream_eof() { |
|
| 130 | - return $this->position >= $this->streamlength; |
|
| 131 | - } |
|
| 132 | - |
|
| 133 | - /** |
|
| 134 | - * Truncates the stream to the new size. |
|
| 135 | - * |
|
| 136 | - * @param int $new_size |
|
| 137 | - * |
|
| 138 | - * @return bool |
|
| 139 | - */ |
|
| 140 | - public function stream_truncate($new_size) { |
|
| 141 | - $this->streamlength = $new_size; |
|
| 142 | - $this->toTruncate = true; |
|
| 143 | - |
|
| 144 | - if ($this->position > $this->streamlength) { |
|
| 145 | - SLog::Write(LOGLEVEL_WARN, sprintf("MAPIStreamWrapper->stream_truncate(): stream position (%d) ahead of new size of %d. Repositioning pointer to end of stream.", $this->position, $this->streamlength)); |
|
| 146 | - $this->position = $this->streamlength; |
|
| 147 | - } |
|
| 148 | - |
|
| 149 | - return true; |
|
| 150 | - } |
|
| 151 | - |
|
| 152 | - /** |
|
| 153 | - * Retrieves information about a stream. |
|
| 154 | - * |
|
| 155 | - * @return array |
|
| 156 | - */ |
|
| 157 | - public function stream_stat() { |
|
| 158 | - return [ |
|
| 159 | - 7 => $this->streamlength, |
|
| 160 | - 'size' => $this->streamlength, |
|
| 161 | - ]; |
|
| 162 | - } |
|
| 163 | - |
|
| 164 | - /** |
|
| 165 | - * Instantiates a MAPIStreamWrapper. |
|
| 166 | - * |
|
| 167 | - * @param mapistream $mapistream The stream to be wrapped |
|
| 168 | - * @param bool $truncatehtmlsafe Indicates if a truncation should be done html-safe - default: false |
|
| 169 | - * |
|
| 170 | - * @return MAPIStreamWrapper |
|
| 171 | - */ |
|
| 172 | - public static function Open($mapistream, $truncatehtmlsafe = false) { |
|
| 173 | - $context = stream_context_create([self::PROTOCOL => ['stream' => &$mapistream, 'truncatehtmlsafe' => $truncatehtmlsafe]]); |
|
| 174 | - |
|
| 175 | - return fopen(self::PROTOCOL . "://", 'r', false, $context); |
|
| 176 | - } |
|
| 12 | + public const PROTOCOL = "mapistream"; |
|
| 13 | + |
|
| 14 | + private $mapistream; |
|
| 15 | + private $position; |
|
| 16 | + private $streamlength; |
|
| 17 | + private $toTruncate; |
|
| 18 | + private $truncateHtmlSafe; |
|
| 19 | + private $context; |
|
| 20 | + |
|
| 21 | + /** |
|
| 22 | + * Opens the stream |
|
| 23 | + * The mapistream reference is passed over the context. |
|
| 24 | + * |
|
| 25 | + * @param string $path Specifies the URL that was passed to the original function |
|
| 26 | + * @param string $mode The mode used to open the file, as detailed for fopen() |
|
| 27 | + * @param int $options Holds additional flags set by the streams API |
|
| 28 | + * @param string $opened_path if the path is opened successfully, and STREAM_USE_PATH is set in options, |
|
| 29 | + * opened_path should be set to the full path of the file/resource that was actually opened |
|
| 30 | + * |
|
| 31 | + * @return bool |
|
| 32 | + */ |
|
| 33 | + public function stream_open($path, $mode, $options, &$opened_path) { |
|
| 34 | + $contextOptions = stream_context_get_options($this->context); |
|
| 35 | + if (!isset($contextOptions[self::PROTOCOL]['stream'])) { |
|
| 36 | + return false; |
|
| 37 | + } |
|
| 38 | + |
|
| 39 | + $this->position = 0; |
|
| 40 | + $this->toTruncate = false; |
|
| 41 | + $this->truncateHtmlSafe = (isset($contextOptions[self::PROTOCOL]['truncatehtmlsafe'])) ? $contextOptions[self::PROTOCOL]['truncatehtmlsafe'] : false; |
|
| 42 | + |
|
| 43 | + // this is our stream! |
|
| 44 | + $this->mapistream = $contextOptions[self::PROTOCOL]['stream']; |
|
| 45 | + |
|
| 46 | + // get the data length from mapi |
|
| 47 | + if ($this->mapistream) { |
|
| 48 | + $stat = mapi_stream_stat($this->mapistream); |
|
| 49 | + $this->streamlength = $stat["cb"]; |
|
| 50 | + } |
|
| 51 | + else { |
|
| 52 | + $this->streamlength = 0; |
|
| 53 | + } |
|
| 54 | + |
|
| 55 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIStreamWrapper::stream_open(): initialized mapistream: %s - streamlength: %d - HTML-safe-truncate: %s", $this->mapistream, $this->streamlength, Utils::PrintAsString($this->truncateHtmlSafe))); |
|
| 56 | + |
|
| 57 | + return true; |
|
| 58 | + } |
|
| 59 | + |
|
| 60 | + /** |
|
| 61 | + * Reads from stream. |
|
| 62 | + * |
|
| 63 | + * @param int $len amount of bytes to be read |
|
| 64 | + * |
|
| 65 | + * @return string |
|
| 66 | + */ |
|
| 67 | + public function stream_read($len) { |
|
| 68 | + $len = ($this->position + $len > $this->streamlength) ? ($this->streamlength - $this->position) : $len; |
|
| 69 | + |
|
| 70 | + // read 4 additional bytes from the stream so we can always truncate correctly |
|
| 71 | + if ($this->toTruncate && $this->position + $len >= $this->streamlength) { |
|
| 72 | + $len += 4; |
|
| 73 | + } |
|
| 74 | + if ($this->mapistream) { |
|
| 75 | + $data = mapi_stream_read($this->mapistream, $len); |
|
| 76 | + } |
|
| 77 | + else { |
|
| 78 | + $data = ""; |
|
| 79 | + } |
|
| 80 | + $this->position += strlen($data); |
|
| 81 | + |
|
| 82 | + // we need to truncate UTF8 compatible if ftruncate() was called |
|
| 83 | + if ($this->toTruncate && $this->position >= $this->streamlength) { |
|
| 84 | + $data = Utils::Utf8_truncate($data, $this->streamlength, $this->truncateHtmlSafe); |
|
| 85 | + } |
|
| 86 | + |
|
| 87 | + return $data; |
|
| 88 | + } |
|
| 89 | + |
|
| 90 | + /** |
|
| 91 | + * Stream "seek" functionality. |
|
| 92 | + * |
|
| 93 | + * @param int $offset |
|
| 94 | + * @param int $whence |
|
| 95 | + * |
|
| 96 | + * @return bool |
|
| 97 | + */ |
|
| 98 | + public function stream_seek($offset, $whence = SEEK_SET) { |
|
| 99 | + switch ($whence) { |
|
| 100 | + case SEEK_SET: |
|
| 101 | + $mapiWhence = STREAM_SEEK_SET; |
|
| 102 | + break; |
|
| 103 | + |
|
| 104 | + case SEEK_END: |
|
| 105 | + $mapiWhence = STREAM_SEEK_END; |
|
| 106 | + break; |
|
| 107 | + |
|
| 108 | + default: |
|
| 109 | + $mapiWhence = STREAM_SEEK_CUR; |
|
| 110 | + } |
|
| 111 | + |
|
| 112 | + return mapi_stream_seek($this->mapistream, $offset, $mapiWhence); |
|
| 113 | + } |
|
| 114 | + |
|
| 115 | + /** |
|
| 116 | + * Returns the current position on stream. |
|
| 117 | + * |
|
| 118 | + * @return int |
|
| 119 | + */ |
|
| 120 | + public function stream_tell() { |
|
| 121 | + return $this->position; |
|
| 122 | + } |
|
| 123 | + |
|
| 124 | + /** |
|
| 125 | + * Indicates if 'end of file' is reached. |
|
| 126 | + * |
|
| 127 | + * @return bool |
|
| 128 | + */ |
|
| 129 | + public function stream_eof() { |
|
| 130 | + return $this->position >= $this->streamlength; |
|
| 131 | + } |
|
| 132 | + |
|
| 133 | + /** |
|
| 134 | + * Truncates the stream to the new size. |
|
| 135 | + * |
|
| 136 | + * @param int $new_size |
|
| 137 | + * |
|
| 138 | + * @return bool |
|
| 139 | + */ |
|
| 140 | + public function stream_truncate($new_size) { |
|
| 141 | + $this->streamlength = $new_size; |
|
| 142 | + $this->toTruncate = true; |
|
| 143 | + |
|
| 144 | + if ($this->position > $this->streamlength) { |
|
| 145 | + SLog::Write(LOGLEVEL_WARN, sprintf("MAPIStreamWrapper->stream_truncate(): stream position (%d) ahead of new size of %d. Repositioning pointer to end of stream.", $this->position, $this->streamlength)); |
|
| 146 | + $this->position = $this->streamlength; |
|
| 147 | + } |
|
| 148 | + |
|
| 149 | + return true; |
|
| 150 | + } |
|
| 151 | + |
|
| 152 | + /** |
|
| 153 | + * Retrieves information about a stream. |
|
| 154 | + * |
|
| 155 | + * @return array |
|
| 156 | + */ |
|
| 157 | + public function stream_stat() { |
|
| 158 | + return [ |
|
| 159 | + 7 => $this->streamlength, |
|
| 160 | + 'size' => $this->streamlength, |
|
| 161 | + ]; |
|
| 162 | + } |
|
| 163 | + |
|
| 164 | + /** |
|
| 165 | + * Instantiates a MAPIStreamWrapper. |
|
| 166 | + * |
|
| 167 | + * @param mapistream $mapistream The stream to be wrapped |
|
| 168 | + * @param bool $truncatehtmlsafe Indicates if a truncation should be done html-safe - default: false |
|
| 169 | + * |
|
| 170 | + * @return MAPIStreamWrapper |
|
| 171 | + */ |
|
| 172 | + public static function Open($mapistream, $truncatehtmlsafe = false) { |
|
| 173 | + $context = stream_context_create([self::PROTOCOL => ['stream' => &$mapistream, 'truncatehtmlsafe' => $truncatehtmlsafe]]); |
|
| 174 | + |
|
| 175 | + return fopen(self::PROTOCOL . "://", 'r', false, $context); |
|
| 176 | + } |
|
| 177 | 177 | } |
| 178 | 178 | |
| 179 | 179 | stream_wrapper_register(MAPIStreamWrapper::PROTOCOL, "MAPIStreamWrapper"); |
@@ -172,7 +172,7 @@ |
||
| 172 | 172 | public static function Open($mapistream, $truncatehtmlsafe = false) { |
| 173 | 173 | $context = stream_context_create([self::PROTOCOL => ['stream' => &$mapistream, 'truncatehtmlsafe' => $truncatehtmlsafe]]); |
| 174 | 174 | |
| 175 | - return fopen(self::PROTOCOL . "://", 'r', false, $context); |
|
| 175 | + return fopen(self::PROTOCOL."://", 'r', false, $context); |
|
| 176 | 176 | } |
| 177 | 177 | } |
| 178 | 178 | |
@@ -47,8 +47,7 @@ discard block |
||
| 47 | 47 | if ($this->mapistream) { |
| 48 | 48 | $stat = mapi_stream_stat($this->mapistream); |
| 49 | 49 | $this->streamlength = $stat["cb"]; |
| 50 | - } |
|
| 51 | - else { |
|
| 50 | + } else { |
|
| 52 | 51 | $this->streamlength = 0; |
| 53 | 52 | } |
| 54 | 53 | |
@@ -73,8 +72,7 @@ discard block |
||
| 73 | 72 | } |
| 74 | 73 | if ($this->mapistream) { |
| 75 | 74 | $data = mapi_stream_read($this->mapistream, $len); |
| 76 | - } |
|
| 77 | - else { |
|
| 75 | + } else { |
|
| 78 | 76 | $data = ""; |
| 79 | 77 | } |
| 80 | 78 | $this->position += strlen($data); |
@@ -15,290 +15,290 @@ |
||
| 15 | 15 | * that the ImportProxies are used. |
| 16 | 16 | */ |
| 17 | 17 | class ExportChangesICS implements IExportChanges { |
| 18 | - private $folderid; |
|
| 19 | - private $store; |
|
| 20 | - private $session; |
|
| 21 | - private $restriction; |
|
| 22 | - private $contentparameters; |
|
| 23 | - private $flags; |
|
| 24 | - private $exporterflags; |
|
| 25 | - private $exporter; |
|
| 26 | - private $moveSrcState; |
|
| 27 | - private $moveDstState; |
|
| 28 | - |
|
| 29 | - /** |
|
| 30 | - * Constructor. |
|
| 31 | - * |
|
| 32 | - * @param mapisession $session |
|
| 33 | - * @param mapistore $store |
|
| 34 | - * @param mixed $folderid |
|
| 35 | - * |
|
| 36 | - * @throws StatusException |
|
| 37 | - */ |
|
| 38 | - public function __construct($session, $store, $folderid = false) { |
|
| 39 | - // Open a hierarchy or a contents exporter depending on whether a folderid was specified |
|
| 40 | - $this->session = $session; |
|
| 41 | - $this->folderid = $folderid; |
|
| 42 | - $this->store = $store; |
|
| 43 | - $this->restriction = false; |
|
| 44 | - |
|
| 45 | - try { |
|
| 46 | - if ($folderid) { |
|
| 47 | - $entryid = mapi_msgstore_entryidfromsourcekey($store, $folderid); |
|
| 48 | - } |
|
| 49 | - else { |
|
| 50 | - $storeprops = mapi_getprops($this->store, [PR_IPM_SUBTREE_ENTRYID, PR_IPM_PUBLIC_FOLDERS_ENTRYID]); |
|
| 51 | - if (GSync::GetBackend()->GetImpersonatedUser() == 'system') { |
|
| 52 | - $entryid = $storeprops[PR_IPM_PUBLIC_FOLDERS_ENTRYID]; |
|
| 53 | - } |
|
| 54 | - else { |
|
| 55 | - $entryid = $storeprops[PR_IPM_SUBTREE_ENTRYID]; |
|
| 56 | - } |
|
| 57 | - } |
|
| 58 | - |
|
| 59 | - $folder = false; |
|
| 60 | - if ($entryid) { |
|
| 61 | - $folder = mapi_msgstore_openentry($this->store, $entryid); |
|
| 62 | - if (!$folder) { |
|
| 63 | - SLog::Write(LOGLEVEL_WARN, sprintf("ExportChangesICS(): Error, mapi_msgstore_openentry() failed: 0x%08X", mapi_last_hresult())); |
|
| 64 | - } |
|
| 65 | - } |
|
| 66 | - |
|
| 67 | - // Get the actual ICS exporter |
|
| 68 | - if ($folder) { |
|
| 69 | - if ($folderid) { |
|
| 70 | - $this->exporter = mapi_openproperty($folder, PR_CONTENTS_SYNCHRONIZER, IID_IExchangeExportChanges, 0, 0); |
|
| 71 | - } |
|
| 72 | - else { |
|
| 73 | - $this->exporter = mapi_openproperty($folder, PR_HIERARCHY_SYNCHRONIZER, IID_IExchangeExportChanges, 0, 0); |
|
| 74 | - } |
|
| 75 | - } |
|
| 76 | - else { |
|
| 77 | - $this->exporter = false; |
|
| 78 | - } |
|
| 79 | - } |
|
| 80 | - catch (MAPIException $me) { |
|
| 81 | - $this->exporter = false; |
|
| 82 | - // We return the general error SYNC_FSSTATUS_CODEUNKNOWN (12) which is also SYNC_STATUS_FOLDERHIERARCHYCHANGED (12) |
|
| 83 | - // if this happened while doing content sync, the mobile will try to resync the folderhierarchy |
|
| 84 | - throw new StatusException(sprintf("ExportChangesICS('%s','%s','%s'): Error, unable to open folder: 0x%X", $session, $store, Utils::PrintAsString($folderid), mapi_last_hresult()), SYNC_FSSTATUS_CODEUNKNOWN); |
|
| 85 | - } |
|
| 86 | - } |
|
| 87 | - |
|
| 88 | - /** |
|
| 89 | - * Configures the exporter. |
|
| 90 | - * |
|
| 91 | - * @param string $state |
|
| 92 | - * @param int $flags |
|
| 93 | - * |
|
| 94 | - * @throws StatusException |
|
| 95 | - * |
|
| 96 | - * @return bool |
|
| 97 | - */ |
|
| 98 | - public function Config($state, $flags = 0) { |
|
| 99 | - $this->exporterflags = 0; |
|
| 100 | - $this->flags = $flags; |
|
| 101 | - |
|
| 102 | - // this should never happen |
|
| 103 | - if ($this->exporter === false || is_array($state)) { |
|
| 104 | - throw new StatusException("ExportChangesICS->Config(): Error, exporter not available", SYNC_FSSTATUS_CODEUNKNOWN, null, LOGLEVEL_ERROR); |
|
| 105 | - } |
|
| 106 | - |
|
| 107 | - // change exporterflags if we are doing a ContentExport |
|
| 108 | - if ($this->folderid) { |
|
| 109 | - $this->exporterflags |= SYNC_NORMAL | SYNC_READ_STATE; |
|
| 110 | - |
|
| 111 | - // Initial sync, we don't want deleted items. If the initial sync is chunked |
|
| 112 | - // we check the change ID of the syncstate (0 at initial sync) |
|
| 113 | - // On subsequent syncs, we do want to receive delete events. |
|
| 114 | - if (strlen($state) == 0 || bin2hex(substr($state, 4, 4)) == "00000000") { |
|
| 115 | - if (!($this->flags & BACKEND_DISCARD_DATA)) { |
|
| 116 | - SLog::Write(LOGLEVEL_DEBUG, "ExportChangesICS->Config(): syncing initial data"); |
|
| 117 | - } |
|
| 118 | - $this->exporterflags |= SYNC_NO_SOFT_DELETIONS | SYNC_NO_DELETIONS; |
|
| 119 | - } |
|
| 120 | - } |
|
| 121 | - |
|
| 122 | - if ($this->flags & BACKEND_DISCARD_DATA) { |
|
| 123 | - $this->exporterflags |= SYNC_CATCHUP; |
|
| 124 | - $this->exporterflags |= SYNC_STATE_READONLY; |
|
| 125 | - } |
|
| 126 | - |
|
| 127 | - // Put the state information in a stream that can be used by ICS |
|
| 128 | - $stream = mapi_stream_create(); |
|
| 129 | - if (strlen($state) == 0) { |
|
| 130 | - $state = hex2bin("0000000000000000"); |
|
| 131 | - } |
|
| 132 | - |
|
| 133 | - if (!($this->flags & BACKEND_DISCARD_DATA)) { |
|
| 134 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("ExportChangesICS->Config() initialized with state: 0x%s", bin2hex($state))); |
|
| 135 | - } |
|
| 136 | - |
|
| 137 | - mapi_stream_write($stream, $state); |
|
| 138 | - $this->statestream = $stream; |
|
| 139 | - } |
|
| 140 | - |
|
| 141 | - /** |
|
| 142 | - * Configures additional parameters used for content synchronization. |
|
| 143 | - * |
|
| 144 | - * @param ContentParameters $contentparameters |
|
| 145 | - * |
|
| 146 | - * @throws StatusException |
|
| 147 | - * |
|
| 148 | - * @return bool |
|
| 149 | - */ |
|
| 150 | - public function ConfigContentParameters($contentparameters) { |
|
| 151 | - $filtertype = $contentparameters->GetFilterType(); |
|
| 152 | - |
|
| 153 | - switch ($contentparameters->GetContentClass()) { |
|
| 154 | - case "Email": |
|
| 155 | - $this->restriction = ($filtertype || !Utils::CheckMapiExtVersion('7')) ? MAPIUtils::GetEmailRestriction(Utils::GetCutOffDate($filtertype)) : false; |
|
| 156 | - break; |
|
| 157 | - |
|
| 158 | - case "Calendar": |
|
| 159 | - $this->restriction = ($filtertype || !Utils::CheckMapiExtVersion('7')) ? MAPIUtils::GetCalendarRestriction($this->store, Utils::GetCutOffDate($filtertype)) : false; |
|
| 160 | - break; |
|
| 161 | - |
|
| 162 | - default: |
|
| 163 | - case "Contacts": |
|
| 164 | - case "Tasks": |
|
| 165 | - $this->restriction = false; |
|
| 166 | - break; |
|
| 167 | - } |
|
| 168 | - |
|
| 169 | - $this->contentParameters = $contentparameters; |
|
| 170 | - } |
|
| 171 | - |
|
| 172 | - /** |
|
| 173 | - * Sets the importer the exporter will sent it's changes to |
|
| 174 | - * and initializes the Exporter. |
|
| 175 | - * |
|
| 176 | - * @param object &$importer Implementation of IImportChanges |
|
| 177 | - * |
|
| 178 | - * @throws StatusException |
|
| 179 | - * |
|
| 180 | - * @return bool |
|
| 181 | - */ |
|
| 182 | - public function InitializeExporter(&$importer) { |
|
| 183 | - // Because we're using ICS, we need to wrap the given importer to make it suitable to pass |
|
| 184 | - // to ICS. We do this in two steps: first, wrap the importer with our own PHP importer class |
|
| 185 | - // which removes all MAPI dependency, and then wrap that class with a C++ wrapper so we can |
|
| 186 | - // pass it to ICS |
|
| 187 | - |
|
| 188 | - // this should never happen! |
|
| 189 | - if ($this->exporter === false || !isset($this->statestream) || !isset($this->flags) || !isset($this->exporterflags) || |
|
| 190 | - ($this->folderid && !isset($this->contentParameters))) { |
|
| 191 | - throw new StatusException("ExportChangesICS->InitializeExporter(): Error, exporter or essential data not available", SYNC_FSSTATUS_CODEUNKNOWN, null, LOGLEVEL_ERROR); |
|
| 192 | - } |
|
| 193 | - |
|
| 194 | - // PHP wrapper |
|
| 195 | - $phpwrapper = new PHPWrapper($this->session, $this->store, $importer, $this->folderid); |
|
| 196 | - |
|
| 197 | - // with a folderid we are going to get content |
|
| 198 | - if ($this->folderid) { |
|
| 199 | - $phpwrapper->ConfigContentParameters($this->contentParameters); |
|
| 200 | - |
|
| 201 | - // ICS c++ wrapper |
|
| 202 | - $mapiimporter = mapi_wrap_importcontentschanges($phpwrapper); |
|
| 203 | - $includeprops = false; |
|
| 204 | - } |
|
| 205 | - else { |
|
| 206 | - $mapiimporter = mapi_wrap_importhierarchychanges($phpwrapper); |
|
| 207 | - $includeprops = [PR_SOURCE_KEY, PR_DISPLAY_NAME]; |
|
| 208 | - } |
|
| 209 | - |
|
| 210 | - if (!$mapiimporter) { |
|
| 211 | - throw new StatusException(sprintf("ExportChangesICS->InitializeExporter(): Error, mapi_wrap_import_*_changes() failed: 0x%X", mapi_last_hresult()), SYNC_FSSTATUS_CODEUNKNOWN, null, LOGLEVEL_WARN); |
|
| 212 | - } |
|
| 213 | - |
|
| 214 | - $ret = mapi_exportchanges_config($this->exporter, $this->statestream, $this->exporterflags, $mapiimporter, $this->restriction, $includeprops, false, 1); |
|
| 215 | - if (!$ret) { |
|
| 216 | - throw new StatusException(sprintf("ExportChangesICS->InitializeExporter(): Error, mapi_exportchanges_config() failed: 0x%X", mapi_last_hresult()), SYNC_FSSTATUS_CODEUNKNOWN, null, LOGLEVEL_WARN); |
|
| 217 | - } |
|
| 218 | - |
|
| 219 | - $changes = mapi_exportchanges_getchangecount($this->exporter); |
|
| 220 | - if ($changes || !($this->flags & BACKEND_DISCARD_DATA)) { |
|
| 221 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("ExportChangesICS->InitializeExporter() successfully. %d changes ready to sync for '%s'.", $changes, ($this->folderid) ? bin2hex($this->folderid) : 'hierarchy')); |
|
| 222 | - } |
|
| 223 | - |
|
| 224 | - return $ret; |
|
| 225 | - } |
|
| 226 | - |
|
| 227 | - /** |
|
| 228 | - * Indicates if the exporter was configured with the BACKEND_DISCARD_DATA flag. |
|
| 229 | - * |
|
| 230 | - * @return bool |
|
| 231 | - */ |
|
| 232 | - public function HasDiscardDataFlag() { |
|
| 233 | - if (isset($this->flags) && $this->flags & BACKEND_DISCARD_DATA) { |
|
| 234 | - return true; |
|
| 235 | - } |
|
| 236 | - |
|
| 237 | - return false; |
|
| 238 | - } |
|
| 239 | - |
|
| 240 | - /** |
|
| 241 | - * Reads the current state from the Exporter. |
|
| 242 | - * |
|
| 243 | - * @throws StatusException |
|
| 244 | - * |
|
| 245 | - * @return string |
|
| 246 | - */ |
|
| 247 | - public function GetState() { |
|
| 248 | - $error = false; |
|
| 249 | - if (!isset($this->statestream) || $this->exporter === false) { |
|
| 250 | - $error = true; |
|
| 251 | - } |
|
| 252 | - |
|
| 253 | - if ($error === true || mapi_exportchanges_updatestate($this->exporter, $this->statestream) != true) { |
|
| 254 | - throw new StatusException(sprintf("ExportChangesICS->GetState(): Error, state not available or unable to update: 0x%X", mapi_last_hresult()), (($this->folderid) ? SYNC_STATUS_FOLDERHIERARCHYCHANGED : SYNC_FSSTATUS_CODEUNKNOWN), null, LOGLEVEL_WARN); |
|
| 255 | - } |
|
| 256 | - |
|
| 257 | - mapi_stream_seek($this->statestream, 0, STREAM_SEEK_SET); |
|
| 258 | - |
|
| 259 | - $state = ""; |
|
| 260 | - while (true) { |
|
| 261 | - $data = mapi_stream_read($this->statestream, 4096); |
|
| 262 | - if (strlen($data)) { |
|
| 263 | - $state .= $data; |
|
| 264 | - } |
|
| 265 | - else { |
|
| 266 | - break; |
|
| 267 | - } |
|
| 268 | - } |
|
| 269 | - |
|
| 270 | - return $state; |
|
| 271 | - } |
|
| 272 | - |
|
| 273 | - /** |
|
| 274 | - * Returns the amount of changes to be exported. |
|
| 275 | - * |
|
| 276 | - * @return int |
|
| 277 | - */ |
|
| 278 | - public function GetChangeCount() { |
|
| 279 | - if ($this->exporter) { |
|
| 280 | - return mapi_exportchanges_getchangecount($this->exporter); |
|
| 281 | - } |
|
| 282 | - |
|
| 283 | - return 0; |
|
| 284 | - } |
|
| 285 | - |
|
| 286 | - /** |
|
| 287 | - * Synchronizes a change. |
|
| 288 | - * |
|
| 289 | - * @return array |
|
| 290 | - */ |
|
| 291 | - public function Synchronize() { |
|
| 292 | - if ($this->flags & BACKEND_DISCARD_DATA) { |
|
| 293 | - SLog::Write(LOGLEVEL_WARN, 'ExportChangesICS->Synchronize(): not supported in combination with the BACKEND_DISCARD_DATA flag.'); |
|
| 294 | - |
|
| 295 | - return false; |
|
| 296 | - } |
|
| 297 | - |
|
| 298 | - if ($this->exporter) { |
|
| 299 | - return mapi_exportchanges_synchronize($this->exporter); |
|
| 300 | - } |
|
| 301 | - |
|
| 302 | - return false; |
|
| 303 | - } |
|
| 18 | + private $folderid; |
|
| 19 | + private $store; |
|
| 20 | + private $session; |
|
| 21 | + private $restriction; |
|
| 22 | + private $contentparameters; |
|
| 23 | + private $flags; |
|
| 24 | + private $exporterflags; |
|
| 25 | + private $exporter; |
|
| 26 | + private $moveSrcState; |
|
| 27 | + private $moveDstState; |
|
| 28 | + |
|
| 29 | + /** |
|
| 30 | + * Constructor. |
|
| 31 | + * |
|
| 32 | + * @param mapisession $session |
|
| 33 | + * @param mapistore $store |
|
| 34 | + * @param mixed $folderid |
|
| 35 | + * |
|
| 36 | + * @throws StatusException |
|
| 37 | + */ |
|
| 38 | + public function __construct($session, $store, $folderid = false) { |
|
| 39 | + // Open a hierarchy or a contents exporter depending on whether a folderid was specified |
|
| 40 | + $this->session = $session; |
|
| 41 | + $this->folderid = $folderid; |
|
| 42 | + $this->store = $store; |
|
| 43 | + $this->restriction = false; |
|
| 44 | + |
|
| 45 | + try { |
|
| 46 | + if ($folderid) { |
|
| 47 | + $entryid = mapi_msgstore_entryidfromsourcekey($store, $folderid); |
|
| 48 | + } |
|
| 49 | + else { |
|
| 50 | + $storeprops = mapi_getprops($this->store, [PR_IPM_SUBTREE_ENTRYID, PR_IPM_PUBLIC_FOLDERS_ENTRYID]); |
|
| 51 | + if (GSync::GetBackend()->GetImpersonatedUser() == 'system') { |
|
| 52 | + $entryid = $storeprops[PR_IPM_PUBLIC_FOLDERS_ENTRYID]; |
|
| 53 | + } |
|
| 54 | + else { |
|
| 55 | + $entryid = $storeprops[PR_IPM_SUBTREE_ENTRYID]; |
|
| 56 | + } |
|
| 57 | + } |
|
| 58 | + |
|
| 59 | + $folder = false; |
|
| 60 | + if ($entryid) { |
|
| 61 | + $folder = mapi_msgstore_openentry($this->store, $entryid); |
|
| 62 | + if (!$folder) { |
|
| 63 | + SLog::Write(LOGLEVEL_WARN, sprintf("ExportChangesICS(): Error, mapi_msgstore_openentry() failed: 0x%08X", mapi_last_hresult())); |
|
| 64 | + } |
|
| 65 | + } |
|
| 66 | + |
|
| 67 | + // Get the actual ICS exporter |
|
| 68 | + if ($folder) { |
|
| 69 | + if ($folderid) { |
|
| 70 | + $this->exporter = mapi_openproperty($folder, PR_CONTENTS_SYNCHRONIZER, IID_IExchangeExportChanges, 0, 0); |
|
| 71 | + } |
|
| 72 | + else { |
|
| 73 | + $this->exporter = mapi_openproperty($folder, PR_HIERARCHY_SYNCHRONIZER, IID_IExchangeExportChanges, 0, 0); |
|
| 74 | + } |
|
| 75 | + } |
|
| 76 | + else { |
|
| 77 | + $this->exporter = false; |
|
| 78 | + } |
|
| 79 | + } |
|
| 80 | + catch (MAPIException $me) { |
|
| 81 | + $this->exporter = false; |
|
| 82 | + // We return the general error SYNC_FSSTATUS_CODEUNKNOWN (12) which is also SYNC_STATUS_FOLDERHIERARCHYCHANGED (12) |
|
| 83 | + // if this happened while doing content sync, the mobile will try to resync the folderhierarchy |
|
| 84 | + throw new StatusException(sprintf("ExportChangesICS('%s','%s','%s'): Error, unable to open folder: 0x%X", $session, $store, Utils::PrintAsString($folderid), mapi_last_hresult()), SYNC_FSSTATUS_CODEUNKNOWN); |
|
| 85 | + } |
|
| 86 | + } |
|
| 87 | + |
|
| 88 | + /** |
|
| 89 | + * Configures the exporter. |
|
| 90 | + * |
|
| 91 | + * @param string $state |
|
| 92 | + * @param int $flags |
|
| 93 | + * |
|
| 94 | + * @throws StatusException |
|
| 95 | + * |
|
| 96 | + * @return bool |
|
| 97 | + */ |
|
| 98 | + public function Config($state, $flags = 0) { |
|
| 99 | + $this->exporterflags = 0; |
|
| 100 | + $this->flags = $flags; |
|
| 101 | + |
|
| 102 | + // this should never happen |
|
| 103 | + if ($this->exporter === false || is_array($state)) { |
|
| 104 | + throw new StatusException("ExportChangesICS->Config(): Error, exporter not available", SYNC_FSSTATUS_CODEUNKNOWN, null, LOGLEVEL_ERROR); |
|
| 105 | + } |
|
| 106 | + |
|
| 107 | + // change exporterflags if we are doing a ContentExport |
|
| 108 | + if ($this->folderid) { |
|
| 109 | + $this->exporterflags |= SYNC_NORMAL | SYNC_READ_STATE; |
|
| 110 | + |
|
| 111 | + // Initial sync, we don't want deleted items. If the initial sync is chunked |
|
| 112 | + // we check the change ID of the syncstate (0 at initial sync) |
|
| 113 | + // On subsequent syncs, we do want to receive delete events. |
|
| 114 | + if (strlen($state) == 0 || bin2hex(substr($state, 4, 4)) == "00000000") { |
|
| 115 | + if (!($this->flags & BACKEND_DISCARD_DATA)) { |
|
| 116 | + SLog::Write(LOGLEVEL_DEBUG, "ExportChangesICS->Config(): syncing initial data"); |
|
| 117 | + } |
|
| 118 | + $this->exporterflags |= SYNC_NO_SOFT_DELETIONS | SYNC_NO_DELETIONS; |
|
| 119 | + } |
|
| 120 | + } |
|
| 121 | + |
|
| 122 | + if ($this->flags & BACKEND_DISCARD_DATA) { |
|
| 123 | + $this->exporterflags |= SYNC_CATCHUP; |
|
| 124 | + $this->exporterflags |= SYNC_STATE_READONLY; |
|
| 125 | + } |
|
| 126 | + |
|
| 127 | + // Put the state information in a stream that can be used by ICS |
|
| 128 | + $stream = mapi_stream_create(); |
|
| 129 | + if (strlen($state) == 0) { |
|
| 130 | + $state = hex2bin("0000000000000000"); |
|
| 131 | + } |
|
| 132 | + |
|
| 133 | + if (!($this->flags & BACKEND_DISCARD_DATA)) { |
|
| 134 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("ExportChangesICS->Config() initialized with state: 0x%s", bin2hex($state))); |
|
| 135 | + } |
|
| 136 | + |
|
| 137 | + mapi_stream_write($stream, $state); |
|
| 138 | + $this->statestream = $stream; |
|
| 139 | + } |
|
| 140 | + |
|
| 141 | + /** |
|
| 142 | + * Configures additional parameters used for content synchronization. |
|
| 143 | + * |
|
| 144 | + * @param ContentParameters $contentparameters |
|
| 145 | + * |
|
| 146 | + * @throws StatusException |
|
| 147 | + * |
|
| 148 | + * @return bool |
|
| 149 | + */ |
|
| 150 | + public function ConfigContentParameters($contentparameters) { |
|
| 151 | + $filtertype = $contentparameters->GetFilterType(); |
|
| 152 | + |
|
| 153 | + switch ($contentparameters->GetContentClass()) { |
|
| 154 | + case "Email": |
|
| 155 | + $this->restriction = ($filtertype || !Utils::CheckMapiExtVersion('7')) ? MAPIUtils::GetEmailRestriction(Utils::GetCutOffDate($filtertype)) : false; |
|
| 156 | + break; |
|
| 157 | + |
|
| 158 | + case "Calendar": |
|
| 159 | + $this->restriction = ($filtertype || !Utils::CheckMapiExtVersion('7')) ? MAPIUtils::GetCalendarRestriction($this->store, Utils::GetCutOffDate($filtertype)) : false; |
|
| 160 | + break; |
|
| 161 | + |
|
| 162 | + default: |
|
| 163 | + case "Contacts": |
|
| 164 | + case "Tasks": |
|
| 165 | + $this->restriction = false; |
|
| 166 | + break; |
|
| 167 | + } |
|
| 168 | + |
|
| 169 | + $this->contentParameters = $contentparameters; |
|
| 170 | + } |
|
| 171 | + |
|
| 172 | + /** |
|
| 173 | + * Sets the importer the exporter will sent it's changes to |
|
| 174 | + * and initializes the Exporter. |
|
| 175 | + * |
|
| 176 | + * @param object &$importer Implementation of IImportChanges |
|
| 177 | + * |
|
| 178 | + * @throws StatusException |
|
| 179 | + * |
|
| 180 | + * @return bool |
|
| 181 | + */ |
|
| 182 | + public function InitializeExporter(&$importer) { |
|
| 183 | + // Because we're using ICS, we need to wrap the given importer to make it suitable to pass |
|
| 184 | + // to ICS. We do this in two steps: first, wrap the importer with our own PHP importer class |
|
| 185 | + // which removes all MAPI dependency, and then wrap that class with a C++ wrapper so we can |
|
| 186 | + // pass it to ICS |
|
| 187 | + |
|
| 188 | + // this should never happen! |
|
| 189 | + if ($this->exporter === false || !isset($this->statestream) || !isset($this->flags) || !isset($this->exporterflags) || |
|
| 190 | + ($this->folderid && !isset($this->contentParameters))) { |
|
| 191 | + throw new StatusException("ExportChangesICS->InitializeExporter(): Error, exporter or essential data not available", SYNC_FSSTATUS_CODEUNKNOWN, null, LOGLEVEL_ERROR); |
|
| 192 | + } |
|
| 193 | + |
|
| 194 | + // PHP wrapper |
|
| 195 | + $phpwrapper = new PHPWrapper($this->session, $this->store, $importer, $this->folderid); |
|
| 196 | + |
|
| 197 | + // with a folderid we are going to get content |
|
| 198 | + if ($this->folderid) { |
|
| 199 | + $phpwrapper->ConfigContentParameters($this->contentParameters); |
|
| 200 | + |
|
| 201 | + // ICS c++ wrapper |
|
| 202 | + $mapiimporter = mapi_wrap_importcontentschanges($phpwrapper); |
|
| 203 | + $includeprops = false; |
|
| 204 | + } |
|
| 205 | + else { |
|
| 206 | + $mapiimporter = mapi_wrap_importhierarchychanges($phpwrapper); |
|
| 207 | + $includeprops = [PR_SOURCE_KEY, PR_DISPLAY_NAME]; |
|
| 208 | + } |
|
| 209 | + |
|
| 210 | + if (!$mapiimporter) { |
|
| 211 | + throw new StatusException(sprintf("ExportChangesICS->InitializeExporter(): Error, mapi_wrap_import_*_changes() failed: 0x%X", mapi_last_hresult()), SYNC_FSSTATUS_CODEUNKNOWN, null, LOGLEVEL_WARN); |
|
| 212 | + } |
|
| 213 | + |
|
| 214 | + $ret = mapi_exportchanges_config($this->exporter, $this->statestream, $this->exporterflags, $mapiimporter, $this->restriction, $includeprops, false, 1); |
|
| 215 | + if (!$ret) { |
|
| 216 | + throw new StatusException(sprintf("ExportChangesICS->InitializeExporter(): Error, mapi_exportchanges_config() failed: 0x%X", mapi_last_hresult()), SYNC_FSSTATUS_CODEUNKNOWN, null, LOGLEVEL_WARN); |
|
| 217 | + } |
|
| 218 | + |
|
| 219 | + $changes = mapi_exportchanges_getchangecount($this->exporter); |
|
| 220 | + if ($changes || !($this->flags & BACKEND_DISCARD_DATA)) { |
|
| 221 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("ExportChangesICS->InitializeExporter() successfully. %d changes ready to sync for '%s'.", $changes, ($this->folderid) ? bin2hex($this->folderid) : 'hierarchy')); |
|
| 222 | + } |
|
| 223 | + |
|
| 224 | + return $ret; |
|
| 225 | + } |
|
| 226 | + |
|
| 227 | + /** |
|
| 228 | + * Indicates if the exporter was configured with the BACKEND_DISCARD_DATA flag. |
|
| 229 | + * |
|
| 230 | + * @return bool |
|
| 231 | + */ |
|
| 232 | + public function HasDiscardDataFlag() { |
|
| 233 | + if (isset($this->flags) && $this->flags & BACKEND_DISCARD_DATA) { |
|
| 234 | + return true; |
|
| 235 | + } |
|
| 236 | + |
|
| 237 | + return false; |
|
| 238 | + } |
|
| 239 | + |
|
| 240 | + /** |
|
| 241 | + * Reads the current state from the Exporter. |
|
| 242 | + * |
|
| 243 | + * @throws StatusException |
|
| 244 | + * |
|
| 245 | + * @return string |
|
| 246 | + */ |
|
| 247 | + public function GetState() { |
|
| 248 | + $error = false; |
|
| 249 | + if (!isset($this->statestream) || $this->exporter === false) { |
|
| 250 | + $error = true; |
|
| 251 | + } |
|
| 252 | + |
|
| 253 | + if ($error === true || mapi_exportchanges_updatestate($this->exporter, $this->statestream) != true) { |
|
| 254 | + throw new StatusException(sprintf("ExportChangesICS->GetState(): Error, state not available or unable to update: 0x%X", mapi_last_hresult()), (($this->folderid) ? SYNC_STATUS_FOLDERHIERARCHYCHANGED : SYNC_FSSTATUS_CODEUNKNOWN), null, LOGLEVEL_WARN); |
|
| 255 | + } |
|
| 256 | + |
|
| 257 | + mapi_stream_seek($this->statestream, 0, STREAM_SEEK_SET); |
|
| 258 | + |
|
| 259 | + $state = ""; |
|
| 260 | + while (true) { |
|
| 261 | + $data = mapi_stream_read($this->statestream, 4096); |
|
| 262 | + if (strlen($data)) { |
|
| 263 | + $state .= $data; |
|
| 264 | + } |
|
| 265 | + else { |
|
| 266 | + break; |
|
| 267 | + } |
|
| 268 | + } |
|
| 269 | + |
|
| 270 | + return $state; |
|
| 271 | + } |
|
| 272 | + |
|
| 273 | + /** |
|
| 274 | + * Returns the amount of changes to be exported. |
|
| 275 | + * |
|
| 276 | + * @return int |
|
| 277 | + */ |
|
| 278 | + public function GetChangeCount() { |
|
| 279 | + if ($this->exporter) { |
|
| 280 | + return mapi_exportchanges_getchangecount($this->exporter); |
|
| 281 | + } |
|
| 282 | + |
|
| 283 | + return 0; |
|
| 284 | + } |
|
| 285 | + |
|
| 286 | + /** |
|
| 287 | + * Synchronizes a change. |
|
| 288 | + * |
|
| 289 | + * @return array |
|
| 290 | + */ |
|
| 291 | + public function Synchronize() { |
|
| 292 | + if ($this->flags & BACKEND_DISCARD_DATA) { |
|
| 293 | + SLog::Write(LOGLEVEL_WARN, 'ExportChangesICS->Synchronize(): not supported in combination with the BACKEND_DISCARD_DATA flag.'); |
|
| 294 | + |
|
| 295 | + return false; |
|
| 296 | + } |
|
| 297 | + |
|
| 298 | + if ($this->exporter) { |
|
| 299 | + return mapi_exportchanges_synchronize($this->exporter); |
|
| 300 | + } |
|
| 301 | + |
|
| 302 | + return false; |
|
| 303 | + } |
|
| 304 | 304 | } |
@@ -106,7 +106,7 @@ discard block |
||
| 106 | 106 | |
| 107 | 107 | // change exporterflags if we are doing a ContentExport |
| 108 | 108 | if ($this->folderid) { |
| 109 | - $this->exporterflags |= SYNC_NORMAL | SYNC_READ_STATE; |
|
| 109 | + $this->exporterflags |= SYNC_NORMAL|SYNC_READ_STATE; |
|
| 110 | 110 | |
| 111 | 111 | // Initial sync, we don't want deleted items. If the initial sync is chunked |
| 112 | 112 | // we check the change ID of the syncstate (0 at initial sync) |
@@ -115,7 +115,7 @@ discard block |
||
| 115 | 115 | if (!($this->flags & BACKEND_DISCARD_DATA)) { |
| 116 | 116 | SLog::Write(LOGLEVEL_DEBUG, "ExportChangesICS->Config(): syncing initial data"); |
| 117 | 117 | } |
| 118 | - $this->exporterflags |= SYNC_NO_SOFT_DELETIONS | SYNC_NO_DELETIONS; |
|
| 118 | + $this->exporterflags |= SYNC_NO_SOFT_DELETIONS|SYNC_NO_DELETIONS; |
|
| 119 | 119 | } |
| 120 | 120 | } |
| 121 | 121 | |
@@ -45,13 +45,11 @@ discard block |
||
| 45 | 45 | try { |
| 46 | 46 | if ($folderid) { |
| 47 | 47 | $entryid = mapi_msgstore_entryidfromsourcekey($store, $folderid); |
| 48 | - } |
|
| 49 | - else { |
|
| 48 | + } else { |
|
| 50 | 49 | $storeprops = mapi_getprops($this->store, [PR_IPM_SUBTREE_ENTRYID, PR_IPM_PUBLIC_FOLDERS_ENTRYID]); |
| 51 | 50 | if (GSync::GetBackend()->GetImpersonatedUser() == 'system') { |
| 52 | 51 | $entryid = $storeprops[PR_IPM_PUBLIC_FOLDERS_ENTRYID]; |
| 53 | - } |
|
| 54 | - else { |
|
| 52 | + } else { |
|
| 55 | 53 | $entryid = $storeprops[PR_IPM_SUBTREE_ENTRYID]; |
| 56 | 54 | } |
| 57 | 55 | } |
@@ -68,16 +66,13 @@ discard block |
||
| 68 | 66 | if ($folder) { |
| 69 | 67 | if ($folderid) { |
| 70 | 68 | $this->exporter = mapi_openproperty($folder, PR_CONTENTS_SYNCHRONIZER, IID_IExchangeExportChanges, 0, 0); |
| 71 | - } |
|
| 72 | - else { |
|
| 69 | + } else { |
|
| 73 | 70 | $this->exporter = mapi_openproperty($folder, PR_HIERARCHY_SYNCHRONIZER, IID_IExchangeExportChanges, 0, 0); |
| 74 | 71 | } |
| 75 | - } |
|
| 76 | - else { |
|
| 72 | + } else { |
|
| 77 | 73 | $this->exporter = false; |
| 78 | 74 | } |
| 79 | - } |
|
| 80 | - catch (MAPIException $me) { |
|
| 75 | + } catch (MAPIException $me) { |
|
| 81 | 76 | $this->exporter = false; |
| 82 | 77 | // We return the general error SYNC_FSSTATUS_CODEUNKNOWN (12) which is also SYNC_STATUS_FOLDERHIERARCHYCHANGED (12) |
| 83 | 78 | // if this happened while doing content sync, the mobile will try to resync the folderhierarchy |
@@ -201,8 +196,7 @@ discard block |
||
| 201 | 196 | // ICS c++ wrapper |
| 202 | 197 | $mapiimporter = mapi_wrap_importcontentschanges($phpwrapper); |
| 203 | 198 | $includeprops = false; |
| 204 | - } |
|
| 205 | - else { |
|
| 199 | + } else { |
|
| 206 | 200 | $mapiimporter = mapi_wrap_importhierarchychanges($phpwrapper); |
| 207 | 201 | $includeprops = [PR_SOURCE_KEY, PR_DISPLAY_NAME]; |
| 208 | 202 | } |
@@ -261,8 +255,7 @@ discard block |
||
| 261 | 255 | $data = mapi_stream_read($this->statestream, 4096); |
| 262 | 256 | if (strlen($data)) { |
| 263 | 257 | $state .= $data; |
| 264 | - } |
|
| 265 | - else { |
|
| 258 | + } else { |
|
| 266 | 259 | break; |
| 267 | 260 | } |
| 268 | 261 | } |
@@ -6,3159 +6,3159 @@ |
||
| 6 | 6 | */ |
| 7 | 7 | |
| 8 | 8 | class MAPIProvider { |
| 9 | - private $session; |
|
| 10 | - private $store; |
|
| 11 | - private $zRFC822; |
|
| 12 | - private $addressbook; |
|
| 13 | - private $storeProps; |
|
| 14 | - private $inboxProps; |
|
| 15 | - private $rootProps; |
|
| 16 | - private $specialFoldersData; |
|
| 17 | - |
|
| 18 | - /** |
|
| 19 | - * Constructor of the MAPI Provider |
|
| 20 | - * Almost all methods of this class require a MAPI session and/or store. |
|
| 21 | - * |
|
| 22 | - * @param resource $session |
|
| 23 | - * @param resource $store |
|
| 24 | - */ |
|
| 25 | - public function __construct($session, $store) { |
|
| 26 | - $this->session = $session; |
|
| 27 | - $this->store = $store; |
|
| 28 | - } |
|
| 29 | - |
|
| 30 | - /*---------------------------------------------------------------------------------------------------------- |
|
| 9 | + private $session; |
|
| 10 | + private $store; |
|
| 11 | + private $zRFC822; |
|
| 12 | + private $addressbook; |
|
| 13 | + private $storeProps; |
|
| 14 | + private $inboxProps; |
|
| 15 | + private $rootProps; |
|
| 16 | + private $specialFoldersData; |
|
| 17 | + |
|
| 18 | + /** |
|
| 19 | + * Constructor of the MAPI Provider |
|
| 20 | + * Almost all methods of this class require a MAPI session and/or store. |
|
| 21 | + * |
|
| 22 | + * @param resource $session |
|
| 23 | + * @param resource $store |
|
| 24 | + */ |
|
| 25 | + public function __construct($session, $store) { |
|
| 26 | + $this->session = $session; |
|
| 27 | + $this->store = $store; |
|
| 28 | + } |
|
| 29 | + |
|
| 30 | + /*---------------------------------------------------------------------------------------------------------- |
|
| 31 | 31 | * GETTER |
| 32 | 32 | */ |
| 33 | 33 | |
| 34 | - /** |
|
| 35 | - * Reads a message from MAPI |
|
| 36 | - * Depending on the message class, a contact, appointment, task or email is read. |
|
| 37 | - * |
|
| 38 | - * @param mixed $mapimessage |
|
| 39 | - * @param ContentParameters $contentparameters |
|
| 40 | - * |
|
| 41 | - * @return SyncObject |
|
| 42 | - */ |
|
| 43 | - public function GetMessage($mapimessage, $contentparameters) { |
|
| 44 | - // Gets the Sync object from a MAPI object according to its message class |
|
| 45 | - |
|
| 46 | - $props = mapi_getprops($mapimessage, [PR_MESSAGE_CLASS]); |
|
| 47 | - if (isset($props[PR_MESSAGE_CLASS])) { |
|
| 48 | - $messageclass = $props[PR_MESSAGE_CLASS]; |
|
| 49 | - } |
|
| 50 | - else { |
|
| 51 | - $messageclass = "IPM"; |
|
| 52 | - } |
|
| 53 | - |
|
| 54 | - if (strpos($messageclass, "IPM.Contact") === 0) { |
|
| 55 | - return $this->getContact($mapimessage, $contentparameters); |
|
| 56 | - } |
|
| 57 | - if (strpos($messageclass, "IPM.Appointment") === 0) { |
|
| 58 | - return $this->getAppointment($mapimessage, $contentparameters); |
|
| 59 | - } |
|
| 60 | - if (strpos($messageclass, "IPM.Task") === 0 && strpos($messageclass, "IPM.TaskRequest") === false) { |
|
| 61 | - return $this->getTask($mapimessage, $contentparameters); |
|
| 62 | - } |
|
| 63 | - if (strpos($messageclass, "IPM.StickyNote") === 0) { |
|
| 64 | - return $this->getNote($mapimessage, $contentparameters); |
|
| 65 | - } |
|
| 66 | - |
|
| 67 | - return $this->getEmail($mapimessage, $contentparameters); |
|
| 68 | - } |
|
| 69 | - |
|
| 70 | - /** |
|
| 71 | - * Reads a contact object from MAPI. |
|
| 72 | - * |
|
| 73 | - * @param mixed $mapimessage |
|
| 74 | - * @param ContentParameters $contentparameters |
|
| 75 | - * |
|
| 76 | - * @return SyncContact |
|
| 77 | - */ |
|
| 78 | - private function getContact($mapimessage, $contentparameters) { |
|
| 79 | - $message = new SyncContact(); |
|
| 80 | - |
|
| 81 | - // Standard one-to-one mappings first |
|
| 82 | - $this->getPropsFromMAPI($message, $mapimessage, MAPIMapping::GetContactMapping()); |
|
| 83 | - |
|
| 84 | - // Contact specific props |
|
| 85 | - $contactproperties = MAPIMapping::GetContactProperties(); |
|
| 86 | - $messageprops = $this->getProps($mapimessage, $contactproperties); |
|
| 87 | - |
|
| 88 | - // set the body according to contentparameters and supported AS version |
|
| 89 | - $this->setMessageBody($mapimessage, $contentparameters, $message); |
|
| 90 | - |
|
| 91 | - // check the picture |
|
| 92 | - if (isset($messageprops[$contactproperties["haspic"]]) && $messageprops[$contactproperties["haspic"]]) { |
|
| 93 | - // Add attachments |
|
| 94 | - $attachtable = mapi_message_getattachmenttable($mapimessage); |
|
| 95 | - mapi_table_restrict($attachtable, MAPIUtils::GetContactPicRestriction()); |
|
| 96 | - $rows = mapi_table_queryallrows($attachtable, [PR_ATTACH_NUM, PR_ATTACH_SIZE]); |
|
| 97 | - |
|
| 98 | - foreach ($rows as $row) { |
|
| 99 | - if (isset($row[PR_ATTACH_NUM])) { |
|
| 100 | - $mapiattach = mapi_message_openattach($mapimessage, $row[PR_ATTACH_NUM]); |
|
| 101 | - $message->picture = base64_encode(mapi_attach_openbin($mapiattach, PR_ATTACH_DATA_BIN)); |
|
| 102 | - } |
|
| 103 | - } |
|
| 104 | - } |
|
| 105 | - |
|
| 106 | - return $message; |
|
| 107 | - } |
|
| 108 | - |
|
| 109 | - /** |
|
| 110 | - * Reads a task object from MAPI. |
|
| 111 | - * |
|
| 112 | - * @param mixed $mapimessage |
|
| 113 | - * @param ContentParameters $contentparameters |
|
| 114 | - * |
|
| 115 | - * @return SyncTask |
|
| 116 | - */ |
|
| 117 | - private function getTask($mapimessage, $contentparameters) { |
|
| 118 | - $message = new SyncTask(); |
|
| 119 | - |
|
| 120 | - // Standard one-to-one mappings first |
|
| 121 | - $this->getPropsFromMAPI($message, $mapimessage, MAPIMapping::GetTaskMapping()); |
|
| 122 | - |
|
| 123 | - // Task specific props |
|
| 124 | - $taskproperties = MAPIMapping::GetTaskProperties(); |
|
| 125 | - $messageprops = $this->getProps($mapimessage, $taskproperties); |
|
| 126 | - |
|
| 127 | - // set the body according to contentparameters and supported AS version |
|
| 128 | - $this->setMessageBody($mapimessage, $contentparameters, $message); |
|
| 129 | - |
|
| 130 | - // task with deadoccur is an occurrence of a recurring task and does not need to be handled as recurring |
|
| 131 | - // webaccess does not set deadoccur for the initial recurring task |
|
| 132 | - if (isset($messageprops[$taskproperties["isrecurringtag"]]) && |
|
| 133 | - $messageprops[$taskproperties["isrecurringtag"]] && |
|
| 134 | - (!isset($messageprops[$taskproperties["deadoccur"]]) || |
|
| 135 | - (isset($messageprops[$taskproperties["deadoccur"]]) && |
|
| 136 | - !$messageprops[$taskproperties["deadoccur"]]))) { |
|
| 137 | - // Process recurrence |
|
| 138 | - $message->recurrence = new SyncTaskRecurrence(); |
|
| 139 | - $this->getRecurrence($mapimessage, $messageprops, $message, $message->recurrence, false); |
|
| 140 | - } |
|
| 141 | - |
|
| 142 | - // when set the task to complete using the WebAccess, the dateComplete property is not set correctly |
|
| 143 | - if ($message->complete == 1 && !isset($message->datecompleted)) { |
|
| 144 | - $message->datecompleted = time(); |
|
| 145 | - } |
|
| 146 | - |
|
| 147 | - // if no reminder is set, announce that to the mobile |
|
| 148 | - if (!isset($message->reminderset)) { |
|
| 149 | - $message->reminderset = 0; |
|
| 150 | - } |
|
| 151 | - |
|
| 152 | - return $message; |
|
| 153 | - } |
|
| 154 | - |
|
| 155 | - /** |
|
| 156 | - * Reads an appointment object from MAPI. |
|
| 157 | - * |
|
| 158 | - * @param mixed $mapimessage |
|
| 159 | - * @param ContentParameters $contentparameters |
|
| 160 | - * |
|
| 161 | - * @return SyncAppointment |
|
| 162 | - */ |
|
| 163 | - private function getAppointment($mapimessage, $contentparameters) { |
|
| 164 | - $message = new SyncAppointment(); |
|
| 165 | - |
|
| 166 | - // Standard one-to-one mappings first |
|
| 167 | - $this->getPropsFromMAPI($message, $mapimessage, MAPIMapping::GetAppointmentMapping()); |
|
| 168 | - |
|
| 169 | - // Appointment specific props |
|
| 170 | - $appointmentprops = MAPIMapping::GetAppointmentProperties(); |
|
| 171 | - $messageprops = $this->getProps($mapimessage, $appointmentprops); |
|
| 172 | - |
|
| 173 | - // set the body according to contentparameters and supported AS version |
|
| 174 | - $this->setMessageBody($mapimessage, $contentparameters, $message); |
|
| 175 | - |
|
| 176 | - // Set reminder time if reminderset is true |
|
| 177 | - if (isset($messageprops[$appointmentprops["reminderset"]]) && $messageprops[$appointmentprops["reminderset"]] == true) { |
|
| 178 | - if ($messageprops[$appointmentprops["remindertime"]] == 0x5AE980E1) { |
|
| 179 | - $message->reminder = 15; |
|
| 180 | - } |
|
| 181 | - else { |
|
| 182 | - $message->reminder = $messageprops[$appointmentprops["remindertime"]]; |
|
| 183 | - } |
|
| 184 | - } |
|
| 185 | - |
|
| 186 | - if (!isset($message->uid)) { |
|
| 187 | - $message->uid = bin2hex($messageprops[$appointmentprops["sourcekey"]]); |
|
| 188 | - } |
|
| 189 | - else { |
|
| 190 | - $message->uid = Utils::GetICalUidFromOLUid($message->uid); |
|
| 191 | - } |
|
| 192 | - |
|
| 193 | - // Always set organizer information because some devices do not work properly without it |
|
| 194 | - if (isset($messageprops[$appointmentprops["representingentryid"]], $messageprops[$appointmentprops["representingname"]]) |
|
| 195 | - ) { |
|
| 196 | - $message->organizeremail = w2u($this->getSMTPAddressFromEntryID($messageprops[$appointmentprops["representingentryid"]])); |
|
| 197 | - // if the email address can't be resolved, fall back to PR_SENT_REPRESENTING_SEARCH_KEY |
|
| 198 | - if ($message->organizeremail == "" && isset($messageprops[$appointmentprops["sentrepresentinsrchk"]])) { |
|
| 199 | - $message->organizeremail = $this->getEmailAddressFromSearchKey($messageprops[$appointmentprops["sentrepresentinsrchk"]]); |
|
| 200 | - } |
|
| 201 | - $message->organizername = w2u($messageprops[$appointmentprops["representingname"]]); |
|
| 202 | - } |
|
| 203 | - |
|
| 204 | - $appTz = false; // if the appointment has some timezone information saved on the server |
|
| 205 | - if (!empty($messageprops[$appointmentprops["timezonetag"]])) { |
|
| 206 | - $tz = $this->getTZFromMAPIBlob($messageprops[$appointmentprops["timezonetag"]]); |
|
| 207 | - $appTz = true; |
|
| 208 | - } |
|
| 209 | - elseif (!empty($messageprops[$appointmentprops["timezonedesc"]])) { |
|
| 210 | - // Windows uses UTC in timezone description in opposite to mstzones in TimezoneUtil which uses GMT |
|
| 211 | - $wintz = str_replace("UTC", "GMT", $messageprops[$appointmentprops["timezonedesc"]]); |
|
| 212 | - $tz = TimezoneUtil::GetFullTZFromTZName(TimezoneUtil::GetTZNameFromWinTZ($wintz)); |
|
| 213 | - $appTz = true; |
|
| 214 | - } |
|
| 215 | - else { |
|
| 216 | - // set server default timezone (correct timezone should be configured!) |
|
| 217 | - $tz = TimezoneUtil::GetFullTZ(); |
|
| 218 | - } |
|
| 219 | - |
|
| 220 | - if (isset($messageprops[$appointmentprops["isrecurring"]]) && $messageprops[$appointmentprops["isrecurring"]]) { |
|
| 221 | - // Process recurrence |
|
| 222 | - $message->recurrence = new SyncRecurrence(); |
|
| 223 | - $this->getRecurrence($mapimessage, $messageprops, $message, $message->recurrence, $tz); |
|
| 224 | - |
|
| 225 | - if (empty($message->alldayevent)) { |
|
| 226 | - $message->timezone = base64_encode(TimezoneUtil::GetSyncBlobFromTZ($tz)); |
|
| 227 | - } |
|
| 228 | - } |
|
| 229 | - |
|
| 230 | - // Do attendees |
|
| 231 | - $reciptable = mapi_message_getrecipienttable($mapimessage); |
|
| 232 | - // Only get first 256 recipients, to prevent possible load issues. |
|
| 233 | - $rows = mapi_table_queryrows($reciptable, [PR_DISPLAY_NAME, PR_EMAIL_ADDRESS, PR_SMTP_ADDRESS, PR_ADDRTYPE, PR_RECIPIENT_TRACKSTATUS, PR_RECIPIENT_TYPE, PR_SEARCH_KEY], 0, 256); |
|
| 234 | - |
|
| 235 | - // Exception: we do not synchronize appointments with more than 250 attendees |
|
| 236 | - if (count($rows) > 250) { |
|
| 237 | - $message->id = bin2hex($messageprops[$appointmentprops["sourcekey"]]); |
|
| 238 | - $mbe = new SyncObjectBrokenException("Appointment has too many attendees"); |
|
| 239 | - $mbe->SetSyncObject($message); |
|
| 240 | - |
|
| 241 | - throw $mbe; |
|
| 242 | - } |
|
| 243 | - |
|
| 244 | - if (count($rows) > 0) { |
|
| 245 | - $message->attendees = []; |
|
| 246 | - } |
|
| 247 | - |
|
| 248 | - foreach ($rows as $row) { |
|
| 249 | - $attendee = new SyncAttendee(); |
|
| 250 | - |
|
| 251 | - $attendee->name = w2u($row[PR_DISPLAY_NAME]); |
|
| 252 | - // smtp address is always a proper email address |
|
| 253 | - if (isset($row[PR_SMTP_ADDRESS])) { |
|
| 254 | - $attendee->email = w2u($row[PR_SMTP_ADDRESS]); |
|
| 255 | - } |
|
| 256 | - elseif (isset($row[PR_ADDRTYPE], $row[PR_EMAIL_ADDRESS])) { |
|
| 257 | - // if address type is SMTP, it's also a proper email address |
|
| 258 | - if ($row[PR_ADDRTYPE] == "SMTP") { |
|
| 259 | - $attendee->email = w2u($row[PR_EMAIL_ADDRESS]); |
|
| 260 | - } |
|
| 261 | - // if address type is ZARAFA, the PR_EMAIL_ADDRESS contains username |
|
| 262 | - elseif ($row[PR_ADDRTYPE] == "ZARAFA") { |
|
| 263 | - $userinfo = @nsp_getuserinfo($row[PR_EMAIL_ADDRESS]); |
|
| 264 | - if (is_array($userinfo) && isset($userinfo["primary_email"])) { |
|
| 265 | - $attendee->email = w2u($userinfo["primary_email"]); |
|
| 266 | - } |
|
| 267 | - // if the user was not found, do a fallback to PR_SEARCH_KEY |
|
| 268 | - // @see https://jira.z-hub.io/browse/ZP-1178 |
|
| 269 | - elseif (isset($row[PR_SEARCH_KEY])) { |
|
| 270 | - $attendee->email = w2u($this->getEmailAddressFromSearchKey($row[PR_SEARCH_KEY])); |
|
| 271 | - } |
|
| 272 | - else { |
|
| 273 | - SLog::Write(LOGLEVEL_WARN, sprintf("MAPIProvider->getAppointment: The attendee '%s' of type ZARAFA can not be resolved. Code: 0x%X", $row[PR_EMAIL_ADDRESS], mapi_last_hresult())); |
|
| 274 | - } |
|
| 275 | - } |
|
| 276 | - } |
|
| 277 | - |
|
| 278 | - // set attendee's status and type if they're available and if we are the organizer |
|
| 279 | - $storeprops = $this->GetStoreProps(); |
|
| 280 | - if (isset($row[PR_RECIPIENT_TRACKSTATUS], $messageprops[$appointmentprops["representingentryid"]], $storeprops[PR_MAILBOX_OWNER_ENTRYID]) && |
|
| 281 | - $messageprops[$appointmentprops["representingentryid"]] == $storeprops[PR_MAILBOX_OWNER_ENTRYID]) { |
|
| 282 | - $attendee->attendeestatus = $row[PR_RECIPIENT_TRACKSTATUS]; |
|
| 283 | - } |
|
| 284 | - if (isset($row[PR_RECIPIENT_TYPE])) { |
|
| 285 | - $attendee->attendeetype = $row[PR_RECIPIENT_TYPE]; |
|
| 286 | - } |
|
| 287 | - // Some attendees have no email or name (eg resources), and if you |
|
| 288 | - // don't send one of those fields, the phone will give an error ... so |
|
| 289 | - // we don't send it in that case. |
|
| 290 | - // also ignore the "attendee" if the email is equal to the organizers' email |
|
| 291 | - if (isset($attendee->name, $attendee->email) && $attendee->email != "" && (!isset($message->organizeremail) || (isset($message->organizeremail) && $attendee->email != $message->organizeremail))) { |
|
| 292 | - array_push($message->attendees, $attendee); |
|
| 293 | - } |
|
| 294 | - } |
|
| 295 | - |
|
| 296 | - // Status 0 = no meeting, status 1 = organizer, status 2/3/4/5 = tentative/accepted/declined/notresponded |
|
| 297 | - if (isset($messageprops[$appointmentprops["meetingstatus"]]) && $messageprops[$appointmentprops["meetingstatus"]] > 1) { |
|
| 298 | - if (!isset($message->attendees) || !is_array($message->attendees)) { |
|
| 299 | - $message->attendees = []; |
|
| 300 | - } |
|
| 301 | - // Work around iOS6 cancellation issue when there are no attendees for this meeting. Just add ourselves as the sole attendee. |
|
| 302 | - if (count($message->attendees) == 0) { |
|
| 303 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->getAppointment: adding ourself as an attendee for iOS6 workaround")); |
|
| 304 | - $attendee = new SyncAttendee(); |
|
| 305 | - |
|
| 306 | - $meinfo = nsp_getuserinfo(Request::GetUser()); |
|
| 307 | - |
|
| 308 | - if (is_array($meinfo)) { |
|
| 309 | - $attendee->email = w2u($meinfo["primary_email"]); |
|
| 310 | - $attendee->name = w2u($meinfo["fullname"]); |
|
| 311 | - $attendee->attendeetype = MAPI_TO; |
|
| 312 | - |
|
| 313 | - array_push($message->attendees, $attendee); |
|
| 314 | - } |
|
| 315 | - } |
|
| 316 | - $message->responsetype = $messageprops[$appointmentprops["responsestatus"]]; |
|
| 317 | - } |
|
| 318 | - |
|
| 319 | - // If it's an appointment which doesn't have any attendees, we have to make sure that |
|
| 320 | - // the user is the owner or it will not work properly with android devices |
|
| 321 | - // @see https://jira.z-hub.io/browse/ZP-1020 |
|
| 322 | - if (isset($messageprops[$appointmentprops["meetingstatus"]]) && $messageprops[$appointmentprops["meetingstatus"]] == olNonMeeting && empty($message->attendees)) { |
|
| 323 | - $meinfo = nsp_getuserinfo(Request::GetUser()); |
|
| 324 | - |
|
| 325 | - if (is_array($meinfo)) { |
|
| 326 | - $message->organizeremail = w2u($meinfo["primary_email"]); |
|
| 327 | - $message->organizername = w2u($meinfo["fullname"]); |
|
| 328 | - SLog::Write(LOGLEVEL_DEBUG, "MAPIProvider->getAppointment(): setting ourself as the organizer for an appointment without attendees."); |
|
| 329 | - } |
|
| 330 | - } |
|
| 331 | - |
|
| 332 | - if (!isset($message->nativebodytype)) { |
|
| 333 | - $message->nativebodytype = MAPIUtils::GetNativeBodyType($messageprops); |
|
| 334 | - } |
|
| 335 | - elseif ($message->nativebodytype == SYNC_BODYPREFERENCE_UNDEFINED) { |
|
| 336 | - $nbt = MAPIUtils::GetNativeBodyType($messageprops); |
|
| 337 | - SLog::Write(LOGLEVEL_INFO, sprintf("MAPIProvider->getAppointment(): native body type is undefined. Set it to %d.", $nbt)); |
|
| 338 | - $message->nativebodytype = $nbt; |
|
| 339 | - } |
|
| 340 | - |
|
| 341 | - // If the user is working from a location other than the office the busystatus should be interpreted as free. |
|
| 342 | - if (isset($message->busystatus) && $message->busystatus == fbWorkingElsewhere) { |
|
| 343 | - $message->busystatus = fbFree; |
|
| 344 | - } |
|
| 345 | - |
|
| 346 | - // If the busystatus has the value of -1, we should be interpreted as tentative (1) / ZP-581 |
|
| 347 | - if (isset($message->busystatus) && $message->busystatus == -1) { |
|
| 348 | - $message->busystatus = fbTentative; |
|
| 349 | - } |
|
| 350 | - |
|
| 351 | - // All-day events might appear as 24h (or multiple of it) long when they start not exactly at midnight (+/- bias of the timezone) |
|
| 352 | - if (isset($message->alldayevent) && $message->alldayevent) { |
|
| 353 | - $localStartTime = localtime($message->starttime, 1); |
|
| 354 | - |
|
| 355 | - // The appointment is all-day but doesn't start at midnight. |
|
| 356 | - // If it was created in another timezone and we have that information, |
|
| 357 | - // set the startime to the midnight of the current timezone. |
|
| 358 | - if ($appTz && ($localStartTime['tm_hour'] || $localStartTime['tm_min'])) { |
|
| 359 | - SLog::Write(LOGLEVEL_DEBUG, "MAPIProvider->getAppointment(): all-day event starting not midnight."); |
|
| 360 | - $duration = $message->endtime - $message->starttime; |
|
| 361 | - $serverTz = TimezoneUtil::GetFullTZ(); |
|
| 362 | - $message->starttime = $this->getGMTTimeByTZ($this->getLocaltimeByTZ($message->starttime, $tz), $serverTz); |
|
| 363 | - $message->endtime = $message->starttime + $duration; |
|
| 364 | - } |
|
| 365 | - } |
|
| 366 | - |
|
| 367 | - return $message; |
|
| 368 | - } |
|
| 369 | - |
|
| 370 | - /** |
|
| 371 | - * Reads recurrence information from MAPI. |
|
| 372 | - * |
|
| 373 | - * @param mixed $mapimessage |
|
| 374 | - * @param array $recurprops |
|
| 375 | - * @param SyncObject &$syncMessage the message |
|
| 376 | - * @param SyncObject &$syncRecurrence the recurrence message |
|
| 377 | - * @param array $tz timezone information |
|
| 378 | - * |
|
| 379 | - * @return |
|
| 380 | - */ |
|
| 381 | - private function getRecurrence($mapimessage, $recurprops, &$syncMessage, &$syncRecurrence, $tz) { |
|
| 382 | - if ($syncRecurrence instanceof SyncTaskRecurrence) { |
|
| 383 | - $recurrence = new TaskRecurrence($this->store, $mapimessage); |
|
| 384 | - } |
|
| 385 | - else { |
|
| 386 | - $recurrence = new Recurrence($this->store, $mapimessage); |
|
| 387 | - } |
|
| 388 | - |
|
| 389 | - switch ($recurrence->recur["type"]) { |
|
| 390 | - case 10: // daily |
|
| 391 | - switch ($recurrence->recur["subtype"]) { |
|
| 392 | - default: |
|
| 393 | - $syncRecurrence->type = 0; |
|
| 394 | - break; |
|
| 395 | - |
|
| 396 | - case 1: |
|
| 397 | - $syncRecurrence->type = 0; |
|
| 398 | - $syncRecurrence->dayofweek = 62; // mon-fri |
|
| 399 | - $syncRecurrence->interval = 1; |
|
| 400 | - break; |
|
| 401 | - } |
|
| 402 | - break; |
|
| 403 | - |
|
| 404 | - case 11: // weekly |
|
| 405 | - $syncRecurrence->type = 1; |
|
| 406 | - break; |
|
| 407 | - |
|
| 408 | - case 12: // monthly |
|
| 409 | - switch ($recurrence->recur["subtype"]) { |
|
| 410 | - default: |
|
| 411 | - $syncRecurrence->type = 2; |
|
| 412 | - break; |
|
| 413 | - |
|
| 414 | - case 3: |
|
| 415 | - $syncRecurrence->type = 3; |
|
| 416 | - break; |
|
| 417 | - } |
|
| 418 | - break; |
|
| 419 | - |
|
| 420 | - case 13: // yearly |
|
| 421 | - switch ($recurrence->recur["subtype"]) { |
|
| 422 | - default: |
|
| 423 | - $syncRecurrence->type = 4; |
|
| 424 | - break; |
|
| 425 | - |
|
| 426 | - case 2: |
|
| 427 | - $syncRecurrence->type = 5; |
|
| 428 | - break; |
|
| 429 | - |
|
| 430 | - case 3: |
|
| 431 | - $syncRecurrence->type = 6; |
|
| 432 | - break; |
|
| 433 | - } |
|
| 434 | - } |
|
| 435 | - // Termination |
|
| 436 | - switch ($recurrence->recur["term"]) { |
|
| 437 | - case 0x21: |
|
| 438 | - $syncRecurrence->until = $recurrence->recur["end"]; |
|
| 439 | - // fixes Mantis #350 : recur-end does not consider timezones - use ClipEnd if available |
|
| 440 | - if (isset($recurprops[$recurrence->proptags["enddate_recurring"]])) { |
|
| 441 | - $syncRecurrence->until = $recurprops[$recurrence->proptags["enddate_recurring"]]; |
|
| 442 | - } |
|
| 443 | - // add one day (minus 1 sec) to the end time to make sure the last occurrence is covered |
|
| 444 | - $syncRecurrence->until += 86399; |
|
| 445 | - break; |
|
| 446 | - |
|
| 447 | - case 0x22: |
|
| 448 | - $syncRecurrence->occurrences = $recurrence->recur["numoccur"]; |
|
| 449 | - break; |
|
| 450 | - |
|
| 451 | - case 0x23: |
|
| 452 | - // never ends |
|
| 453 | - break; |
|
| 454 | - } |
|
| 455 | - |
|
| 456 | - // Correct 'alldayevent' because outlook fails to set it on recurring items of 24 hours or longer |
|
| 457 | - if (isset($recurrence->recur["endocc"], $recurrence->recur["startocc"]) && ($recurrence->recur["endocc"] - $recurrence->recur["startocc"] >= 1440)) { |
|
| 458 | - $syncMessage->alldayevent = true; |
|
| 459 | - } |
|
| 460 | - |
|
| 461 | - // Interval is different according to the type/subtype |
|
| 462 | - switch ($recurrence->recur["type"]) { |
|
| 463 | - case 10: |
|
| 464 | - if ($recurrence->recur["subtype"] == 0) { |
|
| 465 | - $syncRecurrence->interval = (int) ($recurrence->recur["everyn"] / 1440); |
|
| 466 | - } // minutes |
|
| 467 | - break; |
|
| 468 | - |
|
| 469 | - case 11: |
|
| 470 | - case 12: |
|
| 471 | - $syncRecurrence->interval = $recurrence->recur["everyn"]; |
|
| 472 | - break; // months / weeks |
|
| 473 | - |
|
| 474 | - case 13: |
|
| 475 | - $syncRecurrence->interval = (int) ($recurrence->recur["everyn"] / 12); |
|
| 476 | - break; // months |
|
| 477 | - } |
|
| 478 | - |
|
| 479 | - if (isset($recurrence->recur["weekdays"])) { |
|
| 480 | - $syncRecurrence->dayofweek = $recurrence->recur["weekdays"]; |
|
| 481 | - } // bitmask of days (1 == sunday, 128 == saturday |
|
| 482 | - if (isset($recurrence->recur["nday"])) { |
|
| 483 | - $syncRecurrence->weekofmonth = $recurrence->recur["nday"]; |
|
| 484 | - } // N'th {DAY} of {X} (0-5) |
|
| 485 | - if (isset($recurrence->recur["month"])) { |
|
| 486 | - $syncRecurrence->monthofyear = (int) ($recurrence->recur["month"] / (60 * 24 * 29)) + 1; |
|
| 487 | - } // works ok due to rounding. see also $monthminutes below (1-12) |
|
| 488 | - if (isset($recurrence->recur["monthday"])) { |
|
| 489 | - $syncRecurrence->dayofmonth = $recurrence->recur["monthday"]; |
|
| 490 | - } // day of month (1-31) |
|
| 491 | - |
|
| 492 | - // All changed exceptions are appointments within the 'exceptions' array. They contain the same items as a normal appointment |
|
| 493 | - foreach ($recurrence->recur["changed_occurrences"] as $change) { |
|
| 494 | - $exception = new SyncAppointmentException(); |
|
| 495 | - |
|
| 496 | - // start, end, basedate, subject, remind_before, reminderset, location, busystatus, alldayevent, label |
|
| 497 | - if (isset($change["start"])) { |
|
| 498 | - $exception->starttime = $this->getGMTTimeByTZ($change["start"], $tz); |
|
| 499 | - } |
|
| 500 | - if (isset($change["end"])) { |
|
| 501 | - $exception->endtime = $this->getGMTTimeByTZ($change["end"], $tz); |
|
| 502 | - } |
|
| 503 | - if (isset($change["basedate"])) { |
|
| 504 | - $exception->exceptionstarttime = $this->getGMTTimeByTZ($this->getDayStartOfTimestamp($change["basedate"]) + $recurrence->recur["startocc"] * 60, $tz); |
|
| 505 | - |
|
| 506 | - // open body because getting only property might not work because of memory limit |
|
| 507 | - $exceptionatt = $recurrence->getExceptionAttachment($change["basedate"]); |
|
| 508 | - if ($exceptionatt) { |
|
| 509 | - $exceptionobj = mapi_attach_openobj($exceptionatt, 0); |
|
| 510 | - $this->setMessageBodyForType($exceptionobj, SYNC_BODYPREFERENCE_PLAIN, $exception); |
|
| 511 | - } |
|
| 512 | - } |
|
| 513 | - if (isset($change["subject"])) { |
|
| 514 | - $exception->subject = w2u($change["subject"]); |
|
| 515 | - } |
|
| 516 | - if (isset($change["reminder_before"]) && $change["reminder_before"]) { |
|
| 517 | - $exception->reminder = $change["remind_before"]; |
|
| 518 | - } |
|
| 519 | - if (isset($change["location"])) { |
|
| 520 | - $exception->location = w2u($change["location"]); |
|
| 521 | - } |
|
| 522 | - if (isset($change["busystatus"])) { |
|
| 523 | - $exception->busystatus = $change["busystatus"]; |
|
| 524 | - } |
|
| 525 | - if (isset($change["alldayevent"])) { |
|
| 526 | - $exception->alldayevent = $change["alldayevent"]; |
|
| 527 | - } |
|
| 528 | - |
|
| 529 | - // set some data from the original appointment |
|
| 530 | - if (isset($syncMessage->uid)) { |
|
| 531 | - $exception->uid = $syncMessage->uid; |
|
| 532 | - } |
|
| 533 | - if (isset($syncMessage->organizername)) { |
|
| 534 | - $exception->organizername = $syncMessage->organizername; |
|
| 535 | - } |
|
| 536 | - if (isset($syncMessage->organizeremail)) { |
|
| 537 | - $exception->organizeremail = $syncMessage->organizeremail; |
|
| 538 | - } |
|
| 539 | - |
|
| 540 | - if (!isset($syncMessage->exceptions)) { |
|
| 541 | - $syncMessage->exceptions = []; |
|
| 542 | - } |
|
| 543 | - |
|
| 544 | - // If the user is working from a location other than the office the busystatus should be interpreted as free. |
|
| 545 | - if (isset($exception->busystatus) && $exception->busystatus == fbWorkingElsewhere) { |
|
| 546 | - $exception->busystatus = fbFree; |
|
| 547 | - } |
|
| 548 | - |
|
| 549 | - // If the busystatus has the value of -1, we should be interpreted as tentative (1) / ZP-581 |
|
| 550 | - if (isset($exception->busystatus) && $exception->busystatus == -1) { |
|
| 551 | - $exception->busystatus = fbTentative; |
|
| 552 | - } |
|
| 553 | - |
|
| 554 | - // if an exception lasts 24 hours and the series are an allday events, set also the exception to allday event, |
|
| 555 | - // otherwise it will be a 24 hour long event on some mobiles. |
|
| 556 | - // @see https://jira.z-hub.io/browse/ZP-980 |
|
| 557 | - if (isset($exception->starttime, $exception->endtime) && ($exception->endtime - $exception->starttime == 86400) && $syncMessage->alldayevent) { |
|
| 558 | - $exception->alldayevent = 1; |
|
| 559 | - } |
|
| 560 | - array_push($syncMessage->exceptions, $exception); |
|
| 561 | - } |
|
| 562 | - |
|
| 563 | - // Deleted appointments contain only the original date (basedate) and a 'deleted' tag |
|
| 564 | - foreach ($recurrence->recur["deleted_occurrences"] as $deleted) { |
|
| 565 | - $exception = new SyncAppointmentException(); |
|
| 566 | - |
|
| 567 | - $exception->exceptionstarttime = $this->getGMTTimeByTZ($this->getDayStartOfTimestamp($deleted) + $recurrence->recur["startocc"] * 60, $tz); |
|
| 568 | - $exception->deleted = "1"; |
|
| 569 | - |
|
| 570 | - if (!isset($syncMessage->exceptions)) { |
|
| 571 | - $syncMessage->exceptions = []; |
|
| 572 | - } |
|
| 573 | - |
|
| 574 | - array_push($syncMessage->exceptions, $exception); |
|
| 575 | - } |
|
| 576 | - |
|
| 577 | - if (isset($syncMessage->complete) && $syncMessage->complete) { |
|
| 578 | - $syncRecurrence->complete = $syncMessage->complete; |
|
| 579 | - } |
|
| 580 | - } |
|
| 581 | - |
|
| 582 | - /** |
|
| 583 | - * Reads an email object from MAPI. |
|
| 584 | - * |
|
| 585 | - * @param mixed $mapimessage |
|
| 586 | - * @param ContentParameters $contentparameters |
|
| 587 | - * |
|
| 588 | - * @return SyncEmail |
|
| 589 | - */ |
|
| 590 | - private function getEmail($mapimessage, $contentparameters) { |
|
| 591 | - // This workaround fixes ZP-729 and still works with Outlook. |
|
| 592 | - // FIXME: It should be properly fixed when refactoring. |
|
| 593 | - $bpReturnType = Utils::GetBodyPreferenceBestMatch($contentparameters->GetBodyPreference()); |
|
| 594 | - if (($contentparameters->GetMimeSupport() == SYNC_MIMESUPPORT_NEVER) || |
|
| 595 | - ($key = array_search(SYNC_BODYPREFERENCE_MIME, $contentparameters->GetBodyPreference()) === false) || |
|
| 596 | - $bpReturnType != SYNC_BODYPREFERENCE_MIME) { |
|
| 597 | - MAPIUtils::ParseSmime($this->session, $this->store, $this->getAddressbook(), $mapimessage); |
|
| 598 | - } |
|
| 599 | - |
|
| 600 | - $message = new SyncMail(); |
|
| 601 | - |
|
| 602 | - $this->getPropsFromMAPI($message, $mapimessage, MAPIMapping::GetEmailMapping()); |
|
| 603 | - |
|
| 604 | - $emailproperties = MAPIMapping::GetEmailProperties(); |
|
| 605 | - $messageprops = $this->getProps($mapimessage, $emailproperties); |
|
| 606 | - |
|
| 607 | - if (isset($messageprops[PR_SOURCE_KEY])) { |
|
| 608 | - $sourcekey = $messageprops[PR_SOURCE_KEY]; |
|
| 609 | - } |
|
| 610 | - else { |
|
| 611 | - $mbe = new SyncObjectBrokenException("The message doesn't have a sourcekey"); |
|
| 612 | - $mbe->SetSyncObject($message); |
|
| 613 | - |
|
| 614 | - throw $mbe; |
|
| 615 | - } |
|
| 616 | - |
|
| 617 | - // set the body according to contentparameters and supported AS version |
|
| 618 | - $this->setMessageBody($mapimessage, $contentparameters, $message); |
|
| 619 | - |
|
| 620 | - $fromname = $fromaddr = ""; |
|
| 621 | - |
|
| 622 | - if (isset($messageprops[$emailproperties["representingname"]])) { |
|
| 623 | - // remove encapsulating double quotes from the representingname |
|
| 624 | - $fromname = preg_replace('/^\"(.*)\"$/', "\${1}", $messageprops[$emailproperties["representingname"]]); |
|
| 625 | - } |
|
| 626 | - if (isset($messageprops[$emailproperties["representingentryid"]])) { |
|
| 627 | - $fromaddr = $this->getSMTPAddressFromEntryID($messageprops[$emailproperties["representingentryid"]]); |
|
| 628 | - } |
|
| 629 | - |
|
| 630 | - // if the email address can't be resolved, fall back to PR_SENT_REPRESENTING_SEARCH_KEY |
|
| 631 | - if ($fromaddr == "" && isset($messageprops[$emailproperties["representingsearchkey"]])) { |
|
| 632 | - $fromaddr = $this->getEmailAddressFromSearchKey($messageprops[$emailproperties["representingsearchkey"]]); |
|
| 633 | - } |
|
| 634 | - |
|
| 635 | - if ($fromname == $fromaddr) { |
|
| 636 | - $fromname = ""; |
|
| 637 | - } |
|
| 638 | - |
|
| 639 | - if ($fromname) { |
|
| 640 | - $from = "\"" . w2u($fromname) . "\" <" . w2u($fromaddr) . ">"; |
|
| 641 | - } |
|
| 642 | - else { // START CHANGED dw2412 HTC shows "error" if sender name is unknown |
|
| 643 | - $from = "\"" . w2u($fromaddr) . "\" <" . w2u($fromaddr) . ">"; |
|
| 644 | - } |
|
| 645 | - // END CHANGED dw2412 HTC shows "error" if sender name is unknown |
|
| 646 | - |
|
| 647 | - $message->from = $from; |
|
| 648 | - |
|
| 649 | - // process Meeting Requests |
|
| 650 | - if (isset($message->messageclass) && strpos($message->messageclass, "IPM.Schedule.Meeting") === 0) { |
|
| 651 | - $message->meetingrequest = new SyncMeetingRequest(); |
|
| 652 | - $this->getPropsFromMAPI($message->meetingrequest, $mapimessage, MAPIMapping::GetMeetingRequestMapping()); |
|
| 653 | - |
|
| 654 | - $meetingrequestproperties = MAPIMapping::GetMeetingRequestProperties(); |
|
| 655 | - $props = $this->getProps($mapimessage, $meetingrequestproperties); |
|
| 656 | - |
|
| 657 | - // Get the GOID |
|
| 658 | - if (isset($props[$meetingrequestproperties["goidtag"]])) { |
|
| 659 | - $message->meetingrequest->globalobjid = base64_encode($props[$meetingrequestproperties["goidtag"]]); |
|
| 660 | - } |
|
| 661 | - |
|
| 662 | - // Set Timezone |
|
| 663 | - if (isset($props[$meetingrequestproperties["timezonetag"]])) { |
|
| 664 | - $tz = $this->getTZFromMAPIBlob($props[$meetingrequestproperties["timezonetag"]]); |
|
| 665 | - } |
|
| 666 | - else { |
|
| 667 | - $tz = TimezoneUtil::GetFullTZ(); |
|
| 668 | - } |
|
| 669 | - |
|
| 670 | - $message->meetingrequest->timezone = base64_encode(TimezoneUtil::GetSyncBlobFromTZ($tz)); |
|
| 671 | - |
|
| 672 | - // send basedate if exception |
|
| 673 | - if (isset($props[$meetingrequestproperties["recReplTime"]]) || |
|
| 674 | - (isset($props[$meetingrequestproperties["lidIsException"]]) && $props[$meetingrequestproperties["lidIsException"]] == true)) { |
|
| 675 | - if (isset($props[$meetingrequestproperties["recReplTime"]])) { |
|
| 676 | - $basedate = $props[$meetingrequestproperties["recReplTime"]]; |
|
| 677 | - $message->meetingrequest->recurrenceid = $this->getGMTTimeByTZ($basedate, $this->getGMTTZ()); |
|
| 678 | - } |
|
| 679 | - else { |
|
| 680 | - if (!isset($props[$meetingrequestproperties["goidtag"]]) || !isset($props[$meetingrequestproperties["recurStartTime"]]) || !isset($props[$meetingrequestproperties["timezonetag"]])) { |
|
| 681 | - SLog::Write(LOGLEVEL_WARN, "Missing property to set correct basedate for exception"); |
|
| 682 | - } |
|
| 683 | - else { |
|
| 684 | - $basedate = Utils::ExtractBaseDate($props[$meetingrequestproperties["goidtag"]], $props[$meetingrequestproperties["recurStartTime"]]); |
|
| 685 | - $message->meetingrequest->recurrenceid = $this->getGMTTimeByTZ($basedate, $tz); |
|
| 686 | - } |
|
| 687 | - } |
|
| 688 | - } |
|
| 689 | - |
|
| 690 | - // Organizer is the sender |
|
| 691 | - if (strpos($message->messageclass, "IPM.Schedule.Meeting.Resp") === 0) { |
|
| 692 | - $message->meetingrequest->organizer = $message->to; |
|
| 693 | - } |
|
| 694 | - else { |
|
| 695 | - $message->meetingrequest->organizer = $message->from; |
|
| 696 | - } |
|
| 697 | - |
|
| 698 | - // Process recurrence |
|
| 699 | - if (isset($props[$meetingrequestproperties["isrecurringtag"]]) && $props[$meetingrequestproperties["isrecurringtag"]]) { |
|
| 700 | - $myrec = new SyncMeetingRequestRecurrence(); |
|
| 701 | - // get recurrence -> put $message->meetingrequest as message so the 'alldayevent' is set correctly |
|
| 702 | - $this->getRecurrence($mapimessage, $props, $message->meetingrequest, $myrec, $tz); |
|
| 703 | - $message->meetingrequest->recurrences = [$myrec]; |
|
| 704 | - } |
|
| 705 | - |
|
| 706 | - // Force the 'alldayevent' in the object at all times. (non-existent == 0) |
|
| 707 | - if (!isset($message->meetingrequest->alldayevent) || $message->meetingrequest->alldayevent == "") { |
|
| 708 | - $message->meetingrequest->alldayevent = 0; |
|
| 709 | - } |
|
| 710 | - |
|
| 711 | - // Instancetype |
|
| 712 | - // 0 = single appointment |
|
| 713 | - // 1 = master recurring appointment |
|
| 714 | - // 2 = single instance of recurring appointment |
|
| 715 | - // 3 = exception of recurring appointment |
|
| 716 | - $message->meetingrequest->instancetype = 0; |
|
| 717 | - if (isset($props[$meetingrequestproperties["isrecurringtag"]]) && $props[$meetingrequestproperties["isrecurringtag"]] == 1) { |
|
| 718 | - $message->meetingrequest->instancetype = 1; |
|
| 719 | - } |
|
| 720 | - elseif ((!isset($props[$meetingrequestproperties["isrecurringtag"]]) || $props[$meetingrequestproperties["isrecurringtag"]] == 0) && isset($message->meetingrequest->recurrenceid)) { |
|
| 721 | - if (isset($props[$meetingrequestproperties["appSeqNr"]]) && $props[$meetingrequestproperties["appSeqNr"]] == 0) { |
|
| 722 | - $message->meetingrequest->instancetype = 2; |
|
| 723 | - } |
|
| 724 | - else { |
|
| 725 | - $message->meetingrequest->instancetype = 3; |
|
| 726 | - } |
|
| 727 | - } |
|
| 728 | - |
|
| 729 | - // Disable reminder if it is off |
|
| 730 | - if (!isset($props[$meetingrequestproperties["reminderset"]]) || $props[$meetingrequestproperties["reminderset"]] == false) { |
|
| 731 | - $message->meetingrequest->reminder = ""; |
|
| 732 | - } |
|
| 733 | - // the property saves reminder in minutes, but we need it in secs |
|
| 734 | - else { |
|
| 735 | - // /set the default reminder time to seconds |
|
| 736 | - if ($props[$meetingrequestproperties["remindertime"]] == 0x5AE980E1) { |
|
| 737 | - $message->meetingrequest->reminder = 900; |
|
| 738 | - } |
|
| 739 | - else { |
|
| 740 | - $message->meetingrequest->reminder = $props[$meetingrequestproperties["remindertime"]] * 60; |
|
| 741 | - } |
|
| 742 | - } |
|
| 743 | - |
|
| 744 | - // Set sensitivity to 0 if missing |
|
| 745 | - if (!isset($message->meetingrequest->sensitivity)) { |
|
| 746 | - $message->meetingrequest->sensitivity = 0; |
|
| 747 | - } |
|
| 748 | - |
|
| 749 | - // If the user is working from a location other than the office the busystatus should be interpreted as free. |
|
| 750 | - if (isset($message->meetingrequest->busystatus) && $message->meetingrequest->busystatus == fbWorkingElsewhere) { |
|
| 751 | - $message->meetingrequest->busystatus = fbFree; |
|
| 752 | - } |
|
| 753 | - |
|
| 754 | - // If the busystatus has the value of -1, we should be interpreted as tentative (1) / ZP-581 |
|
| 755 | - if (isset($message->meetingrequest->busystatus) && $message->meetingrequest->busystatus == -1) { |
|
| 756 | - $message->meetingrequest->busystatus = fbTentative; |
|
| 757 | - } |
|
| 758 | - |
|
| 759 | - // if a meeting request response hasn't been processed yet, |
|
| 760 | - // do it so that the attendee status is updated on the mobile |
|
| 761 | - if (!isset($messageprops[$emailproperties["processed"]])) { |
|
| 762 | - // check if we are not sending the MR so we can process it - ZP-581 |
|
| 763 | - $cuser = GSync::GetBackend()->GetUserDetails(GSync::GetBackend()->GetCurrentUsername()); |
|
| 764 | - if (isset($cuser["emailaddress"]) && $cuser["emailaddress"] != $fromaddr) { |
|
| 765 | - if (!isset($req)) { |
|
| 766 | - $req = new Meetingrequest($this->store, $mapimessage, $this->session); |
|
| 767 | - } |
|
| 768 | - if ($req->isMeetingRequestResponse()) { |
|
| 769 | - $req->processMeetingRequestResponse(); |
|
| 770 | - } |
|
| 771 | - if ($req->isMeetingCancellation()) { |
|
| 772 | - $req->processMeetingCancellation(); |
|
| 773 | - } |
|
| 774 | - } |
|
| 775 | - } |
|
| 776 | - $message->contentclass = DEFAULT_CALENDAR_CONTENTCLASS; |
|
| 777 | - |
|
| 778 | - // MeetingMessageType values |
|
| 779 | - // 0 = A silent update was performed, or the message type is unspecified. |
|
| 780 | - // 1 = Initial meeting request. |
|
| 781 | - // 2 = Full update. |
|
| 782 | - // 3 = Informational update. |
|
| 783 | - // 4 = Outdated. A newer meeting request or meeting update was received after this message. |
|
| 784 | - // 5 = Identifies the delegator's copy of the meeting request. |
|
| 785 | - // 6 = Identifies that the meeting request has been delegated and the meeting request cannot be responded to. |
|
| 786 | - $message->meetingrequest->meetingmessagetype = mtgEmpty; |
|
| 787 | - |
|
| 788 | - if (isset($props[$meetingrequestproperties["meetingType"]])) { |
|
| 789 | - switch ($props[$meetingrequestproperties["meetingType"]]) { |
|
| 790 | - case mtgRequest: |
|
| 791 | - $message->meetingrequest->meetingmessagetype = 1; |
|
| 792 | - break; |
|
| 793 | - |
|
| 794 | - case mtgFull: |
|
| 795 | - $message->meetingrequest->meetingmessagetype = 2; |
|
| 796 | - break; |
|
| 797 | - |
|
| 798 | - case mtgInfo: |
|
| 799 | - $message->meetingrequest->meetingmessagetype = 3; |
|
| 800 | - break; |
|
| 801 | - |
|
| 802 | - case mtgOutOfDate: |
|
| 803 | - $message->meetingrequest->meetingmessagetype = 4; |
|
| 804 | - break; |
|
| 805 | - |
|
| 806 | - case mtgDelegatorCopy: |
|
| 807 | - $message->meetingrequest->meetingmessagetype = 5; |
|
| 808 | - break; |
|
| 809 | - } |
|
| 810 | - } |
|
| 811 | - } |
|
| 812 | - |
|
| 813 | - // Add attachments |
|
| 814 | - $attachtable = mapi_message_getattachmenttable($mapimessage); |
|
| 815 | - $rows = mapi_table_queryallrows($attachtable, [PR_ATTACH_NUM]); |
|
| 816 | - $entryid = bin2hex($messageprops[$emailproperties["entryid"]]); |
|
| 817 | - $parentSourcekey = bin2hex($messageprops[$emailproperties["parentsourcekey"]]); |
|
| 818 | - |
|
| 819 | - foreach ($rows as $row) { |
|
| 820 | - if (isset($row[PR_ATTACH_NUM])) { |
|
| 821 | - if (Request::GetProtocolVersion() >= 12.0) { |
|
| 822 | - $attach = new SyncBaseAttachment(); |
|
| 823 | - } |
|
| 824 | - else { |
|
| 825 | - $attach = new SyncAttachment(); |
|
| 826 | - } |
|
| 827 | - |
|
| 828 | - $mapiattach = mapi_message_openattach($mapimessage, $row[PR_ATTACH_NUM]); |
|
| 829 | - $attachprops = mapi_getprops($mapiattach, [PR_ATTACH_LONG_FILENAME, PR_ATTACH_FILENAME, PR_ATTACHMENT_HIDDEN, PR_ATTACH_CONTENT_ID, PR_ATTACH_CONTENT_ID_W, PR_ATTACH_MIME_TAG, PR_ATTACH_MIME_TAG_W, PR_ATTACH_METHOD, PR_DISPLAY_NAME, PR_DISPLAY_NAME_W, PR_ATTACH_SIZE, PR_ATTACH_FLAGS]); |
|
| 830 | - if ((isset($attachprops[PR_ATTACH_MIME_TAG]) && strpos(strtolower($attachprops[PR_ATTACH_MIME_TAG]), 'signed') !== false) || |
|
| 831 | - (isset($attachprops[PR_ATTACH_MIME_TAG_W]) && strpos(strtolower($attachprops[PR_ATTACH_MIME_TAG_W]), 'signed') !== false)) { |
|
| 832 | - continue; |
|
| 833 | - } |
|
| 834 | - |
|
| 835 | - // the displayname is handled equally for all AS versions |
|
| 836 | - $attach->displayname = w2u((isset($attachprops[PR_ATTACH_LONG_FILENAME])) ? $attachprops[PR_ATTACH_LONG_FILENAME] : ((isset($attachprops[PR_ATTACH_FILENAME])) ? $attachprops[PR_ATTACH_FILENAME] : ((isset($attachprops[PR_DISPLAY_NAME])) ? $attachprops[PR_DISPLAY_NAME] : "attachment.bin"))); |
|
| 837 | - // fix attachment name in case of inline images |
|
| 838 | - if (($attach->displayname == "inline.txt" && (isset($attachprops[PR_ATTACH_MIME_TAG]) || $attachprops[PR_ATTACH_MIME_TAG_W])) || |
|
| 839 | - (substr_compare($attach->displayname, "attachment", 0, 10, true) === 0 && substr_compare($attach->displayname, ".dat", -4, 4, true) === 0)) { |
|
| 840 | - $mimetype = (isset($attachprops[PR_ATTACH_MIME_TAG])) ? $attachprops[PR_ATTACH_MIME_TAG] : $attachprops[PR_ATTACH_MIME_TAG_W]; |
|
| 841 | - $mime = explode("/", $mimetype); |
|
| 842 | - |
|
| 843 | - if (count($mime) == 2 && $mime[0] == "image") { |
|
| 844 | - $attach->displayname = "inline." . $mime[1]; |
|
| 845 | - } |
|
| 846 | - } |
|
| 847 | - |
|
| 848 | - // set AS version specific parameters |
|
| 849 | - if (Request::GetProtocolVersion() >= 12.0) { |
|
| 850 | - $attach->filereference = sprintf("%s:%s:%s", $entryid, $row[PR_ATTACH_NUM], $parentSourcekey); |
|
| 851 | - $attach->method = (isset($attachprops[PR_ATTACH_METHOD])) ? $attachprops[PR_ATTACH_METHOD] : ATTACH_BY_VALUE; |
|
| 852 | - |
|
| 853 | - // if displayname does not have the eml extension for embedde messages, android and WP devices won't open it |
|
| 854 | - if ($attach->method == ATTACH_EMBEDDED_MSG) { |
|
| 855 | - if (strtolower(substr($attach->displayname, -4)) != '.eml') { |
|
| 856 | - $attach->displayname .= '.eml'; |
|
| 857 | - } |
|
| 858 | - } |
|
| 859 | - // android devices require attachment size in order to display an attachment properly |
|
| 860 | - if (!isset($attachprops[PR_ATTACH_SIZE])) { |
|
| 861 | - $stream = mapi_openproperty($mapiattach, PR_ATTACH_DATA_BIN, IID_IStream, 0, 0); |
|
| 862 | - // It's not possible to open some (embedded only?) messages, so we need to open the attachment object itself to get the data |
|
| 863 | - if (mapi_last_hresult()) { |
|
| 864 | - $embMessage = mapi_attach_openobj($mapiattach); |
|
| 865 | - $addrbook = $this->getAddressbook(); |
|
| 866 | - $stream = mapi_inetmapi_imtoinet($this->session, $addrbook, $embMessage, ['use_tnef' => -1]); |
|
| 867 | - } |
|
| 868 | - $stat = mapi_stream_stat($stream); |
|
| 869 | - $attach->estimatedDataSize = $stat['cb']; |
|
| 870 | - } |
|
| 871 | - else { |
|
| 872 | - $attach->estimatedDataSize = $attachprops[PR_ATTACH_SIZE]; |
|
| 873 | - } |
|
| 874 | - |
|
| 875 | - if (isset($attachprops[PR_ATTACH_CONTENT_ID]) && $attachprops[PR_ATTACH_CONTENT_ID]) { |
|
| 876 | - $attach->contentid = $attachprops[PR_ATTACH_CONTENT_ID]; |
|
| 877 | - } |
|
| 878 | - |
|
| 879 | - if (!isset($attach->contentid) && isset($attachprops[PR_ATTACH_CONTENT_ID_W]) && $attachprops[PR_ATTACH_CONTENT_ID_W]) { |
|
| 880 | - $attach->contentid = $attachprops[PR_ATTACH_CONTENT_ID_W]; |
|
| 881 | - } |
|
| 882 | - |
|
| 883 | - if (isset($attachprops[PR_ATTACHMENT_HIDDEN]) && $attachprops[PR_ATTACHMENT_HIDDEN]) { |
|
| 884 | - $attach->isinline = 1; |
|
| 885 | - } |
|
| 886 | - |
|
| 887 | - if (isset($attach->contentid, $attachprops[PR_ATTACH_FLAGS]) && $attachprops[PR_ATTACH_FLAGS] & 4) { |
|
| 888 | - $attach->isinline = 1; |
|
| 889 | - } |
|
| 890 | - |
|
| 891 | - if (!isset($message->asattachments)) { |
|
| 892 | - $message->asattachments = []; |
|
| 893 | - } |
|
| 894 | - |
|
| 895 | - array_push($message->asattachments, $attach); |
|
| 896 | - } |
|
| 897 | - else { |
|
| 898 | - $attach->attsize = $attachprops[PR_ATTACH_SIZE]; |
|
| 899 | - $attach->attname = sprintf("%s:%s:%s", $entryid, $row[PR_ATTACH_NUM], $parentSourcekey); |
|
| 900 | - if (!isset($message->attachments)) { |
|
| 901 | - $message->attachments = []; |
|
| 902 | - } |
|
| 903 | - |
|
| 904 | - array_push($message->attachments, $attach); |
|
| 905 | - } |
|
| 906 | - } |
|
| 907 | - } |
|
| 908 | - |
|
| 909 | - // Get To/Cc as SMTP addresses (this is different from displayto and displaycc because we are putting |
|
| 910 | - // in the SMTP addresses as well, while displayto and displaycc could just contain the display names |
|
| 911 | - $message->to = []; |
|
| 912 | - $message->cc = []; |
|
| 913 | - |
|
| 914 | - $reciptable = mapi_message_getrecipienttable($mapimessage); |
|
| 915 | - $rows = mapi_table_queryallrows($reciptable, [PR_RECIPIENT_TYPE, PR_DISPLAY_NAME, PR_ADDRTYPE, PR_EMAIL_ADDRESS, PR_SMTP_ADDRESS, PR_ENTRYID, PR_SEARCH_KEY]); |
|
| 916 | - |
|
| 917 | - foreach ($rows as $row) { |
|
| 918 | - $address = ""; |
|
| 919 | - $fulladdr = ""; |
|
| 920 | - |
|
| 921 | - $addrtype = isset($row[PR_ADDRTYPE]) ? $row[PR_ADDRTYPE] : ""; |
|
| 922 | - |
|
| 923 | - if (isset($row[PR_SMTP_ADDRESS])) { |
|
| 924 | - $address = $row[PR_SMTP_ADDRESS]; |
|
| 925 | - } |
|
| 926 | - elseif ($addrtype == "SMTP" && isset($row[PR_EMAIL_ADDRESS])) { |
|
| 927 | - $address = $row[PR_EMAIL_ADDRESS]; |
|
| 928 | - } |
|
| 929 | - elseif ($addrtype == "ZARAFA" && isset($row[PR_ENTRYID])) { |
|
| 930 | - $address = $this->getSMTPAddressFromEntryID($row[PR_ENTRYID]); |
|
| 931 | - } |
|
| 932 | - |
|
| 933 | - // if the user was not found, do a fallback to PR_SEARCH_KEY |
|
| 934 | - // @see https://jira.z-hub.io/browse/ZP-1178 |
|
| 935 | - if (empty($address) && isset($row[PR_SEARCH_KEY])) { |
|
| 936 | - $address = $this->getEmailAddressFromSearchKey($row[PR_SEARCH_KEY]); |
|
| 937 | - } |
|
| 938 | - |
|
| 939 | - $name = isset($row[PR_DISPLAY_NAME]) ? $row[PR_DISPLAY_NAME] : ""; |
|
| 940 | - |
|
| 941 | - if ($name == "" || $name == $address) { |
|
| 942 | - $fulladdr = w2u($address); |
|
| 943 | - } |
|
| 944 | - else { |
|
| 945 | - if (substr($name, 0, 1) != '"' && substr($name, -1) != '"') { |
|
| 946 | - $fulladdr = "\"" . w2u($name) . "\" <" . w2u($address) . ">"; |
|
| 947 | - } |
|
| 948 | - else { |
|
| 949 | - $fulladdr = w2u($name) . "<" . w2u($address) . ">"; |
|
| 950 | - } |
|
| 951 | - } |
|
| 952 | - |
|
| 953 | - if ($row[PR_RECIPIENT_TYPE] == MAPI_TO) { |
|
| 954 | - array_push($message->to, $fulladdr); |
|
| 955 | - } |
|
| 956 | - elseif ($row[PR_RECIPIENT_TYPE] == MAPI_CC) { |
|
| 957 | - array_push($message->cc, $fulladdr); |
|
| 958 | - } |
|
| 959 | - } |
|
| 960 | - |
|
| 961 | - if (is_array($message->to) && !empty($message->to)) { |
|
| 962 | - $message->to = implode(", ", $message->to); |
|
| 963 | - } |
|
| 964 | - if (is_array($message->cc) && !empty($message->cc)) { |
|
| 965 | - $message->cc = implode(", ", $message->cc); |
|
| 966 | - } |
|
| 967 | - |
|
| 968 | - // without importance some mobiles assume "0" (low) - Mantis #439 |
|
| 969 | - if (!isset($message->importance)) { |
|
| 970 | - $message->importance = IMPORTANCE_NORMAL; |
|
| 971 | - } |
|
| 972 | - |
|
| 973 | - if (!isset($message->internetcpid)) { |
|
| 974 | - $message->internetcpid = (defined('STORE_INTERNET_CPID')) ? constant('STORE_INTERNET_CPID') : INTERNET_CPID_WINDOWS1252; |
|
| 975 | - } |
|
| 976 | - $this->setFlag($mapimessage, $message); |
|
| 977 | - // TODO checkcontentclass |
|
| 978 | - if (!isset($message->contentclass)) { |
|
| 979 | - $message->contentclass = DEFAULT_EMAIL_CONTENTCLASS; |
|
| 980 | - } |
|
| 981 | - |
|
| 982 | - if (!isset($message->nativebodytype)) { |
|
| 983 | - $message->nativebodytype = MAPIUtils::GetNativeBodyType($messageprops); |
|
| 984 | - } |
|
| 985 | - elseif ($message->nativebodytype == SYNC_BODYPREFERENCE_UNDEFINED) { |
|
| 986 | - $nbt = MAPIUtils::GetNativeBodyType($messageprops); |
|
| 987 | - SLog::Write(LOGLEVEL_INFO, sprintf("MAPIProvider->getEmail(): native body type is undefined. Set it to %d.", $nbt)); |
|
| 988 | - $message->nativebodytype = $nbt; |
|
| 989 | - } |
|
| 990 | - |
|
| 991 | - // reply, reply to all, forward flags |
|
| 992 | - if (isset($message->lastverbexecuted) && $message->lastverbexecuted) { |
|
| 993 | - $message->lastverbexecuted = Utils::GetLastVerbExecuted($message->lastverbexecuted); |
|
| 994 | - } |
|
| 995 | - |
|
| 996 | - return $message; |
|
| 997 | - } |
|
| 998 | - |
|
| 999 | - /** |
|
| 1000 | - * Reads a note object from MAPI. |
|
| 1001 | - * |
|
| 1002 | - * @param mixed $mapimessage |
|
| 1003 | - * @param ContentParameters $contentparameters |
|
| 1004 | - * |
|
| 1005 | - * @return SyncNote |
|
| 1006 | - */ |
|
| 1007 | - private function getNote($mapimessage, $contentparameters) { |
|
| 1008 | - $message = new SyncNote(); |
|
| 1009 | - |
|
| 1010 | - // Standard one-to-one mappings first |
|
| 1011 | - $this->getPropsFromMAPI($message, $mapimessage, MAPIMapping::GetNoteMapping()); |
|
| 1012 | - |
|
| 1013 | - // set the body according to contentparameters and supported AS version |
|
| 1014 | - $this->setMessageBody($mapimessage, $contentparameters, $message); |
|
| 1015 | - |
|
| 1016 | - return $message; |
|
| 1017 | - } |
|
| 1018 | - |
|
| 1019 | - /** |
|
| 1020 | - * Creates a SyncFolder from MAPI properties. |
|
| 1021 | - * |
|
| 1022 | - * @param mixed $folderprops |
|
| 1023 | - * |
|
| 1024 | - * @return SyncFolder |
|
| 1025 | - */ |
|
| 1026 | - public function GetFolder($folderprops) { |
|
| 1027 | - $folder = new SyncFolder(); |
|
| 1028 | - |
|
| 1029 | - $storeprops = $this->GetStoreProps(); |
|
| 1030 | - |
|
| 1031 | - // For ZCP 7.0.x we need to retrieve more properties explicitly, see ZP-780 |
|
| 1032 | - if (isset($folderprops[PR_SOURCE_KEY]) && !isset($folderprops[PR_ENTRYID]) && !isset($folderprops[PR_CONTAINER_CLASS])) { |
|
| 1033 | - $entryid = mapi_msgstore_entryidfromsourcekey($this->store, $folderprops[PR_SOURCE_KEY]); |
|
| 1034 | - $mapifolder = mapi_msgstore_openentry($this->store, $entryid); |
|
| 1035 | - $folderprops = mapi_getprops($mapifolder, [PR_DISPLAY_NAME, PR_PARENT_ENTRYID, PR_ENTRYID, PR_SOURCE_KEY, PR_PARENT_SOURCE_KEY, PR_CONTAINER_CLASS, PR_ATTR_HIDDEN, PR_EXTENDED_FOLDER_FLAGS]); |
|
| 1036 | - SLog::Write(LOGLEVEL_DEBUG, "MAPIProvider->GetFolder(): received insufficient of data from ICS. Fetching required data."); |
|
| 1037 | - } |
|
| 1038 | - |
|
| 1039 | - if (!isset( |
|
| 1040 | - $folderprops[PR_DISPLAY_NAME], |
|
| 1041 | - $folderprops[PR_PARENT_ENTRYID], |
|
| 1042 | - $folderprops[PR_SOURCE_KEY], |
|
| 1043 | - $folderprops[PR_ENTRYID], |
|
| 1044 | - $folderprops[PR_PARENT_SOURCE_KEY], |
|
| 1045 | - $storeprops[PR_IPM_SUBTREE_ENTRYID])) { |
|
| 1046 | - SLog::Write(LOGLEVEL_ERROR, "MAPIProvider->GetFolder(): invalid folder. Missing properties"); |
|
| 1047 | - |
|
| 1048 | - return false; |
|
| 1049 | - } |
|
| 1050 | - |
|
| 1051 | - // ignore hidden folders |
|
| 1052 | - if (isset($folderprops[PR_ATTR_HIDDEN]) && $folderprops[PR_ATTR_HIDDEN] != false) { |
|
| 1053 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->GetFolder(): invalid folder '%s' as it is a hidden folder (PR_ATTR_HIDDEN)", $folderprops[PR_DISPLAY_NAME])); |
|
| 1054 | - |
|
| 1055 | - return false; |
|
| 1056 | - } |
|
| 1057 | - |
|
| 1058 | - // ignore certain undesired folders, like "RSS Feeds" and "Suggested contacts" |
|
| 1059 | - if ((isset($folderprops[PR_CONTAINER_CLASS]) && $folderprops[PR_CONTAINER_CLASS] == "IPF.Note.OutlookHomepage") || |
|
| 1060 | - in_array($folderprops[PR_ENTRYID], $this->getSpecialFoldersData())) { |
|
| 1061 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->GetFolder(): folder '%s' should not be synchronized", $folderprops[PR_DISPLAY_NAME])); |
|
| 1062 | - |
|
| 1063 | - return false; |
|
| 1064 | - } |
|
| 1065 | - |
|
| 1066 | - $folder->BackendId = bin2hex($folderprops[PR_SOURCE_KEY]); |
|
| 1067 | - $folderOrigin = DeviceManager::FLD_ORIGIN_USER; |
|
| 1068 | - if (GSync::GetBackend()->GetImpersonatedUser()) { |
|
| 1069 | - $folderOrigin = DeviceManager::FLD_ORIGIN_IMPERSONATED; |
|
| 1070 | - } |
|
| 1071 | - $folder->serverid = GSync::GetDeviceManager()->GetFolderIdForBackendId($folder->BackendId, true, $folderOrigin, $folderprops[PR_DISPLAY_NAME]); |
|
| 1072 | - if ($folderprops[PR_PARENT_ENTRYID] == $storeprops[PR_IPM_SUBTREE_ENTRYID] || $folderprops[PR_PARENT_ENTRYID] == $storeprops[PR_IPM_PUBLIC_FOLDERS_ENTRYID]) { |
|
| 1073 | - $folder->parentid = "0"; |
|
| 1074 | - } |
|
| 1075 | - else { |
|
| 1076 | - $folder->parentid = GSync::GetDeviceManager()->GetFolderIdForBackendId(bin2hex($folderprops[PR_PARENT_SOURCE_KEY])); |
|
| 1077 | - } |
|
| 1078 | - $folder->displayname = w2u($folderprops[PR_DISPLAY_NAME]); |
|
| 1079 | - $folder->type = $this->GetFolderType($folderprops[PR_ENTRYID], isset($folderprops[PR_CONTAINER_CLASS]) ? $folderprops[PR_CONTAINER_CLASS] : false); |
|
| 1080 | - |
|
| 1081 | - return $folder; |
|
| 1082 | - } |
|
| 1083 | - |
|
| 1084 | - /** |
|
| 1085 | - * Returns the foldertype for an entryid |
|
| 1086 | - * Gets the folder type by checking the default folders in MAPI. |
|
| 1087 | - * |
|
| 1088 | - * @param string $entryid |
|
| 1089 | - * @param string $class (opt) |
|
| 1090 | - * |
|
| 1091 | - * @return long |
|
| 1092 | - */ |
|
| 1093 | - public function GetFolderType($entryid, $class = false) { |
|
| 1094 | - $storeprops = $this->GetStoreProps(); |
|
| 1095 | - $inboxprops = $this->GetInboxProps(); |
|
| 1096 | - |
|
| 1097 | - if ($entryid == $storeprops[PR_IPM_WASTEBASKET_ENTRYID]) { |
|
| 1098 | - return SYNC_FOLDER_TYPE_WASTEBASKET; |
|
| 1099 | - } |
|
| 1100 | - if ($entryid == $storeprops[PR_IPM_SENTMAIL_ENTRYID]) { |
|
| 1101 | - return SYNC_FOLDER_TYPE_SENTMAIL; |
|
| 1102 | - } |
|
| 1103 | - if ($entryid == $storeprops[PR_IPM_OUTBOX_ENTRYID]) { |
|
| 1104 | - return SYNC_FOLDER_TYPE_OUTBOX; |
|
| 1105 | - } |
|
| 1106 | - |
|
| 1107 | - // Public folders do not have inboxprops |
|
| 1108 | - // @see https://jira.z-hub.io/browse/ZP-995 |
|
| 1109 | - if (!empty($inboxprops)) { |
|
| 1110 | - if ($entryid == $inboxprops[PR_ENTRYID]) { |
|
| 1111 | - return SYNC_FOLDER_TYPE_INBOX; |
|
| 1112 | - } |
|
| 1113 | - if ($entryid == $inboxprops[PR_IPM_DRAFTS_ENTRYID]) { |
|
| 1114 | - return SYNC_FOLDER_TYPE_DRAFTS; |
|
| 1115 | - } |
|
| 1116 | - if ($entryid == $inboxprops[PR_IPM_TASK_ENTRYID]) { |
|
| 1117 | - return SYNC_FOLDER_TYPE_TASK; |
|
| 1118 | - } |
|
| 1119 | - if ($entryid == $inboxprops[PR_IPM_APPOINTMENT_ENTRYID]) { |
|
| 1120 | - return SYNC_FOLDER_TYPE_APPOINTMENT; |
|
| 1121 | - } |
|
| 1122 | - if ($entryid == $inboxprops[PR_IPM_CONTACT_ENTRYID]) { |
|
| 1123 | - return SYNC_FOLDER_TYPE_CONTACT; |
|
| 1124 | - } |
|
| 1125 | - if ($entryid == $inboxprops[PR_IPM_NOTE_ENTRYID]) { |
|
| 1126 | - return SYNC_FOLDER_TYPE_NOTE; |
|
| 1127 | - } |
|
| 1128 | - if ($entryid == $inboxprops[PR_IPM_JOURNAL_ENTRYID]) { |
|
| 1129 | - return SYNC_FOLDER_TYPE_JOURNAL; |
|
| 1130 | - } |
|
| 1131 | - } |
|
| 1132 | - |
|
| 1133 | - // user created folders |
|
| 1134 | - if ($class == "IPF.Note") { |
|
| 1135 | - return SYNC_FOLDER_TYPE_USER_MAIL; |
|
| 1136 | - } |
|
| 1137 | - if ($class == "IPF.Task") { |
|
| 1138 | - return SYNC_FOLDER_TYPE_USER_TASK; |
|
| 1139 | - } |
|
| 1140 | - if ($class == "IPF.Appointment") { |
|
| 1141 | - return SYNC_FOLDER_TYPE_USER_APPOINTMENT; |
|
| 1142 | - } |
|
| 1143 | - if ($class == "IPF.Contact") { |
|
| 1144 | - return SYNC_FOLDER_TYPE_USER_CONTACT; |
|
| 1145 | - } |
|
| 1146 | - if ($class == "IPF.StickyNote") { |
|
| 1147 | - return SYNC_FOLDER_TYPE_USER_NOTE; |
|
| 1148 | - } |
|
| 1149 | - if ($class == "IPF.Journal") { |
|
| 1150 | - return SYNC_FOLDER_TYPE_USER_JOURNAL; |
|
| 1151 | - } |
|
| 1152 | - |
|
| 1153 | - return SYNC_FOLDER_TYPE_OTHER; |
|
| 1154 | - } |
|
| 1155 | - |
|
| 1156 | - /** |
|
| 1157 | - * Indicates if the entry id is a default MAPI folder. |
|
| 1158 | - * |
|
| 1159 | - * @param string $entryid |
|
| 1160 | - * |
|
| 1161 | - * @return bool |
|
| 1162 | - */ |
|
| 1163 | - public function IsMAPIDefaultFolder($entryid) { |
|
| 1164 | - $msgstore_props = mapi_getprops($this->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_MAILBOX_OWNER_ENTRYID]); |
|
| 1165 | - |
|
| 1166 | - $inboxProps = []; |
|
| 1167 | - $inbox = mapi_msgstore_getreceivefolder($this->store); |
|
| 1168 | - if (!mapi_last_hresult()) { |
|
| 1169 | - $inboxProps = mapi_getprops($inbox, [PR_ENTRYID]); |
|
| 1170 | - } |
|
| 1171 | - |
|
| 1172 | - $root = mapi_msgstore_openentry($this->store, null); // TODO use getRootProps() |
|
| 1173 | - $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]); |
|
| 1174 | - |
|
| 1175 | - $additional_ren_entryids = []; |
|
| 1176 | - if (isset($rootProps[PR_ADDITIONAL_REN_ENTRYIDS])) { |
|
| 1177 | - $additional_ren_entryids = $rootProps[PR_ADDITIONAL_REN_ENTRYIDS]; |
|
| 1178 | - } |
|
| 1179 | - |
|
| 1180 | - $defaultfolders = [ |
|
| 1181 | - "inbox" => ["inbox" => PR_ENTRYID], |
|
| 1182 | - "outbox" => ["store" => PR_IPM_OUTBOX_ENTRYID], |
|
| 1183 | - "sent" => ["store" => PR_IPM_SENTMAIL_ENTRYID], |
|
| 1184 | - "wastebasket" => ["store" => PR_IPM_WASTEBASKET_ENTRYID], |
|
| 1185 | - "favorites" => ["store" => PR_IPM_FAVORITES_ENTRYID], |
|
| 1186 | - "publicfolders" => ["store" => PR_IPM_PUBLIC_FOLDERS_ENTRYID], |
|
| 1187 | - "calendar" => ["root" => PR_IPM_APPOINTMENT_ENTRYID], |
|
| 1188 | - "contact" => ["root" => PR_IPM_CONTACT_ENTRYID], |
|
| 1189 | - "drafts" => ["root" => PR_IPM_DRAFTS_ENTRYID], |
|
| 1190 | - "journal" => ["root" => PR_IPM_JOURNAL_ENTRYID], |
|
| 1191 | - "note" => ["root" => PR_IPM_NOTE_ENTRYID], |
|
| 1192 | - "task" => ["root" => PR_IPM_TASK_ENTRYID], |
|
| 1193 | - "junk" => ["additional" => 4], |
|
| 1194 | - "syncissues" => ["additional" => 1], |
|
| 1195 | - "conflicts" => ["additional" => 0], |
|
| 1196 | - "localfailures" => ["additional" => 2], |
|
| 1197 | - "serverfailures" => ["additional" => 3], |
|
| 1198 | - ]; |
|
| 1199 | - |
|
| 1200 | - foreach ($defaultfolders as $key => $prop) { |
|
| 1201 | - $tag = reset($prop); |
|
| 1202 | - $from = key($prop); |
|
| 1203 | - |
|
| 1204 | - switch ($from) { |
|
| 1205 | - case "inbox": |
|
| 1206 | - if (isset($inboxProps[$tag]) && $entryid == $inboxProps[$tag]) { |
|
| 1207 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->IsMAPIFolder(): Inbox found, key '%s'", $key)); |
|
| 1208 | - |
|
| 1209 | - return true; |
|
| 1210 | - } |
|
| 1211 | - break; |
|
| 1212 | - |
|
| 1213 | - case "store": |
|
| 1214 | - if (isset($msgstore_props[$tag]) && $entryid == $msgstore_props[$tag]) { |
|
| 1215 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->IsMAPIFolder(): Store folder found, key '%s'", $key)); |
|
| 1216 | - |
|
| 1217 | - return true; |
|
| 1218 | - } |
|
| 1219 | - break; |
|
| 1220 | - |
|
| 1221 | - case "root": |
|
| 1222 | - if (isset($rootProps[$tag]) && $entryid == $rootProps[$tag]) { |
|
| 1223 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->IsMAPIFolder(): Root folder found, key '%s'", $key)); |
|
| 1224 | - |
|
| 1225 | - return true; |
|
| 1226 | - } |
|
| 1227 | - break; |
|
| 1228 | - |
|
| 1229 | - case "additional": |
|
| 1230 | - if (isset($additional_ren_entryids[$tag]) && $entryid == $additional_ren_entryids[$tag]) { |
|
| 1231 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->IsMAPIFolder(): Additional folder found, key '%s'", $key)); |
|
| 1232 | - |
|
| 1233 | - return true; |
|
| 1234 | - } |
|
| 1235 | - break; |
|
| 1236 | - } |
|
| 1237 | - } |
|
| 1238 | - |
|
| 1239 | - return false; |
|
| 1240 | - } |
|
| 1241 | - |
|
| 1242 | - /*---------------------------------------------------------------------------------------------------------- |
|
| 34 | + /** |
|
| 35 | + * Reads a message from MAPI |
|
| 36 | + * Depending on the message class, a contact, appointment, task or email is read. |
|
| 37 | + * |
|
| 38 | + * @param mixed $mapimessage |
|
| 39 | + * @param ContentParameters $contentparameters |
|
| 40 | + * |
|
| 41 | + * @return SyncObject |
|
| 42 | + */ |
|
| 43 | + public function GetMessage($mapimessage, $contentparameters) { |
|
| 44 | + // Gets the Sync object from a MAPI object according to its message class |
|
| 45 | + |
|
| 46 | + $props = mapi_getprops($mapimessage, [PR_MESSAGE_CLASS]); |
|
| 47 | + if (isset($props[PR_MESSAGE_CLASS])) { |
|
| 48 | + $messageclass = $props[PR_MESSAGE_CLASS]; |
|
| 49 | + } |
|
| 50 | + else { |
|
| 51 | + $messageclass = "IPM"; |
|
| 52 | + } |
|
| 53 | + |
|
| 54 | + if (strpos($messageclass, "IPM.Contact") === 0) { |
|
| 55 | + return $this->getContact($mapimessage, $contentparameters); |
|
| 56 | + } |
|
| 57 | + if (strpos($messageclass, "IPM.Appointment") === 0) { |
|
| 58 | + return $this->getAppointment($mapimessage, $contentparameters); |
|
| 59 | + } |
|
| 60 | + if (strpos($messageclass, "IPM.Task") === 0 && strpos($messageclass, "IPM.TaskRequest") === false) { |
|
| 61 | + return $this->getTask($mapimessage, $contentparameters); |
|
| 62 | + } |
|
| 63 | + if (strpos($messageclass, "IPM.StickyNote") === 0) { |
|
| 64 | + return $this->getNote($mapimessage, $contentparameters); |
|
| 65 | + } |
|
| 66 | + |
|
| 67 | + return $this->getEmail($mapimessage, $contentparameters); |
|
| 68 | + } |
|
| 69 | + |
|
| 70 | + /** |
|
| 71 | + * Reads a contact object from MAPI. |
|
| 72 | + * |
|
| 73 | + * @param mixed $mapimessage |
|
| 74 | + * @param ContentParameters $contentparameters |
|
| 75 | + * |
|
| 76 | + * @return SyncContact |
|
| 77 | + */ |
|
| 78 | + private function getContact($mapimessage, $contentparameters) { |
|
| 79 | + $message = new SyncContact(); |
|
| 80 | + |
|
| 81 | + // Standard one-to-one mappings first |
|
| 82 | + $this->getPropsFromMAPI($message, $mapimessage, MAPIMapping::GetContactMapping()); |
|
| 83 | + |
|
| 84 | + // Contact specific props |
|
| 85 | + $contactproperties = MAPIMapping::GetContactProperties(); |
|
| 86 | + $messageprops = $this->getProps($mapimessage, $contactproperties); |
|
| 87 | + |
|
| 88 | + // set the body according to contentparameters and supported AS version |
|
| 89 | + $this->setMessageBody($mapimessage, $contentparameters, $message); |
|
| 90 | + |
|
| 91 | + // check the picture |
|
| 92 | + if (isset($messageprops[$contactproperties["haspic"]]) && $messageprops[$contactproperties["haspic"]]) { |
|
| 93 | + // Add attachments |
|
| 94 | + $attachtable = mapi_message_getattachmenttable($mapimessage); |
|
| 95 | + mapi_table_restrict($attachtable, MAPIUtils::GetContactPicRestriction()); |
|
| 96 | + $rows = mapi_table_queryallrows($attachtable, [PR_ATTACH_NUM, PR_ATTACH_SIZE]); |
|
| 97 | + |
|
| 98 | + foreach ($rows as $row) { |
|
| 99 | + if (isset($row[PR_ATTACH_NUM])) { |
|
| 100 | + $mapiattach = mapi_message_openattach($mapimessage, $row[PR_ATTACH_NUM]); |
|
| 101 | + $message->picture = base64_encode(mapi_attach_openbin($mapiattach, PR_ATTACH_DATA_BIN)); |
|
| 102 | + } |
|
| 103 | + } |
|
| 104 | + } |
|
| 105 | + |
|
| 106 | + return $message; |
|
| 107 | + } |
|
| 108 | + |
|
| 109 | + /** |
|
| 110 | + * Reads a task object from MAPI. |
|
| 111 | + * |
|
| 112 | + * @param mixed $mapimessage |
|
| 113 | + * @param ContentParameters $contentparameters |
|
| 114 | + * |
|
| 115 | + * @return SyncTask |
|
| 116 | + */ |
|
| 117 | + private function getTask($mapimessage, $contentparameters) { |
|
| 118 | + $message = new SyncTask(); |
|
| 119 | + |
|
| 120 | + // Standard one-to-one mappings first |
|
| 121 | + $this->getPropsFromMAPI($message, $mapimessage, MAPIMapping::GetTaskMapping()); |
|
| 122 | + |
|
| 123 | + // Task specific props |
|
| 124 | + $taskproperties = MAPIMapping::GetTaskProperties(); |
|
| 125 | + $messageprops = $this->getProps($mapimessage, $taskproperties); |
|
| 126 | + |
|
| 127 | + // set the body according to contentparameters and supported AS version |
|
| 128 | + $this->setMessageBody($mapimessage, $contentparameters, $message); |
|
| 129 | + |
|
| 130 | + // task with deadoccur is an occurrence of a recurring task and does not need to be handled as recurring |
|
| 131 | + // webaccess does not set deadoccur for the initial recurring task |
|
| 132 | + if (isset($messageprops[$taskproperties["isrecurringtag"]]) && |
|
| 133 | + $messageprops[$taskproperties["isrecurringtag"]] && |
|
| 134 | + (!isset($messageprops[$taskproperties["deadoccur"]]) || |
|
| 135 | + (isset($messageprops[$taskproperties["deadoccur"]]) && |
|
| 136 | + !$messageprops[$taskproperties["deadoccur"]]))) { |
|
| 137 | + // Process recurrence |
|
| 138 | + $message->recurrence = new SyncTaskRecurrence(); |
|
| 139 | + $this->getRecurrence($mapimessage, $messageprops, $message, $message->recurrence, false); |
|
| 140 | + } |
|
| 141 | + |
|
| 142 | + // when set the task to complete using the WebAccess, the dateComplete property is not set correctly |
|
| 143 | + if ($message->complete == 1 && !isset($message->datecompleted)) { |
|
| 144 | + $message->datecompleted = time(); |
|
| 145 | + } |
|
| 146 | + |
|
| 147 | + // if no reminder is set, announce that to the mobile |
|
| 148 | + if (!isset($message->reminderset)) { |
|
| 149 | + $message->reminderset = 0; |
|
| 150 | + } |
|
| 151 | + |
|
| 152 | + return $message; |
|
| 153 | + } |
|
| 154 | + |
|
| 155 | + /** |
|
| 156 | + * Reads an appointment object from MAPI. |
|
| 157 | + * |
|
| 158 | + * @param mixed $mapimessage |
|
| 159 | + * @param ContentParameters $contentparameters |
|
| 160 | + * |
|
| 161 | + * @return SyncAppointment |
|
| 162 | + */ |
|
| 163 | + private function getAppointment($mapimessage, $contentparameters) { |
|
| 164 | + $message = new SyncAppointment(); |
|
| 165 | + |
|
| 166 | + // Standard one-to-one mappings first |
|
| 167 | + $this->getPropsFromMAPI($message, $mapimessage, MAPIMapping::GetAppointmentMapping()); |
|
| 168 | + |
|
| 169 | + // Appointment specific props |
|
| 170 | + $appointmentprops = MAPIMapping::GetAppointmentProperties(); |
|
| 171 | + $messageprops = $this->getProps($mapimessage, $appointmentprops); |
|
| 172 | + |
|
| 173 | + // set the body according to contentparameters and supported AS version |
|
| 174 | + $this->setMessageBody($mapimessage, $contentparameters, $message); |
|
| 175 | + |
|
| 176 | + // Set reminder time if reminderset is true |
|
| 177 | + if (isset($messageprops[$appointmentprops["reminderset"]]) && $messageprops[$appointmentprops["reminderset"]] == true) { |
|
| 178 | + if ($messageprops[$appointmentprops["remindertime"]] == 0x5AE980E1) { |
|
| 179 | + $message->reminder = 15; |
|
| 180 | + } |
|
| 181 | + else { |
|
| 182 | + $message->reminder = $messageprops[$appointmentprops["remindertime"]]; |
|
| 183 | + } |
|
| 184 | + } |
|
| 185 | + |
|
| 186 | + if (!isset($message->uid)) { |
|
| 187 | + $message->uid = bin2hex($messageprops[$appointmentprops["sourcekey"]]); |
|
| 188 | + } |
|
| 189 | + else { |
|
| 190 | + $message->uid = Utils::GetICalUidFromOLUid($message->uid); |
|
| 191 | + } |
|
| 192 | + |
|
| 193 | + // Always set organizer information because some devices do not work properly without it |
|
| 194 | + if (isset($messageprops[$appointmentprops["representingentryid"]], $messageprops[$appointmentprops["representingname"]]) |
|
| 195 | + ) { |
|
| 196 | + $message->organizeremail = w2u($this->getSMTPAddressFromEntryID($messageprops[$appointmentprops["representingentryid"]])); |
|
| 197 | + // if the email address can't be resolved, fall back to PR_SENT_REPRESENTING_SEARCH_KEY |
|
| 198 | + if ($message->organizeremail == "" && isset($messageprops[$appointmentprops["sentrepresentinsrchk"]])) { |
|
| 199 | + $message->organizeremail = $this->getEmailAddressFromSearchKey($messageprops[$appointmentprops["sentrepresentinsrchk"]]); |
|
| 200 | + } |
|
| 201 | + $message->organizername = w2u($messageprops[$appointmentprops["representingname"]]); |
|
| 202 | + } |
|
| 203 | + |
|
| 204 | + $appTz = false; // if the appointment has some timezone information saved on the server |
|
| 205 | + if (!empty($messageprops[$appointmentprops["timezonetag"]])) { |
|
| 206 | + $tz = $this->getTZFromMAPIBlob($messageprops[$appointmentprops["timezonetag"]]); |
|
| 207 | + $appTz = true; |
|
| 208 | + } |
|
| 209 | + elseif (!empty($messageprops[$appointmentprops["timezonedesc"]])) { |
|
| 210 | + // Windows uses UTC in timezone description in opposite to mstzones in TimezoneUtil which uses GMT |
|
| 211 | + $wintz = str_replace("UTC", "GMT", $messageprops[$appointmentprops["timezonedesc"]]); |
|
| 212 | + $tz = TimezoneUtil::GetFullTZFromTZName(TimezoneUtil::GetTZNameFromWinTZ($wintz)); |
|
| 213 | + $appTz = true; |
|
| 214 | + } |
|
| 215 | + else { |
|
| 216 | + // set server default timezone (correct timezone should be configured!) |
|
| 217 | + $tz = TimezoneUtil::GetFullTZ(); |
|
| 218 | + } |
|
| 219 | + |
|
| 220 | + if (isset($messageprops[$appointmentprops["isrecurring"]]) && $messageprops[$appointmentprops["isrecurring"]]) { |
|
| 221 | + // Process recurrence |
|
| 222 | + $message->recurrence = new SyncRecurrence(); |
|
| 223 | + $this->getRecurrence($mapimessage, $messageprops, $message, $message->recurrence, $tz); |
|
| 224 | + |
|
| 225 | + if (empty($message->alldayevent)) { |
|
| 226 | + $message->timezone = base64_encode(TimezoneUtil::GetSyncBlobFromTZ($tz)); |
|
| 227 | + } |
|
| 228 | + } |
|
| 229 | + |
|
| 230 | + // Do attendees |
|
| 231 | + $reciptable = mapi_message_getrecipienttable($mapimessage); |
|
| 232 | + // Only get first 256 recipients, to prevent possible load issues. |
|
| 233 | + $rows = mapi_table_queryrows($reciptable, [PR_DISPLAY_NAME, PR_EMAIL_ADDRESS, PR_SMTP_ADDRESS, PR_ADDRTYPE, PR_RECIPIENT_TRACKSTATUS, PR_RECIPIENT_TYPE, PR_SEARCH_KEY], 0, 256); |
|
| 234 | + |
|
| 235 | + // Exception: we do not synchronize appointments with more than 250 attendees |
|
| 236 | + if (count($rows) > 250) { |
|
| 237 | + $message->id = bin2hex($messageprops[$appointmentprops["sourcekey"]]); |
|
| 238 | + $mbe = new SyncObjectBrokenException("Appointment has too many attendees"); |
|
| 239 | + $mbe->SetSyncObject($message); |
|
| 240 | + |
|
| 241 | + throw $mbe; |
|
| 242 | + } |
|
| 243 | + |
|
| 244 | + if (count($rows) > 0) { |
|
| 245 | + $message->attendees = []; |
|
| 246 | + } |
|
| 247 | + |
|
| 248 | + foreach ($rows as $row) { |
|
| 249 | + $attendee = new SyncAttendee(); |
|
| 250 | + |
|
| 251 | + $attendee->name = w2u($row[PR_DISPLAY_NAME]); |
|
| 252 | + // smtp address is always a proper email address |
|
| 253 | + if (isset($row[PR_SMTP_ADDRESS])) { |
|
| 254 | + $attendee->email = w2u($row[PR_SMTP_ADDRESS]); |
|
| 255 | + } |
|
| 256 | + elseif (isset($row[PR_ADDRTYPE], $row[PR_EMAIL_ADDRESS])) { |
|
| 257 | + // if address type is SMTP, it's also a proper email address |
|
| 258 | + if ($row[PR_ADDRTYPE] == "SMTP") { |
|
| 259 | + $attendee->email = w2u($row[PR_EMAIL_ADDRESS]); |
|
| 260 | + } |
|
| 261 | + // if address type is ZARAFA, the PR_EMAIL_ADDRESS contains username |
|
| 262 | + elseif ($row[PR_ADDRTYPE] == "ZARAFA") { |
|
| 263 | + $userinfo = @nsp_getuserinfo($row[PR_EMAIL_ADDRESS]); |
|
| 264 | + if (is_array($userinfo) && isset($userinfo["primary_email"])) { |
|
| 265 | + $attendee->email = w2u($userinfo["primary_email"]); |
|
| 266 | + } |
|
| 267 | + // if the user was not found, do a fallback to PR_SEARCH_KEY |
|
| 268 | + // @see https://jira.z-hub.io/browse/ZP-1178 |
|
| 269 | + elseif (isset($row[PR_SEARCH_KEY])) { |
|
| 270 | + $attendee->email = w2u($this->getEmailAddressFromSearchKey($row[PR_SEARCH_KEY])); |
|
| 271 | + } |
|
| 272 | + else { |
|
| 273 | + SLog::Write(LOGLEVEL_WARN, sprintf("MAPIProvider->getAppointment: The attendee '%s' of type ZARAFA can not be resolved. Code: 0x%X", $row[PR_EMAIL_ADDRESS], mapi_last_hresult())); |
|
| 274 | + } |
|
| 275 | + } |
|
| 276 | + } |
|
| 277 | + |
|
| 278 | + // set attendee's status and type if they're available and if we are the organizer |
|
| 279 | + $storeprops = $this->GetStoreProps(); |
|
| 280 | + if (isset($row[PR_RECIPIENT_TRACKSTATUS], $messageprops[$appointmentprops["representingentryid"]], $storeprops[PR_MAILBOX_OWNER_ENTRYID]) && |
|
| 281 | + $messageprops[$appointmentprops["representingentryid"]] == $storeprops[PR_MAILBOX_OWNER_ENTRYID]) { |
|
| 282 | + $attendee->attendeestatus = $row[PR_RECIPIENT_TRACKSTATUS]; |
|
| 283 | + } |
|
| 284 | + if (isset($row[PR_RECIPIENT_TYPE])) { |
|
| 285 | + $attendee->attendeetype = $row[PR_RECIPIENT_TYPE]; |
|
| 286 | + } |
|
| 287 | + // Some attendees have no email or name (eg resources), and if you |
|
| 288 | + // don't send one of those fields, the phone will give an error ... so |
|
| 289 | + // we don't send it in that case. |
|
| 290 | + // also ignore the "attendee" if the email is equal to the organizers' email |
|
| 291 | + if (isset($attendee->name, $attendee->email) && $attendee->email != "" && (!isset($message->organizeremail) || (isset($message->organizeremail) && $attendee->email != $message->organizeremail))) { |
|
| 292 | + array_push($message->attendees, $attendee); |
|
| 293 | + } |
|
| 294 | + } |
|
| 295 | + |
|
| 296 | + // Status 0 = no meeting, status 1 = organizer, status 2/3/4/5 = tentative/accepted/declined/notresponded |
|
| 297 | + if (isset($messageprops[$appointmentprops["meetingstatus"]]) && $messageprops[$appointmentprops["meetingstatus"]] > 1) { |
|
| 298 | + if (!isset($message->attendees) || !is_array($message->attendees)) { |
|
| 299 | + $message->attendees = []; |
|
| 300 | + } |
|
| 301 | + // Work around iOS6 cancellation issue when there are no attendees for this meeting. Just add ourselves as the sole attendee. |
|
| 302 | + if (count($message->attendees) == 0) { |
|
| 303 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->getAppointment: adding ourself as an attendee for iOS6 workaround")); |
|
| 304 | + $attendee = new SyncAttendee(); |
|
| 305 | + |
|
| 306 | + $meinfo = nsp_getuserinfo(Request::GetUser()); |
|
| 307 | + |
|
| 308 | + if (is_array($meinfo)) { |
|
| 309 | + $attendee->email = w2u($meinfo["primary_email"]); |
|
| 310 | + $attendee->name = w2u($meinfo["fullname"]); |
|
| 311 | + $attendee->attendeetype = MAPI_TO; |
|
| 312 | + |
|
| 313 | + array_push($message->attendees, $attendee); |
|
| 314 | + } |
|
| 315 | + } |
|
| 316 | + $message->responsetype = $messageprops[$appointmentprops["responsestatus"]]; |
|
| 317 | + } |
|
| 318 | + |
|
| 319 | + // If it's an appointment which doesn't have any attendees, we have to make sure that |
|
| 320 | + // the user is the owner or it will not work properly with android devices |
|
| 321 | + // @see https://jira.z-hub.io/browse/ZP-1020 |
|
| 322 | + if (isset($messageprops[$appointmentprops["meetingstatus"]]) && $messageprops[$appointmentprops["meetingstatus"]] == olNonMeeting && empty($message->attendees)) { |
|
| 323 | + $meinfo = nsp_getuserinfo(Request::GetUser()); |
|
| 324 | + |
|
| 325 | + if (is_array($meinfo)) { |
|
| 326 | + $message->organizeremail = w2u($meinfo["primary_email"]); |
|
| 327 | + $message->organizername = w2u($meinfo["fullname"]); |
|
| 328 | + SLog::Write(LOGLEVEL_DEBUG, "MAPIProvider->getAppointment(): setting ourself as the organizer for an appointment without attendees."); |
|
| 329 | + } |
|
| 330 | + } |
|
| 331 | + |
|
| 332 | + if (!isset($message->nativebodytype)) { |
|
| 333 | + $message->nativebodytype = MAPIUtils::GetNativeBodyType($messageprops); |
|
| 334 | + } |
|
| 335 | + elseif ($message->nativebodytype == SYNC_BODYPREFERENCE_UNDEFINED) { |
|
| 336 | + $nbt = MAPIUtils::GetNativeBodyType($messageprops); |
|
| 337 | + SLog::Write(LOGLEVEL_INFO, sprintf("MAPIProvider->getAppointment(): native body type is undefined. Set it to %d.", $nbt)); |
|
| 338 | + $message->nativebodytype = $nbt; |
|
| 339 | + } |
|
| 340 | + |
|
| 341 | + // If the user is working from a location other than the office the busystatus should be interpreted as free. |
|
| 342 | + if (isset($message->busystatus) && $message->busystatus == fbWorkingElsewhere) { |
|
| 343 | + $message->busystatus = fbFree; |
|
| 344 | + } |
|
| 345 | + |
|
| 346 | + // If the busystatus has the value of -1, we should be interpreted as tentative (1) / ZP-581 |
|
| 347 | + if (isset($message->busystatus) && $message->busystatus == -1) { |
|
| 348 | + $message->busystatus = fbTentative; |
|
| 349 | + } |
|
| 350 | + |
|
| 351 | + // All-day events might appear as 24h (or multiple of it) long when they start not exactly at midnight (+/- bias of the timezone) |
|
| 352 | + if (isset($message->alldayevent) && $message->alldayevent) { |
|
| 353 | + $localStartTime = localtime($message->starttime, 1); |
|
| 354 | + |
|
| 355 | + // The appointment is all-day but doesn't start at midnight. |
|
| 356 | + // If it was created in another timezone and we have that information, |
|
| 357 | + // set the startime to the midnight of the current timezone. |
|
| 358 | + if ($appTz && ($localStartTime['tm_hour'] || $localStartTime['tm_min'])) { |
|
| 359 | + SLog::Write(LOGLEVEL_DEBUG, "MAPIProvider->getAppointment(): all-day event starting not midnight."); |
|
| 360 | + $duration = $message->endtime - $message->starttime; |
|
| 361 | + $serverTz = TimezoneUtil::GetFullTZ(); |
|
| 362 | + $message->starttime = $this->getGMTTimeByTZ($this->getLocaltimeByTZ($message->starttime, $tz), $serverTz); |
|
| 363 | + $message->endtime = $message->starttime + $duration; |
|
| 364 | + } |
|
| 365 | + } |
|
| 366 | + |
|
| 367 | + return $message; |
|
| 368 | + } |
|
| 369 | + |
|
| 370 | + /** |
|
| 371 | + * Reads recurrence information from MAPI. |
|
| 372 | + * |
|
| 373 | + * @param mixed $mapimessage |
|
| 374 | + * @param array $recurprops |
|
| 375 | + * @param SyncObject &$syncMessage the message |
|
| 376 | + * @param SyncObject &$syncRecurrence the recurrence message |
|
| 377 | + * @param array $tz timezone information |
|
| 378 | + * |
|
| 379 | + * @return |
|
| 380 | + */ |
|
| 381 | + private function getRecurrence($mapimessage, $recurprops, &$syncMessage, &$syncRecurrence, $tz) { |
|
| 382 | + if ($syncRecurrence instanceof SyncTaskRecurrence) { |
|
| 383 | + $recurrence = new TaskRecurrence($this->store, $mapimessage); |
|
| 384 | + } |
|
| 385 | + else { |
|
| 386 | + $recurrence = new Recurrence($this->store, $mapimessage); |
|
| 387 | + } |
|
| 388 | + |
|
| 389 | + switch ($recurrence->recur["type"]) { |
|
| 390 | + case 10: // daily |
|
| 391 | + switch ($recurrence->recur["subtype"]) { |
|
| 392 | + default: |
|
| 393 | + $syncRecurrence->type = 0; |
|
| 394 | + break; |
|
| 395 | + |
|
| 396 | + case 1: |
|
| 397 | + $syncRecurrence->type = 0; |
|
| 398 | + $syncRecurrence->dayofweek = 62; // mon-fri |
|
| 399 | + $syncRecurrence->interval = 1; |
|
| 400 | + break; |
|
| 401 | + } |
|
| 402 | + break; |
|
| 403 | + |
|
| 404 | + case 11: // weekly |
|
| 405 | + $syncRecurrence->type = 1; |
|
| 406 | + break; |
|
| 407 | + |
|
| 408 | + case 12: // monthly |
|
| 409 | + switch ($recurrence->recur["subtype"]) { |
|
| 410 | + default: |
|
| 411 | + $syncRecurrence->type = 2; |
|
| 412 | + break; |
|
| 413 | + |
|
| 414 | + case 3: |
|
| 415 | + $syncRecurrence->type = 3; |
|
| 416 | + break; |
|
| 417 | + } |
|
| 418 | + break; |
|
| 419 | + |
|
| 420 | + case 13: // yearly |
|
| 421 | + switch ($recurrence->recur["subtype"]) { |
|
| 422 | + default: |
|
| 423 | + $syncRecurrence->type = 4; |
|
| 424 | + break; |
|
| 425 | + |
|
| 426 | + case 2: |
|
| 427 | + $syncRecurrence->type = 5; |
|
| 428 | + break; |
|
| 429 | + |
|
| 430 | + case 3: |
|
| 431 | + $syncRecurrence->type = 6; |
|
| 432 | + break; |
|
| 433 | + } |
|
| 434 | + } |
|
| 435 | + // Termination |
|
| 436 | + switch ($recurrence->recur["term"]) { |
|
| 437 | + case 0x21: |
|
| 438 | + $syncRecurrence->until = $recurrence->recur["end"]; |
|
| 439 | + // fixes Mantis #350 : recur-end does not consider timezones - use ClipEnd if available |
|
| 440 | + if (isset($recurprops[$recurrence->proptags["enddate_recurring"]])) { |
|
| 441 | + $syncRecurrence->until = $recurprops[$recurrence->proptags["enddate_recurring"]]; |
|
| 442 | + } |
|
| 443 | + // add one day (minus 1 sec) to the end time to make sure the last occurrence is covered |
|
| 444 | + $syncRecurrence->until += 86399; |
|
| 445 | + break; |
|
| 446 | + |
|
| 447 | + case 0x22: |
|
| 448 | + $syncRecurrence->occurrences = $recurrence->recur["numoccur"]; |
|
| 449 | + break; |
|
| 450 | + |
|
| 451 | + case 0x23: |
|
| 452 | + // never ends |
|
| 453 | + break; |
|
| 454 | + } |
|
| 455 | + |
|
| 456 | + // Correct 'alldayevent' because outlook fails to set it on recurring items of 24 hours or longer |
|
| 457 | + if (isset($recurrence->recur["endocc"], $recurrence->recur["startocc"]) && ($recurrence->recur["endocc"] - $recurrence->recur["startocc"] >= 1440)) { |
|
| 458 | + $syncMessage->alldayevent = true; |
|
| 459 | + } |
|
| 460 | + |
|
| 461 | + // Interval is different according to the type/subtype |
|
| 462 | + switch ($recurrence->recur["type"]) { |
|
| 463 | + case 10: |
|
| 464 | + if ($recurrence->recur["subtype"] == 0) { |
|
| 465 | + $syncRecurrence->interval = (int) ($recurrence->recur["everyn"] / 1440); |
|
| 466 | + } // minutes |
|
| 467 | + break; |
|
| 468 | + |
|
| 469 | + case 11: |
|
| 470 | + case 12: |
|
| 471 | + $syncRecurrence->interval = $recurrence->recur["everyn"]; |
|
| 472 | + break; // months / weeks |
|
| 473 | + |
|
| 474 | + case 13: |
|
| 475 | + $syncRecurrence->interval = (int) ($recurrence->recur["everyn"] / 12); |
|
| 476 | + break; // months |
|
| 477 | + } |
|
| 478 | + |
|
| 479 | + if (isset($recurrence->recur["weekdays"])) { |
|
| 480 | + $syncRecurrence->dayofweek = $recurrence->recur["weekdays"]; |
|
| 481 | + } // bitmask of days (1 == sunday, 128 == saturday |
|
| 482 | + if (isset($recurrence->recur["nday"])) { |
|
| 483 | + $syncRecurrence->weekofmonth = $recurrence->recur["nday"]; |
|
| 484 | + } // N'th {DAY} of {X} (0-5) |
|
| 485 | + if (isset($recurrence->recur["month"])) { |
|
| 486 | + $syncRecurrence->monthofyear = (int) ($recurrence->recur["month"] / (60 * 24 * 29)) + 1; |
|
| 487 | + } // works ok due to rounding. see also $monthminutes below (1-12) |
|
| 488 | + if (isset($recurrence->recur["monthday"])) { |
|
| 489 | + $syncRecurrence->dayofmonth = $recurrence->recur["monthday"]; |
|
| 490 | + } // day of month (1-31) |
|
| 491 | + |
|
| 492 | + // All changed exceptions are appointments within the 'exceptions' array. They contain the same items as a normal appointment |
|
| 493 | + foreach ($recurrence->recur["changed_occurrences"] as $change) { |
|
| 494 | + $exception = new SyncAppointmentException(); |
|
| 495 | + |
|
| 496 | + // start, end, basedate, subject, remind_before, reminderset, location, busystatus, alldayevent, label |
|
| 497 | + if (isset($change["start"])) { |
|
| 498 | + $exception->starttime = $this->getGMTTimeByTZ($change["start"], $tz); |
|
| 499 | + } |
|
| 500 | + if (isset($change["end"])) { |
|
| 501 | + $exception->endtime = $this->getGMTTimeByTZ($change["end"], $tz); |
|
| 502 | + } |
|
| 503 | + if (isset($change["basedate"])) { |
|
| 504 | + $exception->exceptionstarttime = $this->getGMTTimeByTZ($this->getDayStartOfTimestamp($change["basedate"]) + $recurrence->recur["startocc"] * 60, $tz); |
|
| 505 | + |
|
| 506 | + // open body because getting only property might not work because of memory limit |
|
| 507 | + $exceptionatt = $recurrence->getExceptionAttachment($change["basedate"]); |
|
| 508 | + if ($exceptionatt) { |
|
| 509 | + $exceptionobj = mapi_attach_openobj($exceptionatt, 0); |
|
| 510 | + $this->setMessageBodyForType($exceptionobj, SYNC_BODYPREFERENCE_PLAIN, $exception); |
|
| 511 | + } |
|
| 512 | + } |
|
| 513 | + if (isset($change["subject"])) { |
|
| 514 | + $exception->subject = w2u($change["subject"]); |
|
| 515 | + } |
|
| 516 | + if (isset($change["reminder_before"]) && $change["reminder_before"]) { |
|
| 517 | + $exception->reminder = $change["remind_before"]; |
|
| 518 | + } |
|
| 519 | + if (isset($change["location"])) { |
|
| 520 | + $exception->location = w2u($change["location"]); |
|
| 521 | + } |
|
| 522 | + if (isset($change["busystatus"])) { |
|
| 523 | + $exception->busystatus = $change["busystatus"]; |
|
| 524 | + } |
|
| 525 | + if (isset($change["alldayevent"])) { |
|
| 526 | + $exception->alldayevent = $change["alldayevent"]; |
|
| 527 | + } |
|
| 528 | + |
|
| 529 | + // set some data from the original appointment |
|
| 530 | + if (isset($syncMessage->uid)) { |
|
| 531 | + $exception->uid = $syncMessage->uid; |
|
| 532 | + } |
|
| 533 | + if (isset($syncMessage->organizername)) { |
|
| 534 | + $exception->organizername = $syncMessage->organizername; |
|
| 535 | + } |
|
| 536 | + if (isset($syncMessage->organizeremail)) { |
|
| 537 | + $exception->organizeremail = $syncMessage->organizeremail; |
|
| 538 | + } |
|
| 539 | + |
|
| 540 | + if (!isset($syncMessage->exceptions)) { |
|
| 541 | + $syncMessage->exceptions = []; |
|
| 542 | + } |
|
| 543 | + |
|
| 544 | + // If the user is working from a location other than the office the busystatus should be interpreted as free. |
|
| 545 | + if (isset($exception->busystatus) && $exception->busystatus == fbWorkingElsewhere) { |
|
| 546 | + $exception->busystatus = fbFree; |
|
| 547 | + } |
|
| 548 | + |
|
| 549 | + // If the busystatus has the value of -1, we should be interpreted as tentative (1) / ZP-581 |
|
| 550 | + if (isset($exception->busystatus) && $exception->busystatus == -1) { |
|
| 551 | + $exception->busystatus = fbTentative; |
|
| 552 | + } |
|
| 553 | + |
|
| 554 | + // if an exception lasts 24 hours and the series are an allday events, set also the exception to allday event, |
|
| 555 | + // otherwise it will be a 24 hour long event on some mobiles. |
|
| 556 | + // @see https://jira.z-hub.io/browse/ZP-980 |
|
| 557 | + if (isset($exception->starttime, $exception->endtime) && ($exception->endtime - $exception->starttime == 86400) && $syncMessage->alldayevent) { |
|
| 558 | + $exception->alldayevent = 1; |
|
| 559 | + } |
|
| 560 | + array_push($syncMessage->exceptions, $exception); |
|
| 561 | + } |
|
| 562 | + |
|
| 563 | + // Deleted appointments contain only the original date (basedate) and a 'deleted' tag |
|
| 564 | + foreach ($recurrence->recur["deleted_occurrences"] as $deleted) { |
|
| 565 | + $exception = new SyncAppointmentException(); |
|
| 566 | + |
|
| 567 | + $exception->exceptionstarttime = $this->getGMTTimeByTZ($this->getDayStartOfTimestamp($deleted) + $recurrence->recur["startocc"] * 60, $tz); |
|
| 568 | + $exception->deleted = "1"; |
|
| 569 | + |
|
| 570 | + if (!isset($syncMessage->exceptions)) { |
|
| 571 | + $syncMessage->exceptions = []; |
|
| 572 | + } |
|
| 573 | + |
|
| 574 | + array_push($syncMessage->exceptions, $exception); |
|
| 575 | + } |
|
| 576 | + |
|
| 577 | + if (isset($syncMessage->complete) && $syncMessage->complete) { |
|
| 578 | + $syncRecurrence->complete = $syncMessage->complete; |
|
| 579 | + } |
|
| 580 | + } |
|
| 581 | + |
|
| 582 | + /** |
|
| 583 | + * Reads an email object from MAPI. |
|
| 584 | + * |
|
| 585 | + * @param mixed $mapimessage |
|
| 586 | + * @param ContentParameters $contentparameters |
|
| 587 | + * |
|
| 588 | + * @return SyncEmail |
|
| 589 | + */ |
|
| 590 | + private function getEmail($mapimessage, $contentparameters) { |
|
| 591 | + // This workaround fixes ZP-729 and still works with Outlook. |
|
| 592 | + // FIXME: It should be properly fixed when refactoring. |
|
| 593 | + $bpReturnType = Utils::GetBodyPreferenceBestMatch($contentparameters->GetBodyPreference()); |
|
| 594 | + if (($contentparameters->GetMimeSupport() == SYNC_MIMESUPPORT_NEVER) || |
|
| 595 | + ($key = array_search(SYNC_BODYPREFERENCE_MIME, $contentparameters->GetBodyPreference()) === false) || |
|
| 596 | + $bpReturnType != SYNC_BODYPREFERENCE_MIME) { |
|
| 597 | + MAPIUtils::ParseSmime($this->session, $this->store, $this->getAddressbook(), $mapimessage); |
|
| 598 | + } |
|
| 599 | + |
|
| 600 | + $message = new SyncMail(); |
|
| 601 | + |
|
| 602 | + $this->getPropsFromMAPI($message, $mapimessage, MAPIMapping::GetEmailMapping()); |
|
| 603 | + |
|
| 604 | + $emailproperties = MAPIMapping::GetEmailProperties(); |
|
| 605 | + $messageprops = $this->getProps($mapimessage, $emailproperties); |
|
| 606 | + |
|
| 607 | + if (isset($messageprops[PR_SOURCE_KEY])) { |
|
| 608 | + $sourcekey = $messageprops[PR_SOURCE_KEY]; |
|
| 609 | + } |
|
| 610 | + else { |
|
| 611 | + $mbe = new SyncObjectBrokenException("The message doesn't have a sourcekey"); |
|
| 612 | + $mbe->SetSyncObject($message); |
|
| 613 | + |
|
| 614 | + throw $mbe; |
|
| 615 | + } |
|
| 616 | + |
|
| 617 | + // set the body according to contentparameters and supported AS version |
|
| 618 | + $this->setMessageBody($mapimessage, $contentparameters, $message); |
|
| 619 | + |
|
| 620 | + $fromname = $fromaddr = ""; |
|
| 621 | + |
|
| 622 | + if (isset($messageprops[$emailproperties["representingname"]])) { |
|
| 623 | + // remove encapsulating double quotes from the representingname |
|
| 624 | + $fromname = preg_replace('/^\"(.*)\"$/', "\${1}", $messageprops[$emailproperties["representingname"]]); |
|
| 625 | + } |
|
| 626 | + if (isset($messageprops[$emailproperties["representingentryid"]])) { |
|
| 627 | + $fromaddr = $this->getSMTPAddressFromEntryID($messageprops[$emailproperties["representingentryid"]]); |
|
| 628 | + } |
|
| 629 | + |
|
| 630 | + // if the email address can't be resolved, fall back to PR_SENT_REPRESENTING_SEARCH_KEY |
|
| 631 | + if ($fromaddr == "" && isset($messageprops[$emailproperties["representingsearchkey"]])) { |
|
| 632 | + $fromaddr = $this->getEmailAddressFromSearchKey($messageprops[$emailproperties["representingsearchkey"]]); |
|
| 633 | + } |
|
| 634 | + |
|
| 635 | + if ($fromname == $fromaddr) { |
|
| 636 | + $fromname = ""; |
|
| 637 | + } |
|
| 638 | + |
|
| 639 | + if ($fromname) { |
|
| 640 | + $from = "\"" . w2u($fromname) . "\" <" . w2u($fromaddr) . ">"; |
|
| 641 | + } |
|
| 642 | + else { // START CHANGED dw2412 HTC shows "error" if sender name is unknown |
|
| 643 | + $from = "\"" . w2u($fromaddr) . "\" <" . w2u($fromaddr) . ">"; |
|
| 644 | + } |
|
| 645 | + // END CHANGED dw2412 HTC shows "error" if sender name is unknown |
|
| 646 | + |
|
| 647 | + $message->from = $from; |
|
| 648 | + |
|
| 649 | + // process Meeting Requests |
|
| 650 | + if (isset($message->messageclass) && strpos($message->messageclass, "IPM.Schedule.Meeting") === 0) { |
|
| 651 | + $message->meetingrequest = new SyncMeetingRequest(); |
|
| 652 | + $this->getPropsFromMAPI($message->meetingrequest, $mapimessage, MAPIMapping::GetMeetingRequestMapping()); |
|
| 653 | + |
|
| 654 | + $meetingrequestproperties = MAPIMapping::GetMeetingRequestProperties(); |
|
| 655 | + $props = $this->getProps($mapimessage, $meetingrequestproperties); |
|
| 656 | + |
|
| 657 | + // Get the GOID |
|
| 658 | + if (isset($props[$meetingrequestproperties["goidtag"]])) { |
|
| 659 | + $message->meetingrequest->globalobjid = base64_encode($props[$meetingrequestproperties["goidtag"]]); |
|
| 660 | + } |
|
| 661 | + |
|
| 662 | + // Set Timezone |
|
| 663 | + if (isset($props[$meetingrequestproperties["timezonetag"]])) { |
|
| 664 | + $tz = $this->getTZFromMAPIBlob($props[$meetingrequestproperties["timezonetag"]]); |
|
| 665 | + } |
|
| 666 | + else { |
|
| 667 | + $tz = TimezoneUtil::GetFullTZ(); |
|
| 668 | + } |
|
| 669 | + |
|
| 670 | + $message->meetingrequest->timezone = base64_encode(TimezoneUtil::GetSyncBlobFromTZ($tz)); |
|
| 671 | + |
|
| 672 | + // send basedate if exception |
|
| 673 | + if (isset($props[$meetingrequestproperties["recReplTime"]]) || |
|
| 674 | + (isset($props[$meetingrequestproperties["lidIsException"]]) && $props[$meetingrequestproperties["lidIsException"]] == true)) { |
|
| 675 | + if (isset($props[$meetingrequestproperties["recReplTime"]])) { |
|
| 676 | + $basedate = $props[$meetingrequestproperties["recReplTime"]]; |
|
| 677 | + $message->meetingrequest->recurrenceid = $this->getGMTTimeByTZ($basedate, $this->getGMTTZ()); |
|
| 678 | + } |
|
| 679 | + else { |
|
| 680 | + if (!isset($props[$meetingrequestproperties["goidtag"]]) || !isset($props[$meetingrequestproperties["recurStartTime"]]) || !isset($props[$meetingrequestproperties["timezonetag"]])) { |
|
| 681 | + SLog::Write(LOGLEVEL_WARN, "Missing property to set correct basedate for exception"); |
|
| 682 | + } |
|
| 683 | + else { |
|
| 684 | + $basedate = Utils::ExtractBaseDate($props[$meetingrequestproperties["goidtag"]], $props[$meetingrequestproperties["recurStartTime"]]); |
|
| 685 | + $message->meetingrequest->recurrenceid = $this->getGMTTimeByTZ($basedate, $tz); |
|
| 686 | + } |
|
| 687 | + } |
|
| 688 | + } |
|
| 689 | + |
|
| 690 | + // Organizer is the sender |
|
| 691 | + if (strpos($message->messageclass, "IPM.Schedule.Meeting.Resp") === 0) { |
|
| 692 | + $message->meetingrequest->organizer = $message->to; |
|
| 693 | + } |
|
| 694 | + else { |
|
| 695 | + $message->meetingrequest->organizer = $message->from; |
|
| 696 | + } |
|
| 697 | + |
|
| 698 | + // Process recurrence |
|
| 699 | + if (isset($props[$meetingrequestproperties["isrecurringtag"]]) && $props[$meetingrequestproperties["isrecurringtag"]]) { |
|
| 700 | + $myrec = new SyncMeetingRequestRecurrence(); |
|
| 701 | + // get recurrence -> put $message->meetingrequest as message so the 'alldayevent' is set correctly |
|
| 702 | + $this->getRecurrence($mapimessage, $props, $message->meetingrequest, $myrec, $tz); |
|
| 703 | + $message->meetingrequest->recurrences = [$myrec]; |
|
| 704 | + } |
|
| 705 | + |
|
| 706 | + // Force the 'alldayevent' in the object at all times. (non-existent == 0) |
|
| 707 | + if (!isset($message->meetingrequest->alldayevent) || $message->meetingrequest->alldayevent == "") { |
|
| 708 | + $message->meetingrequest->alldayevent = 0; |
|
| 709 | + } |
|
| 710 | + |
|
| 711 | + // Instancetype |
|
| 712 | + // 0 = single appointment |
|
| 713 | + // 1 = master recurring appointment |
|
| 714 | + // 2 = single instance of recurring appointment |
|
| 715 | + // 3 = exception of recurring appointment |
|
| 716 | + $message->meetingrequest->instancetype = 0; |
|
| 717 | + if (isset($props[$meetingrequestproperties["isrecurringtag"]]) && $props[$meetingrequestproperties["isrecurringtag"]] == 1) { |
|
| 718 | + $message->meetingrequest->instancetype = 1; |
|
| 719 | + } |
|
| 720 | + elseif ((!isset($props[$meetingrequestproperties["isrecurringtag"]]) || $props[$meetingrequestproperties["isrecurringtag"]] == 0) && isset($message->meetingrequest->recurrenceid)) { |
|
| 721 | + if (isset($props[$meetingrequestproperties["appSeqNr"]]) && $props[$meetingrequestproperties["appSeqNr"]] == 0) { |
|
| 722 | + $message->meetingrequest->instancetype = 2; |
|
| 723 | + } |
|
| 724 | + else { |
|
| 725 | + $message->meetingrequest->instancetype = 3; |
|
| 726 | + } |
|
| 727 | + } |
|
| 728 | + |
|
| 729 | + // Disable reminder if it is off |
|
| 730 | + if (!isset($props[$meetingrequestproperties["reminderset"]]) || $props[$meetingrequestproperties["reminderset"]] == false) { |
|
| 731 | + $message->meetingrequest->reminder = ""; |
|
| 732 | + } |
|
| 733 | + // the property saves reminder in minutes, but we need it in secs |
|
| 734 | + else { |
|
| 735 | + // /set the default reminder time to seconds |
|
| 736 | + if ($props[$meetingrequestproperties["remindertime"]] == 0x5AE980E1) { |
|
| 737 | + $message->meetingrequest->reminder = 900; |
|
| 738 | + } |
|
| 739 | + else { |
|
| 740 | + $message->meetingrequest->reminder = $props[$meetingrequestproperties["remindertime"]] * 60; |
|
| 741 | + } |
|
| 742 | + } |
|
| 743 | + |
|
| 744 | + // Set sensitivity to 0 if missing |
|
| 745 | + if (!isset($message->meetingrequest->sensitivity)) { |
|
| 746 | + $message->meetingrequest->sensitivity = 0; |
|
| 747 | + } |
|
| 748 | + |
|
| 749 | + // If the user is working from a location other than the office the busystatus should be interpreted as free. |
|
| 750 | + if (isset($message->meetingrequest->busystatus) && $message->meetingrequest->busystatus == fbWorkingElsewhere) { |
|
| 751 | + $message->meetingrequest->busystatus = fbFree; |
|
| 752 | + } |
|
| 753 | + |
|
| 754 | + // If the busystatus has the value of -1, we should be interpreted as tentative (1) / ZP-581 |
|
| 755 | + if (isset($message->meetingrequest->busystatus) && $message->meetingrequest->busystatus == -1) { |
|
| 756 | + $message->meetingrequest->busystatus = fbTentative; |
|
| 757 | + } |
|
| 758 | + |
|
| 759 | + // if a meeting request response hasn't been processed yet, |
|
| 760 | + // do it so that the attendee status is updated on the mobile |
|
| 761 | + if (!isset($messageprops[$emailproperties["processed"]])) { |
|
| 762 | + // check if we are not sending the MR so we can process it - ZP-581 |
|
| 763 | + $cuser = GSync::GetBackend()->GetUserDetails(GSync::GetBackend()->GetCurrentUsername()); |
|
| 764 | + if (isset($cuser["emailaddress"]) && $cuser["emailaddress"] != $fromaddr) { |
|
| 765 | + if (!isset($req)) { |
|
| 766 | + $req = new Meetingrequest($this->store, $mapimessage, $this->session); |
|
| 767 | + } |
|
| 768 | + if ($req->isMeetingRequestResponse()) { |
|
| 769 | + $req->processMeetingRequestResponse(); |
|
| 770 | + } |
|
| 771 | + if ($req->isMeetingCancellation()) { |
|
| 772 | + $req->processMeetingCancellation(); |
|
| 773 | + } |
|
| 774 | + } |
|
| 775 | + } |
|
| 776 | + $message->contentclass = DEFAULT_CALENDAR_CONTENTCLASS; |
|
| 777 | + |
|
| 778 | + // MeetingMessageType values |
|
| 779 | + // 0 = A silent update was performed, or the message type is unspecified. |
|
| 780 | + // 1 = Initial meeting request. |
|
| 781 | + // 2 = Full update. |
|
| 782 | + // 3 = Informational update. |
|
| 783 | + // 4 = Outdated. A newer meeting request or meeting update was received after this message. |
|
| 784 | + // 5 = Identifies the delegator's copy of the meeting request. |
|
| 785 | + // 6 = Identifies that the meeting request has been delegated and the meeting request cannot be responded to. |
|
| 786 | + $message->meetingrequest->meetingmessagetype = mtgEmpty; |
|
| 787 | + |
|
| 788 | + if (isset($props[$meetingrequestproperties["meetingType"]])) { |
|
| 789 | + switch ($props[$meetingrequestproperties["meetingType"]]) { |
|
| 790 | + case mtgRequest: |
|
| 791 | + $message->meetingrequest->meetingmessagetype = 1; |
|
| 792 | + break; |
|
| 793 | + |
|
| 794 | + case mtgFull: |
|
| 795 | + $message->meetingrequest->meetingmessagetype = 2; |
|
| 796 | + break; |
|
| 797 | + |
|
| 798 | + case mtgInfo: |
|
| 799 | + $message->meetingrequest->meetingmessagetype = 3; |
|
| 800 | + break; |
|
| 801 | + |
|
| 802 | + case mtgOutOfDate: |
|
| 803 | + $message->meetingrequest->meetingmessagetype = 4; |
|
| 804 | + break; |
|
| 805 | + |
|
| 806 | + case mtgDelegatorCopy: |
|
| 807 | + $message->meetingrequest->meetingmessagetype = 5; |
|
| 808 | + break; |
|
| 809 | + } |
|
| 810 | + } |
|
| 811 | + } |
|
| 812 | + |
|
| 813 | + // Add attachments |
|
| 814 | + $attachtable = mapi_message_getattachmenttable($mapimessage); |
|
| 815 | + $rows = mapi_table_queryallrows($attachtable, [PR_ATTACH_NUM]); |
|
| 816 | + $entryid = bin2hex($messageprops[$emailproperties["entryid"]]); |
|
| 817 | + $parentSourcekey = bin2hex($messageprops[$emailproperties["parentsourcekey"]]); |
|
| 818 | + |
|
| 819 | + foreach ($rows as $row) { |
|
| 820 | + if (isset($row[PR_ATTACH_NUM])) { |
|
| 821 | + if (Request::GetProtocolVersion() >= 12.0) { |
|
| 822 | + $attach = new SyncBaseAttachment(); |
|
| 823 | + } |
|
| 824 | + else { |
|
| 825 | + $attach = new SyncAttachment(); |
|
| 826 | + } |
|
| 827 | + |
|
| 828 | + $mapiattach = mapi_message_openattach($mapimessage, $row[PR_ATTACH_NUM]); |
|
| 829 | + $attachprops = mapi_getprops($mapiattach, [PR_ATTACH_LONG_FILENAME, PR_ATTACH_FILENAME, PR_ATTACHMENT_HIDDEN, PR_ATTACH_CONTENT_ID, PR_ATTACH_CONTENT_ID_W, PR_ATTACH_MIME_TAG, PR_ATTACH_MIME_TAG_W, PR_ATTACH_METHOD, PR_DISPLAY_NAME, PR_DISPLAY_NAME_W, PR_ATTACH_SIZE, PR_ATTACH_FLAGS]); |
|
| 830 | + if ((isset($attachprops[PR_ATTACH_MIME_TAG]) && strpos(strtolower($attachprops[PR_ATTACH_MIME_TAG]), 'signed') !== false) || |
|
| 831 | + (isset($attachprops[PR_ATTACH_MIME_TAG_W]) && strpos(strtolower($attachprops[PR_ATTACH_MIME_TAG_W]), 'signed') !== false)) { |
|
| 832 | + continue; |
|
| 833 | + } |
|
| 834 | + |
|
| 835 | + // the displayname is handled equally for all AS versions |
|
| 836 | + $attach->displayname = w2u((isset($attachprops[PR_ATTACH_LONG_FILENAME])) ? $attachprops[PR_ATTACH_LONG_FILENAME] : ((isset($attachprops[PR_ATTACH_FILENAME])) ? $attachprops[PR_ATTACH_FILENAME] : ((isset($attachprops[PR_DISPLAY_NAME])) ? $attachprops[PR_DISPLAY_NAME] : "attachment.bin"))); |
|
| 837 | + // fix attachment name in case of inline images |
|
| 838 | + if (($attach->displayname == "inline.txt" && (isset($attachprops[PR_ATTACH_MIME_TAG]) || $attachprops[PR_ATTACH_MIME_TAG_W])) || |
|
| 839 | + (substr_compare($attach->displayname, "attachment", 0, 10, true) === 0 && substr_compare($attach->displayname, ".dat", -4, 4, true) === 0)) { |
|
| 840 | + $mimetype = (isset($attachprops[PR_ATTACH_MIME_TAG])) ? $attachprops[PR_ATTACH_MIME_TAG] : $attachprops[PR_ATTACH_MIME_TAG_W]; |
|
| 841 | + $mime = explode("/", $mimetype); |
|
| 842 | + |
|
| 843 | + if (count($mime) == 2 && $mime[0] == "image") { |
|
| 844 | + $attach->displayname = "inline." . $mime[1]; |
|
| 845 | + } |
|
| 846 | + } |
|
| 847 | + |
|
| 848 | + // set AS version specific parameters |
|
| 849 | + if (Request::GetProtocolVersion() >= 12.0) { |
|
| 850 | + $attach->filereference = sprintf("%s:%s:%s", $entryid, $row[PR_ATTACH_NUM], $parentSourcekey); |
|
| 851 | + $attach->method = (isset($attachprops[PR_ATTACH_METHOD])) ? $attachprops[PR_ATTACH_METHOD] : ATTACH_BY_VALUE; |
|
| 852 | + |
|
| 853 | + // if displayname does not have the eml extension for embedde messages, android and WP devices won't open it |
|
| 854 | + if ($attach->method == ATTACH_EMBEDDED_MSG) { |
|
| 855 | + if (strtolower(substr($attach->displayname, -4)) != '.eml') { |
|
| 856 | + $attach->displayname .= '.eml'; |
|
| 857 | + } |
|
| 858 | + } |
|
| 859 | + // android devices require attachment size in order to display an attachment properly |
|
| 860 | + if (!isset($attachprops[PR_ATTACH_SIZE])) { |
|
| 861 | + $stream = mapi_openproperty($mapiattach, PR_ATTACH_DATA_BIN, IID_IStream, 0, 0); |
|
| 862 | + // It's not possible to open some (embedded only?) messages, so we need to open the attachment object itself to get the data |
|
| 863 | + if (mapi_last_hresult()) { |
|
| 864 | + $embMessage = mapi_attach_openobj($mapiattach); |
|
| 865 | + $addrbook = $this->getAddressbook(); |
|
| 866 | + $stream = mapi_inetmapi_imtoinet($this->session, $addrbook, $embMessage, ['use_tnef' => -1]); |
|
| 867 | + } |
|
| 868 | + $stat = mapi_stream_stat($stream); |
|
| 869 | + $attach->estimatedDataSize = $stat['cb']; |
|
| 870 | + } |
|
| 871 | + else { |
|
| 872 | + $attach->estimatedDataSize = $attachprops[PR_ATTACH_SIZE]; |
|
| 873 | + } |
|
| 874 | + |
|
| 875 | + if (isset($attachprops[PR_ATTACH_CONTENT_ID]) && $attachprops[PR_ATTACH_CONTENT_ID]) { |
|
| 876 | + $attach->contentid = $attachprops[PR_ATTACH_CONTENT_ID]; |
|
| 877 | + } |
|
| 878 | + |
|
| 879 | + if (!isset($attach->contentid) && isset($attachprops[PR_ATTACH_CONTENT_ID_W]) && $attachprops[PR_ATTACH_CONTENT_ID_W]) { |
|
| 880 | + $attach->contentid = $attachprops[PR_ATTACH_CONTENT_ID_W]; |
|
| 881 | + } |
|
| 882 | + |
|
| 883 | + if (isset($attachprops[PR_ATTACHMENT_HIDDEN]) && $attachprops[PR_ATTACHMENT_HIDDEN]) { |
|
| 884 | + $attach->isinline = 1; |
|
| 885 | + } |
|
| 886 | + |
|
| 887 | + if (isset($attach->contentid, $attachprops[PR_ATTACH_FLAGS]) && $attachprops[PR_ATTACH_FLAGS] & 4) { |
|
| 888 | + $attach->isinline = 1; |
|
| 889 | + } |
|
| 890 | + |
|
| 891 | + if (!isset($message->asattachments)) { |
|
| 892 | + $message->asattachments = []; |
|
| 893 | + } |
|
| 894 | + |
|
| 895 | + array_push($message->asattachments, $attach); |
|
| 896 | + } |
|
| 897 | + else { |
|
| 898 | + $attach->attsize = $attachprops[PR_ATTACH_SIZE]; |
|
| 899 | + $attach->attname = sprintf("%s:%s:%s", $entryid, $row[PR_ATTACH_NUM], $parentSourcekey); |
|
| 900 | + if (!isset($message->attachments)) { |
|
| 901 | + $message->attachments = []; |
|
| 902 | + } |
|
| 903 | + |
|
| 904 | + array_push($message->attachments, $attach); |
|
| 905 | + } |
|
| 906 | + } |
|
| 907 | + } |
|
| 908 | + |
|
| 909 | + // Get To/Cc as SMTP addresses (this is different from displayto and displaycc because we are putting |
|
| 910 | + // in the SMTP addresses as well, while displayto and displaycc could just contain the display names |
|
| 911 | + $message->to = []; |
|
| 912 | + $message->cc = []; |
|
| 913 | + |
|
| 914 | + $reciptable = mapi_message_getrecipienttable($mapimessage); |
|
| 915 | + $rows = mapi_table_queryallrows($reciptable, [PR_RECIPIENT_TYPE, PR_DISPLAY_NAME, PR_ADDRTYPE, PR_EMAIL_ADDRESS, PR_SMTP_ADDRESS, PR_ENTRYID, PR_SEARCH_KEY]); |
|
| 916 | + |
|
| 917 | + foreach ($rows as $row) { |
|
| 918 | + $address = ""; |
|
| 919 | + $fulladdr = ""; |
|
| 920 | + |
|
| 921 | + $addrtype = isset($row[PR_ADDRTYPE]) ? $row[PR_ADDRTYPE] : ""; |
|
| 922 | + |
|
| 923 | + if (isset($row[PR_SMTP_ADDRESS])) { |
|
| 924 | + $address = $row[PR_SMTP_ADDRESS]; |
|
| 925 | + } |
|
| 926 | + elseif ($addrtype == "SMTP" && isset($row[PR_EMAIL_ADDRESS])) { |
|
| 927 | + $address = $row[PR_EMAIL_ADDRESS]; |
|
| 928 | + } |
|
| 929 | + elseif ($addrtype == "ZARAFA" && isset($row[PR_ENTRYID])) { |
|
| 930 | + $address = $this->getSMTPAddressFromEntryID($row[PR_ENTRYID]); |
|
| 931 | + } |
|
| 932 | + |
|
| 933 | + // if the user was not found, do a fallback to PR_SEARCH_KEY |
|
| 934 | + // @see https://jira.z-hub.io/browse/ZP-1178 |
|
| 935 | + if (empty($address) && isset($row[PR_SEARCH_KEY])) { |
|
| 936 | + $address = $this->getEmailAddressFromSearchKey($row[PR_SEARCH_KEY]); |
|
| 937 | + } |
|
| 938 | + |
|
| 939 | + $name = isset($row[PR_DISPLAY_NAME]) ? $row[PR_DISPLAY_NAME] : ""; |
|
| 940 | + |
|
| 941 | + if ($name == "" || $name == $address) { |
|
| 942 | + $fulladdr = w2u($address); |
|
| 943 | + } |
|
| 944 | + else { |
|
| 945 | + if (substr($name, 0, 1) != '"' && substr($name, -1) != '"') { |
|
| 946 | + $fulladdr = "\"" . w2u($name) . "\" <" . w2u($address) . ">"; |
|
| 947 | + } |
|
| 948 | + else { |
|
| 949 | + $fulladdr = w2u($name) . "<" . w2u($address) . ">"; |
|
| 950 | + } |
|
| 951 | + } |
|
| 952 | + |
|
| 953 | + if ($row[PR_RECIPIENT_TYPE] == MAPI_TO) { |
|
| 954 | + array_push($message->to, $fulladdr); |
|
| 955 | + } |
|
| 956 | + elseif ($row[PR_RECIPIENT_TYPE] == MAPI_CC) { |
|
| 957 | + array_push($message->cc, $fulladdr); |
|
| 958 | + } |
|
| 959 | + } |
|
| 960 | + |
|
| 961 | + if (is_array($message->to) && !empty($message->to)) { |
|
| 962 | + $message->to = implode(", ", $message->to); |
|
| 963 | + } |
|
| 964 | + if (is_array($message->cc) && !empty($message->cc)) { |
|
| 965 | + $message->cc = implode(", ", $message->cc); |
|
| 966 | + } |
|
| 967 | + |
|
| 968 | + // without importance some mobiles assume "0" (low) - Mantis #439 |
|
| 969 | + if (!isset($message->importance)) { |
|
| 970 | + $message->importance = IMPORTANCE_NORMAL; |
|
| 971 | + } |
|
| 972 | + |
|
| 973 | + if (!isset($message->internetcpid)) { |
|
| 974 | + $message->internetcpid = (defined('STORE_INTERNET_CPID')) ? constant('STORE_INTERNET_CPID') : INTERNET_CPID_WINDOWS1252; |
|
| 975 | + } |
|
| 976 | + $this->setFlag($mapimessage, $message); |
|
| 977 | + // TODO checkcontentclass |
|
| 978 | + if (!isset($message->contentclass)) { |
|
| 979 | + $message->contentclass = DEFAULT_EMAIL_CONTENTCLASS; |
|
| 980 | + } |
|
| 981 | + |
|
| 982 | + if (!isset($message->nativebodytype)) { |
|
| 983 | + $message->nativebodytype = MAPIUtils::GetNativeBodyType($messageprops); |
|
| 984 | + } |
|
| 985 | + elseif ($message->nativebodytype == SYNC_BODYPREFERENCE_UNDEFINED) { |
|
| 986 | + $nbt = MAPIUtils::GetNativeBodyType($messageprops); |
|
| 987 | + SLog::Write(LOGLEVEL_INFO, sprintf("MAPIProvider->getEmail(): native body type is undefined. Set it to %d.", $nbt)); |
|
| 988 | + $message->nativebodytype = $nbt; |
|
| 989 | + } |
|
| 990 | + |
|
| 991 | + // reply, reply to all, forward flags |
|
| 992 | + if (isset($message->lastverbexecuted) && $message->lastverbexecuted) { |
|
| 993 | + $message->lastverbexecuted = Utils::GetLastVerbExecuted($message->lastverbexecuted); |
|
| 994 | + } |
|
| 995 | + |
|
| 996 | + return $message; |
|
| 997 | + } |
|
| 998 | + |
|
| 999 | + /** |
|
| 1000 | + * Reads a note object from MAPI. |
|
| 1001 | + * |
|
| 1002 | + * @param mixed $mapimessage |
|
| 1003 | + * @param ContentParameters $contentparameters |
|
| 1004 | + * |
|
| 1005 | + * @return SyncNote |
|
| 1006 | + */ |
|
| 1007 | + private function getNote($mapimessage, $contentparameters) { |
|
| 1008 | + $message = new SyncNote(); |
|
| 1009 | + |
|
| 1010 | + // Standard one-to-one mappings first |
|
| 1011 | + $this->getPropsFromMAPI($message, $mapimessage, MAPIMapping::GetNoteMapping()); |
|
| 1012 | + |
|
| 1013 | + // set the body according to contentparameters and supported AS version |
|
| 1014 | + $this->setMessageBody($mapimessage, $contentparameters, $message); |
|
| 1015 | + |
|
| 1016 | + return $message; |
|
| 1017 | + } |
|
| 1018 | + |
|
| 1019 | + /** |
|
| 1020 | + * Creates a SyncFolder from MAPI properties. |
|
| 1021 | + * |
|
| 1022 | + * @param mixed $folderprops |
|
| 1023 | + * |
|
| 1024 | + * @return SyncFolder |
|
| 1025 | + */ |
|
| 1026 | + public function GetFolder($folderprops) { |
|
| 1027 | + $folder = new SyncFolder(); |
|
| 1028 | + |
|
| 1029 | + $storeprops = $this->GetStoreProps(); |
|
| 1030 | + |
|
| 1031 | + // For ZCP 7.0.x we need to retrieve more properties explicitly, see ZP-780 |
|
| 1032 | + if (isset($folderprops[PR_SOURCE_KEY]) && !isset($folderprops[PR_ENTRYID]) && !isset($folderprops[PR_CONTAINER_CLASS])) { |
|
| 1033 | + $entryid = mapi_msgstore_entryidfromsourcekey($this->store, $folderprops[PR_SOURCE_KEY]); |
|
| 1034 | + $mapifolder = mapi_msgstore_openentry($this->store, $entryid); |
|
| 1035 | + $folderprops = mapi_getprops($mapifolder, [PR_DISPLAY_NAME, PR_PARENT_ENTRYID, PR_ENTRYID, PR_SOURCE_KEY, PR_PARENT_SOURCE_KEY, PR_CONTAINER_CLASS, PR_ATTR_HIDDEN, PR_EXTENDED_FOLDER_FLAGS]); |
|
| 1036 | + SLog::Write(LOGLEVEL_DEBUG, "MAPIProvider->GetFolder(): received insufficient of data from ICS. Fetching required data."); |
|
| 1037 | + } |
|
| 1038 | + |
|
| 1039 | + if (!isset( |
|
| 1040 | + $folderprops[PR_DISPLAY_NAME], |
|
| 1041 | + $folderprops[PR_PARENT_ENTRYID], |
|
| 1042 | + $folderprops[PR_SOURCE_KEY], |
|
| 1043 | + $folderprops[PR_ENTRYID], |
|
| 1044 | + $folderprops[PR_PARENT_SOURCE_KEY], |
|
| 1045 | + $storeprops[PR_IPM_SUBTREE_ENTRYID])) { |
|
| 1046 | + SLog::Write(LOGLEVEL_ERROR, "MAPIProvider->GetFolder(): invalid folder. Missing properties"); |
|
| 1047 | + |
|
| 1048 | + return false; |
|
| 1049 | + } |
|
| 1050 | + |
|
| 1051 | + // ignore hidden folders |
|
| 1052 | + if (isset($folderprops[PR_ATTR_HIDDEN]) && $folderprops[PR_ATTR_HIDDEN] != false) { |
|
| 1053 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->GetFolder(): invalid folder '%s' as it is a hidden folder (PR_ATTR_HIDDEN)", $folderprops[PR_DISPLAY_NAME])); |
|
| 1054 | + |
|
| 1055 | + return false; |
|
| 1056 | + } |
|
| 1057 | + |
|
| 1058 | + // ignore certain undesired folders, like "RSS Feeds" and "Suggested contacts" |
|
| 1059 | + if ((isset($folderprops[PR_CONTAINER_CLASS]) && $folderprops[PR_CONTAINER_CLASS] == "IPF.Note.OutlookHomepage") || |
|
| 1060 | + in_array($folderprops[PR_ENTRYID], $this->getSpecialFoldersData())) { |
|
| 1061 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->GetFolder(): folder '%s' should not be synchronized", $folderprops[PR_DISPLAY_NAME])); |
|
| 1062 | + |
|
| 1063 | + return false; |
|
| 1064 | + } |
|
| 1065 | + |
|
| 1066 | + $folder->BackendId = bin2hex($folderprops[PR_SOURCE_KEY]); |
|
| 1067 | + $folderOrigin = DeviceManager::FLD_ORIGIN_USER; |
|
| 1068 | + if (GSync::GetBackend()->GetImpersonatedUser()) { |
|
| 1069 | + $folderOrigin = DeviceManager::FLD_ORIGIN_IMPERSONATED; |
|
| 1070 | + } |
|
| 1071 | + $folder->serverid = GSync::GetDeviceManager()->GetFolderIdForBackendId($folder->BackendId, true, $folderOrigin, $folderprops[PR_DISPLAY_NAME]); |
|
| 1072 | + if ($folderprops[PR_PARENT_ENTRYID] == $storeprops[PR_IPM_SUBTREE_ENTRYID] || $folderprops[PR_PARENT_ENTRYID] == $storeprops[PR_IPM_PUBLIC_FOLDERS_ENTRYID]) { |
|
| 1073 | + $folder->parentid = "0"; |
|
| 1074 | + } |
|
| 1075 | + else { |
|
| 1076 | + $folder->parentid = GSync::GetDeviceManager()->GetFolderIdForBackendId(bin2hex($folderprops[PR_PARENT_SOURCE_KEY])); |
|
| 1077 | + } |
|
| 1078 | + $folder->displayname = w2u($folderprops[PR_DISPLAY_NAME]); |
|
| 1079 | + $folder->type = $this->GetFolderType($folderprops[PR_ENTRYID], isset($folderprops[PR_CONTAINER_CLASS]) ? $folderprops[PR_CONTAINER_CLASS] : false); |
|
| 1080 | + |
|
| 1081 | + return $folder; |
|
| 1082 | + } |
|
| 1083 | + |
|
| 1084 | + /** |
|
| 1085 | + * Returns the foldertype for an entryid |
|
| 1086 | + * Gets the folder type by checking the default folders in MAPI. |
|
| 1087 | + * |
|
| 1088 | + * @param string $entryid |
|
| 1089 | + * @param string $class (opt) |
|
| 1090 | + * |
|
| 1091 | + * @return long |
|
| 1092 | + */ |
|
| 1093 | + public function GetFolderType($entryid, $class = false) { |
|
| 1094 | + $storeprops = $this->GetStoreProps(); |
|
| 1095 | + $inboxprops = $this->GetInboxProps(); |
|
| 1096 | + |
|
| 1097 | + if ($entryid == $storeprops[PR_IPM_WASTEBASKET_ENTRYID]) { |
|
| 1098 | + return SYNC_FOLDER_TYPE_WASTEBASKET; |
|
| 1099 | + } |
|
| 1100 | + if ($entryid == $storeprops[PR_IPM_SENTMAIL_ENTRYID]) { |
|
| 1101 | + return SYNC_FOLDER_TYPE_SENTMAIL; |
|
| 1102 | + } |
|
| 1103 | + if ($entryid == $storeprops[PR_IPM_OUTBOX_ENTRYID]) { |
|
| 1104 | + return SYNC_FOLDER_TYPE_OUTBOX; |
|
| 1105 | + } |
|
| 1106 | + |
|
| 1107 | + // Public folders do not have inboxprops |
|
| 1108 | + // @see https://jira.z-hub.io/browse/ZP-995 |
|
| 1109 | + if (!empty($inboxprops)) { |
|
| 1110 | + if ($entryid == $inboxprops[PR_ENTRYID]) { |
|
| 1111 | + return SYNC_FOLDER_TYPE_INBOX; |
|
| 1112 | + } |
|
| 1113 | + if ($entryid == $inboxprops[PR_IPM_DRAFTS_ENTRYID]) { |
|
| 1114 | + return SYNC_FOLDER_TYPE_DRAFTS; |
|
| 1115 | + } |
|
| 1116 | + if ($entryid == $inboxprops[PR_IPM_TASK_ENTRYID]) { |
|
| 1117 | + return SYNC_FOLDER_TYPE_TASK; |
|
| 1118 | + } |
|
| 1119 | + if ($entryid == $inboxprops[PR_IPM_APPOINTMENT_ENTRYID]) { |
|
| 1120 | + return SYNC_FOLDER_TYPE_APPOINTMENT; |
|
| 1121 | + } |
|
| 1122 | + if ($entryid == $inboxprops[PR_IPM_CONTACT_ENTRYID]) { |
|
| 1123 | + return SYNC_FOLDER_TYPE_CONTACT; |
|
| 1124 | + } |
|
| 1125 | + if ($entryid == $inboxprops[PR_IPM_NOTE_ENTRYID]) { |
|
| 1126 | + return SYNC_FOLDER_TYPE_NOTE; |
|
| 1127 | + } |
|
| 1128 | + if ($entryid == $inboxprops[PR_IPM_JOURNAL_ENTRYID]) { |
|
| 1129 | + return SYNC_FOLDER_TYPE_JOURNAL; |
|
| 1130 | + } |
|
| 1131 | + } |
|
| 1132 | + |
|
| 1133 | + // user created folders |
|
| 1134 | + if ($class == "IPF.Note") { |
|
| 1135 | + return SYNC_FOLDER_TYPE_USER_MAIL; |
|
| 1136 | + } |
|
| 1137 | + if ($class == "IPF.Task") { |
|
| 1138 | + return SYNC_FOLDER_TYPE_USER_TASK; |
|
| 1139 | + } |
|
| 1140 | + if ($class == "IPF.Appointment") { |
|
| 1141 | + return SYNC_FOLDER_TYPE_USER_APPOINTMENT; |
|
| 1142 | + } |
|
| 1143 | + if ($class == "IPF.Contact") { |
|
| 1144 | + return SYNC_FOLDER_TYPE_USER_CONTACT; |
|
| 1145 | + } |
|
| 1146 | + if ($class == "IPF.StickyNote") { |
|
| 1147 | + return SYNC_FOLDER_TYPE_USER_NOTE; |
|
| 1148 | + } |
|
| 1149 | + if ($class == "IPF.Journal") { |
|
| 1150 | + return SYNC_FOLDER_TYPE_USER_JOURNAL; |
|
| 1151 | + } |
|
| 1152 | + |
|
| 1153 | + return SYNC_FOLDER_TYPE_OTHER; |
|
| 1154 | + } |
|
| 1155 | + |
|
| 1156 | + /** |
|
| 1157 | + * Indicates if the entry id is a default MAPI folder. |
|
| 1158 | + * |
|
| 1159 | + * @param string $entryid |
|
| 1160 | + * |
|
| 1161 | + * @return bool |
|
| 1162 | + */ |
|
| 1163 | + public function IsMAPIDefaultFolder($entryid) { |
|
| 1164 | + $msgstore_props = mapi_getprops($this->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_MAILBOX_OWNER_ENTRYID]); |
|
| 1165 | + |
|
| 1166 | + $inboxProps = []; |
|
| 1167 | + $inbox = mapi_msgstore_getreceivefolder($this->store); |
|
| 1168 | + if (!mapi_last_hresult()) { |
|
| 1169 | + $inboxProps = mapi_getprops($inbox, [PR_ENTRYID]); |
|
| 1170 | + } |
|
| 1171 | + |
|
| 1172 | + $root = mapi_msgstore_openentry($this->store, null); // TODO use getRootProps() |
|
| 1173 | + $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]); |
|
| 1174 | + |
|
| 1175 | + $additional_ren_entryids = []; |
|
| 1176 | + if (isset($rootProps[PR_ADDITIONAL_REN_ENTRYIDS])) { |
|
| 1177 | + $additional_ren_entryids = $rootProps[PR_ADDITIONAL_REN_ENTRYIDS]; |
|
| 1178 | + } |
|
| 1179 | + |
|
| 1180 | + $defaultfolders = [ |
|
| 1181 | + "inbox" => ["inbox" => PR_ENTRYID], |
|
| 1182 | + "outbox" => ["store" => PR_IPM_OUTBOX_ENTRYID], |
|
| 1183 | + "sent" => ["store" => PR_IPM_SENTMAIL_ENTRYID], |
|
| 1184 | + "wastebasket" => ["store" => PR_IPM_WASTEBASKET_ENTRYID], |
|
| 1185 | + "favorites" => ["store" => PR_IPM_FAVORITES_ENTRYID], |
|
| 1186 | + "publicfolders" => ["store" => PR_IPM_PUBLIC_FOLDERS_ENTRYID], |
|
| 1187 | + "calendar" => ["root" => PR_IPM_APPOINTMENT_ENTRYID], |
|
| 1188 | + "contact" => ["root" => PR_IPM_CONTACT_ENTRYID], |
|
| 1189 | + "drafts" => ["root" => PR_IPM_DRAFTS_ENTRYID], |
|
| 1190 | + "journal" => ["root" => PR_IPM_JOURNAL_ENTRYID], |
|
| 1191 | + "note" => ["root" => PR_IPM_NOTE_ENTRYID], |
|
| 1192 | + "task" => ["root" => PR_IPM_TASK_ENTRYID], |
|
| 1193 | + "junk" => ["additional" => 4], |
|
| 1194 | + "syncissues" => ["additional" => 1], |
|
| 1195 | + "conflicts" => ["additional" => 0], |
|
| 1196 | + "localfailures" => ["additional" => 2], |
|
| 1197 | + "serverfailures" => ["additional" => 3], |
|
| 1198 | + ]; |
|
| 1199 | + |
|
| 1200 | + foreach ($defaultfolders as $key => $prop) { |
|
| 1201 | + $tag = reset($prop); |
|
| 1202 | + $from = key($prop); |
|
| 1203 | + |
|
| 1204 | + switch ($from) { |
|
| 1205 | + case "inbox": |
|
| 1206 | + if (isset($inboxProps[$tag]) && $entryid == $inboxProps[$tag]) { |
|
| 1207 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->IsMAPIFolder(): Inbox found, key '%s'", $key)); |
|
| 1208 | + |
|
| 1209 | + return true; |
|
| 1210 | + } |
|
| 1211 | + break; |
|
| 1212 | + |
|
| 1213 | + case "store": |
|
| 1214 | + if (isset($msgstore_props[$tag]) && $entryid == $msgstore_props[$tag]) { |
|
| 1215 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->IsMAPIFolder(): Store folder found, key '%s'", $key)); |
|
| 1216 | + |
|
| 1217 | + return true; |
|
| 1218 | + } |
|
| 1219 | + break; |
|
| 1220 | + |
|
| 1221 | + case "root": |
|
| 1222 | + if (isset($rootProps[$tag]) && $entryid == $rootProps[$tag]) { |
|
| 1223 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->IsMAPIFolder(): Root folder found, key '%s'", $key)); |
|
| 1224 | + |
|
| 1225 | + return true; |
|
| 1226 | + } |
|
| 1227 | + break; |
|
| 1228 | + |
|
| 1229 | + case "additional": |
|
| 1230 | + if (isset($additional_ren_entryids[$tag]) && $entryid == $additional_ren_entryids[$tag]) { |
|
| 1231 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->IsMAPIFolder(): Additional folder found, key '%s'", $key)); |
|
| 1232 | + |
|
| 1233 | + return true; |
|
| 1234 | + } |
|
| 1235 | + break; |
|
| 1236 | + } |
|
| 1237 | + } |
|
| 1238 | + |
|
| 1239 | + return false; |
|
| 1240 | + } |
|
| 1241 | + |
|
| 1242 | + /*---------------------------------------------------------------------------------------------------------- |
|
| 1243 | 1243 | * SETTER |
| 1244 | 1244 | */ |
| 1245 | 1245 | |
| 1246 | - /** |
|
| 1247 | - * Writes a SyncObject to MAPI |
|
| 1248 | - * Depending on the message class, a contact, appointment, task or email is written. |
|
| 1249 | - * |
|
| 1250 | - * @param mixed $mapimessage |
|
| 1251 | - * @param SyncObject $message |
|
| 1252 | - * |
|
| 1253 | - * @return bool |
|
| 1254 | - */ |
|
| 1255 | - public function SetMessage($mapimessage, $message) { |
|
| 1256 | - // TODO check with instanceof |
|
| 1257 | - switch (strtolower(get_class($message))) { |
|
| 1258 | - case "synccontact": |
|
| 1259 | - return $this->setContact($mapimessage, $message); |
|
| 1260 | - |
|
| 1261 | - case "syncappointment": |
|
| 1262 | - return $this->setAppointment($mapimessage, $message); |
|
| 1263 | - |
|
| 1264 | - case "synctask": |
|
| 1265 | - return $this->setTask($mapimessage, $message); |
|
| 1266 | - |
|
| 1267 | - case "syncnote": |
|
| 1268 | - return $this->setNote($mapimessage, $message); |
|
| 1269 | - |
|
| 1270 | - default: |
|
| 1271 | - // for emails only flag (read and todo) changes are possible |
|
| 1272 | - return $this->setEmail($mapimessage, $message); |
|
| 1273 | - } |
|
| 1274 | - } |
|
| 1275 | - |
|
| 1276 | - /** |
|
| 1277 | - * Writes SyncMail to MAPI (actually flags only). |
|
| 1278 | - * |
|
| 1279 | - * @param mixed $mapimessage |
|
| 1280 | - * @param SyncMail $message |
|
| 1281 | - */ |
|
| 1282 | - private function setEmail($mapimessage, $message) { |
|
| 1283 | - // update categories |
|
| 1284 | - if (!isset($message->categories)) { |
|
| 1285 | - $message->categories = []; |
|
| 1286 | - } |
|
| 1287 | - $emailmap = MAPIMapping::GetEmailMapping(); |
|
| 1288 | - $this->setPropsInMAPI($mapimessage, $message, ["categories" => $emailmap["categories"]]); |
|
| 1289 | - |
|
| 1290 | - $flagmapping = MAPIMapping::GetMailFlagsMapping(); |
|
| 1291 | - $flagprops = MAPIMapping::GetMailFlagsProperties(); |
|
| 1292 | - $flagprops = array_merge($this->getPropIdsFromStrings($flagmapping), $this->getPropIdsFromStrings($flagprops)); |
|
| 1293 | - // flag specific properties to be set |
|
| 1294 | - $props = $delprops = []; |
|
| 1295 | - // unset message flags if: |
|
| 1296 | - // flag is not set |
|
| 1297 | - if (empty($message->flag) || |
|
| 1298 | - // flag status is not set |
|
| 1299 | - !isset($message->flag->flagstatus) || |
|
| 1300 | - // flag status is 0 or empty |
|
| 1301 | - (isset($message->flag->flagstatus) && ($message->flag->flagstatus == 0 || $message->flag->flagstatus == ""))) { |
|
| 1302 | - // if message flag is empty, some properties need to be deleted |
|
| 1303 | - // and some set to 0 or false |
|
| 1304 | - |
|
| 1305 | - $props[$flagprops["todoitemsflags"]] = 0; |
|
| 1306 | - $props[$flagprops["status"]] = 0; |
|
| 1307 | - $props[$flagprops["completion"]] = 0.0; |
|
| 1308 | - $props[$flagprops["flagtype"]] = ""; |
|
| 1309 | - $props[$flagprops["ordinaldate"]] = 0x7FFFFFFF; // ordinal date is 12am 1.1.4501, set it to max possible value |
|
| 1310 | - $props[$flagprops["subordinaldate"]] = ""; |
|
| 1311 | - $props[$flagprops["replyrequested"]] = false; |
|
| 1312 | - $props[$flagprops["responserequested"]] = false; |
|
| 1313 | - $props[$flagprops["reminderset"]] = false; |
|
| 1314 | - $props[$flagprops["complete"]] = false; |
|
| 1315 | - |
|
| 1316 | - $delprops[] = $flagprops["todotitle"]; |
|
| 1317 | - $delprops[] = $flagprops["duedate"]; |
|
| 1318 | - $delprops[] = $flagprops["startdate"]; |
|
| 1319 | - $delprops[] = $flagprops["datecompleted"]; |
|
| 1320 | - $delprops[] = $flagprops["utcstartdate"]; |
|
| 1321 | - $delprops[] = $flagprops["utcduedate"]; |
|
| 1322 | - $delprops[] = $flagprops["completetime"]; |
|
| 1323 | - $delprops[] = $flagprops["flagstatus"]; |
|
| 1324 | - $delprops[] = $flagprops["flagicon"]; |
|
| 1325 | - } |
|
| 1326 | - else { |
|
| 1327 | - $this->setPropsInMAPI($mapimessage, $message->flag, $flagmapping); |
|
| 1328 | - $props[$flagprops["todoitemsflags"]] = 1; |
|
| 1329 | - if (isset($message->subject) && strlen($message->subject) > 0) { |
|
| 1330 | - $props[$flagprops["todotitle"]] = $message->subject; |
|
| 1331 | - } |
|
| 1332 | - // ordinal date is utc current time |
|
| 1333 | - if (!isset($message->flag->ordinaldate) || empty($message->flag->ordinaldate)) { |
|
| 1334 | - $props[$flagprops["ordinaldate"]] = time(); |
|
| 1335 | - } |
|
| 1336 | - // the default value |
|
| 1337 | - if (!isset($message->flag->subordinaldate) || empty($message->flag->subordinaldate)) { |
|
| 1338 | - $props[$flagprops["subordinaldate"]] = "5555555"; |
|
| 1339 | - } |
|
| 1340 | - $props[$flagprops["flagicon"]] = 6; // red flag icon |
|
| 1341 | - $props[$flagprops["replyrequested"]] = true; |
|
| 1342 | - $props[$flagprops["responserequested"]] = true; |
|
| 1343 | - |
|
| 1344 | - if ($message->flag->flagstatus == SYNC_FLAGSTATUS_COMPLETE) { |
|
| 1345 | - $props[$flagprops["status"]] = olTaskComplete; |
|
| 1346 | - $props[$flagprops["completion"]] = 1.0; |
|
| 1347 | - $props[$flagprops["complete"]] = true; |
|
| 1348 | - $props[$flagprops["replyrequested"]] = false; |
|
| 1349 | - $props[$flagprops["responserequested"]] = false; |
|
| 1350 | - unset($props[$flagprops["flagicon"]]); |
|
| 1351 | - $delprops[] = $flagprops["flagicon"]; |
|
| 1352 | - } |
|
| 1353 | - } |
|
| 1354 | - |
|
| 1355 | - if (!empty($props)) { |
|
| 1356 | - mapi_setprops($mapimessage, $props); |
|
| 1357 | - } |
|
| 1358 | - if (!empty($delprops)) { |
|
| 1359 | - mapi_deleteprops($mapimessage, $delprops); |
|
| 1360 | - } |
|
| 1361 | - } |
|
| 1362 | - |
|
| 1363 | - /** |
|
| 1364 | - * Writes a SyncAppointment to MAPI. |
|
| 1365 | - * |
|
| 1366 | - * @param mixed $mapimessage |
|
| 1367 | - * @param SyncAppointment $message |
|
| 1368 | - * @param mixed $appointment |
|
| 1369 | - * |
|
| 1370 | - * @return bool |
|
| 1371 | - */ |
|
| 1372 | - private function setAppointment($mapimessage, $appointment) { |
|
| 1373 | - // Get timezone info |
|
| 1374 | - if (isset($appointment->timezone)) { |
|
| 1375 | - $tz = $this->getTZFromSyncBlob(base64_decode($appointment->timezone)); |
|
| 1376 | - } |
|
| 1377 | - else { |
|
| 1378 | - $tz = false; |
|
| 1379 | - } |
|
| 1380 | - |
|
| 1381 | - // start and end time may not be set - try to get them from the existing appointment for further calculation - see https://jira.z-hub.io/browse/ZP-983 |
|
| 1382 | - if (!isset($appointment->starttime) || !isset($appointment->endtime)) { |
|
| 1383 | - $amapping = MAPIMapping::GetAppointmentMapping(); |
|
| 1384 | - $amapping = $this->getPropIdsFromStrings($amapping); |
|
| 1385 | - $existingstartendpropsmap = [$amapping["starttime"], $amapping["endtime"]]; |
|
| 1386 | - $existingstartendprops = $this->getProps($mapimessage, $existingstartendpropsmap); |
|
| 1387 | - |
|
| 1388 | - if (isset($existingstartendprops[$amapping["starttime"]]) && !isset($appointment->starttime)) { |
|
| 1389 | - $appointment->starttime = $existingstartendprops[$amapping["starttime"]]; |
|
| 1390 | - SLog::Write(LOGLEVEL_WBXML, sprintf("MAPIProvider->setAppointment(): Parameter 'starttime' was not set, using value from MAPI %d (%s).", $appointment->starttime, gmstrftime("%Y%m%dT%H%M%SZ", $appointment->starttime))); |
|
| 1391 | - } |
|
| 1392 | - if (isset($existingstartendprops[$amapping["endtime"]]) && !isset($appointment->endtime)) { |
|
| 1393 | - $appointment->endtime = $existingstartendprops[$amapping["endtime"]]; |
|
| 1394 | - SLog::Write(LOGLEVEL_WBXML, sprintf("MAPIProvider->setAppointment(): Parameter 'endtime' was not set, using value from MAPI %d (%s).", $appointment->endtime, gmstrftime("%Y%m%dT%H%M%SZ", $appointment->endtime))); |
|
| 1395 | - } |
|
| 1396 | - } |
|
| 1397 | - if (!isset($appointment->starttime) || !isset($appointment->endtime)) { |
|
| 1398 | - throw new StatusException("MAPIProvider->setAppointment(): Error, start and/or end time not set and can not be retrieved from MAPI.", SYNC_STATUS_SYNCCANNOTBECOMPLETED); |
|
| 1399 | - } |
|
| 1400 | - |
|
| 1401 | - // calculate duration because without it some webaccess views are broken. duration is in min |
|
| 1402 | - $localstart = $this->getLocaltimeByTZ($appointment->starttime, $tz); |
|
| 1403 | - $localend = $this->getLocaltimeByTZ($appointment->endtime, $tz); |
|
| 1404 | - $duration = ($localend - $localstart) / 60; |
|
| 1405 | - |
|
| 1406 | - // nokia sends an yearly event with 0 mins duration but as all day event, |
|
| 1407 | - // so make it end next day |
|
| 1408 | - if ($appointment->starttime == $appointment->endtime && isset($appointment->alldayevent) && $appointment->alldayevent) { |
|
| 1409 | - $duration = 1440; |
|
| 1410 | - $appointment->endtime = $appointment->starttime + 24 * 60 * 60; |
|
| 1411 | - $localend = $localstart + 24 * 60 * 60; |
|
| 1412 | - } |
|
| 1413 | - |
|
| 1414 | - // is the transmitted UID OL compatible? |
|
| 1415 | - // if not, encapsulate the transmitted uid |
|
| 1416 | - $appointment->uid = Utils::GetOLUidFromICalUid($appointment->uid); |
|
| 1417 | - |
|
| 1418 | - mapi_setprops($mapimessage, [PR_MESSAGE_CLASS => "IPM.Appointment"]); |
|
| 1419 | - |
|
| 1420 | - $appointmentmapping = MAPIMapping::GetAppointmentMapping(); |
|
| 1421 | - $this->setPropsInMAPI($mapimessage, $appointment, $appointmentmapping); |
|
| 1422 | - $appointmentprops = MAPIMapping::GetAppointmentProperties(); |
|
| 1423 | - $appointmentprops = array_merge($this->getPropIdsFromStrings($appointmentmapping), $this->getPropIdsFromStrings($appointmentprops)); |
|
| 1424 | - // appointment specific properties to be set |
|
| 1425 | - $props = []; |
|
| 1426 | - |
|
| 1427 | - // sensitivity is not enough to mark an appointment as private, so we use another mapi tag |
|
| 1428 | - $private = (isset($appointment->sensitivity) && $appointment->sensitivity >= SENSITIVITY_PRIVATE) ? true : false; |
|
| 1429 | - |
|
| 1430 | - // Set commonstart/commonend to start/end and remindertime to start, duration, private and cleanGlobalObjectId |
|
| 1431 | - $props[$appointmentprops["commonstart"]] = $appointment->starttime; |
|
| 1432 | - $props[$appointmentprops["commonend"]] = $appointment->endtime; |
|
| 1433 | - $props[$appointmentprops["reminderstart"]] = $appointment->starttime; |
|
| 1434 | - // Set reminder boolean to 'true' if reminder is set |
|
| 1435 | - $props[$appointmentprops["reminderset"]] = isset($appointment->reminder) ? true : false; |
|
| 1436 | - $props[$appointmentprops["duration"]] = $duration; |
|
| 1437 | - $props[$appointmentprops["private"]] = $private; |
|
| 1438 | - $props[$appointmentprops["uid"]] = $appointment->uid; |
|
| 1439 | - // Set named prop 8510, unknown property, but enables deleting a single occurrence of a recurring |
|
| 1440 | - // type in OLK2003. |
|
| 1441 | - $props[$appointmentprops["sideeffects"]] = 369; |
|
| 1442 | - |
|
| 1443 | - if (isset($appointment->reminder) && $appointment->reminder >= 0) { |
|
| 1444 | - // Set 'flagdueby' to correct value (start - reminderminutes) |
|
| 1445 | - $props[$appointmentprops["flagdueby"]] = $appointment->starttime - $appointment->reminder * 60; |
|
| 1446 | - $props[$appointmentprops["remindertime"]] = $appointment->reminder; |
|
| 1447 | - } |
|
| 1448 | - // unset the reminder |
|
| 1449 | - else { |
|
| 1450 | - $props[$appointmentprops["reminderset"]] = false; |
|
| 1451 | - } |
|
| 1452 | - |
|
| 1453 | - if (isset($appointment->asbody)) { |
|
| 1454 | - $this->setASbody($appointment->asbody, $props, $appointmentprops); |
|
| 1455 | - } |
|
| 1456 | - |
|
| 1457 | - if ($tz !== false) { |
|
| 1458 | - $props[$appointmentprops["timezonetag"]] = $this->getMAPIBlobFromTZ($tz); |
|
| 1459 | - } |
|
| 1460 | - |
|
| 1461 | - if (isset($appointment->recurrence)) { |
|
| 1462 | - // Set PR_ICON_INDEX to 1025 to show correct icon in category view |
|
| 1463 | - $props[$appointmentprops["icon"]] = 1025; |
|
| 1464 | - |
|
| 1465 | - // if there aren't any exceptions, use the 'old style' set recurrence |
|
| 1466 | - $noexceptions = true; |
|
| 1467 | - |
|
| 1468 | - $recurrence = new Recurrence($this->store, $mapimessage); |
|
| 1469 | - $recur = []; |
|
| 1470 | - $this->setRecurrence($appointment, $recur); |
|
| 1471 | - |
|
| 1472 | - // set the recurrence type to that of the MAPI |
|
| 1473 | - $props[$appointmentprops["recurrencetype"]] = $recur["recurrencetype"]; |
|
| 1474 | - |
|
| 1475 | - $starttime = $this->gmtime($localstart); |
|
| 1476 | - $endtime = $this->gmtime($localend); |
|
| 1477 | - |
|
| 1478 | - // set recurrence start here because it's calculated differently for tasks and appointments |
|
| 1479 | - $recur["start"] = $this->getDayStartOfTimestamp($this->getGMTTimeByTZ($localstart, $tz)); |
|
| 1480 | - |
|
| 1481 | - $recur["startocc"] = $starttime["tm_hour"] * 60 + $starttime["tm_min"]; |
|
| 1482 | - $recur["endocc"] = $recur["startocc"] + $duration; // Note that this may be > 24*60 if multi-day |
|
| 1483 | - |
|
| 1484 | - // only tasks can regenerate |
|
| 1485 | - $recur["regen"] = false; |
|
| 1486 | - |
|
| 1487 | - // Process exceptions. The PDA will send all exceptions for this recurring item. |
|
| 1488 | - if (isset($appointment->exceptions)) { |
|
| 1489 | - foreach ($appointment->exceptions as $exception) { |
|
| 1490 | - // we always need the base date |
|
| 1491 | - if (!isset($exception->exceptionstarttime)) { |
|
| 1492 | - continue; |
|
| 1493 | - } |
|
| 1494 | - |
|
| 1495 | - $basedate = $this->getDayStartOfTimestamp($exception->exceptionstarttime); |
|
| 1496 | - if (isset($exception->deleted) && $exception->deleted) { |
|
| 1497 | - $noexceptions = false; |
|
| 1498 | - // Delete exception |
|
| 1499 | - $recurrence->createException([], $basedate, true); |
|
| 1500 | - } |
|
| 1501 | - else { |
|
| 1502 | - // Change exception |
|
| 1503 | - $mapiexception = ["basedate" => $basedate]; |
|
| 1504 | - // other exception properties which are not handled in recurrence |
|
| 1505 | - $exceptionprops = []; |
|
| 1506 | - |
|
| 1507 | - if (isset($exception->starttime)) { |
|
| 1508 | - $mapiexception["start"] = $this->getLocaltimeByTZ($exception->starttime, $tz); |
|
| 1509 | - $exceptionprops[$appointmentprops["starttime"]] = $exception->starttime; |
|
| 1510 | - } |
|
| 1511 | - if (isset($exception->endtime)) { |
|
| 1512 | - $mapiexception["end"] = $this->getLocaltimeByTZ($exception->endtime, $tz); |
|
| 1513 | - $exceptionprops[$appointmentprops["endtime"]] = $exception->endtime; |
|
| 1514 | - } |
|
| 1515 | - if (isset($exception->subject)) { |
|
| 1516 | - $exceptionprops[$appointmentprops["subject"]] = $mapiexception["subject"] = u2w($exception->subject); |
|
| 1517 | - } |
|
| 1518 | - if (isset($exception->location)) { |
|
| 1519 | - $exceptionprops[$appointmentprops["location"]] = $mapiexception["location"] = u2w($exception->location); |
|
| 1520 | - } |
|
| 1521 | - if (isset($exception->busystatus)) { |
|
| 1522 | - $exceptionprops[$appointmentprops["busystatus"]] = $mapiexception["busystatus"] = $exception->busystatus; |
|
| 1523 | - } |
|
| 1524 | - if (isset($exception->reminder)) { |
|
| 1525 | - $exceptionprops[$appointmentprops["reminderset"]] = $mapiexception["reminder_set"] = 1; |
|
| 1526 | - $exceptionprops[$appointmentprops["remindertime"]] = $mapiexception["remind_before"] = $exception->reminder; |
|
| 1527 | - } |
|
| 1528 | - if (isset($exception->alldayevent)) { |
|
| 1529 | - $exceptionprops[$appointmentprops["alldayevent"]] = $mapiexception["alldayevent"] = $exception->alldayevent; |
|
| 1530 | - } |
|
| 1531 | - |
|
| 1532 | - if (!isset($recur["changed_occurrences"])) { |
|
| 1533 | - $recur["changed_occurrences"] = []; |
|
| 1534 | - } |
|
| 1535 | - |
|
| 1536 | - if (isset($exception->body)) { |
|
| 1537 | - $exceptionprops[$appointmentprops["body"]] = u2w($exception->body); |
|
| 1538 | - } |
|
| 1539 | - |
|
| 1540 | - if (isset($exception->asbody)) { |
|
| 1541 | - $this->setASbody($exception->asbody, $exceptionprops, $appointmentprops); |
|
| 1542 | - $mapiexception["body"] = $exceptionprops[$appointmentprops["body"]] = |
|
| 1543 | - (isset($exceptionprops[$appointmentprops["body"]])) ? $exceptionprops[$appointmentprops["body"]] : |
|
| 1544 | - ((isset($exceptionprops[$appointmentprops["html"]])) ? $exceptionprops[$appointmentprops["html"]] : ""); |
|
| 1545 | - } |
|
| 1546 | - |
|
| 1547 | - array_push($recur["changed_occurrences"], $mapiexception); |
|
| 1548 | - |
|
| 1549 | - if (!empty($exceptionprops)) { |
|
| 1550 | - $noexceptions = false; |
|
| 1551 | - if ($recurrence->isException($basedate)) { |
|
| 1552 | - $recurrence->modifyException($exceptionprops, $basedate); |
|
| 1553 | - } |
|
| 1554 | - else { |
|
| 1555 | - $recurrence->createException($exceptionprops, $basedate); |
|
| 1556 | - } |
|
| 1557 | - } |
|
| 1558 | - } |
|
| 1559 | - } |
|
| 1560 | - } |
|
| 1561 | - |
|
| 1562 | - // setRecurrence deletes the attachments from an appointment |
|
| 1563 | - if ($noexceptions) { |
|
| 1564 | - $recurrence->setRecurrence($tz, $recur); |
|
| 1565 | - } |
|
| 1566 | - } |
|
| 1567 | - else { |
|
| 1568 | - $props[$appointmentprops["isrecurring"]] = false; |
|
| 1569 | - } |
|
| 1570 | - |
|
| 1571 | - // always set the PR_SENT_REPRESENTING_* props so that the attendee status update also works with the webaccess |
|
| 1572 | - $p = [$appointmentprops["representingentryid"], $appointmentprops["representingname"], $appointmentprops["sentrepresentingaddt"], |
|
| 1573 | - $appointmentprops["sentrepresentingemail"], $appointmentprops["sentrepresentinsrchk"], $appointmentprops["responsestatus"], ]; |
|
| 1574 | - $representingprops = $this->getProps($mapimessage, $p); |
|
| 1575 | - |
|
| 1576 | - if (!isset($representingprops[$appointmentprops["representingentryid"]])) { |
|
| 1577 | - // TODO use GetStoreProps |
|
| 1578 | - $storeProps = mapi_getprops($this->store, [PR_MAILBOX_OWNER_ENTRYID]); |
|
| 1579 | - $props[$appointmentprops["representingentryid"]] = $storeProps[PR_MAILBOX_OWNER_ENTRYID]; |
|
| 1580 | - $displayname = $this->getFullnameFromEntryID($storeProps[PR_MAILBOX_OWNER_ENTRYID]); |
|
| 1581 | - |
|
| 1582 | - $props[$appointmentprops["representingname"]] = ($displayname !== false) ? $displayname : Request::GetUser(); |
|
| 1583 | - $props[$appointmentprops["sentrepresentingemail"]] = Request::GetUser(); |
|
| 1584 | - $props[$appointmentprops["sentrepresentingaddt"]] = "ZARAFA"; |
|
| 1585 | - $props[$appointmentprops["sentrepresentinsrchk"]] = $props[$appointmentprops["sentrepresentingaddt"]] . ":" . $props[$appointmentprops["sentrepresentingemail"]]; |
|
| 1586 | - |
|
| 1587 | - if (isset($appointment->attendees) && is_array($appointment->attendees) && !empty($appointment->attendees)) { |
|
| 1588 | - $props[$appointmentprops["icon"]] = 1026; |
|
| 1589 | - // the user is the organizer |
|
| 1590 | - // set these properties to show tracking tab in webapp |
|
| 1591 | - |
|
| 1592 | - $props[$appointmentprops["mrwassent"]] = true; |
|
| 1593 | - $props[$appointmentprops["responsestatus"]] = olResponseOrganized; |
|
| 1594 | - $props[$appointmentprops["meetingstatus"]] = olMeeting; |
|
| 1595 | - } |
|
| 1596 | - } |
|
| 1597 | - // we also have to set the responsestatus and not only meetingstatus, so we use another mapi tag |
|
| 1598 | - if (!isset($props[$appointmentprops["responsestatus"]])) { |
|
| 1599 | - if (isset($appointment->responsetype)) { |
|
| 1600 | - $props[$appointmentprops["responsestatus"]] = $appointment->responsetype; |
|
| 1601 | - } |
|
| 1602 | - // only set responsestatus to none if it is not set on the server |
|
| 1603 | - elseif (!isset($representingprops[$appointmentprops["responsestatus"]])) { |
|
| 1604 | - $props[$appointmentprops["responsestatus"]] = olResponseNone; |
|
| 1605 | - } |
|
| 1606 | - } |
|
| 1607 | - |
|
| 1608 | - // Do attendees |
|
| 1609 | - if (isset($appointment->attendees) && is_array($appointment->attendees)) { |
|
| 1610 | - $recips = []; |
|
| 1611 | - |
|
| 1612 | - // Outlook XP requires organizer in the attendee list as well |
|
| 1613 | - $org = []; |
|
| 1614 | - $org[PR_ENTRYID] = isset($representingprops[$appointmentprops["representingentryid"]]) ? $representingprops[$appointmentprops["representingentryid"]] : $props[$appointmentprops["representingentryid"]]; |
|
| 1615 | - $org[PR_DISPLAY_NAME] = isset($representingprops[$appointmentprops["representingname"]]) ? $representingprops[$appointmentprops["representingname"]] : $props[$appointmentprops["representingname"]]; |
|
| 1616 | - $org[PR_ADDRTYPE] = isset($representingprops[$appointmentprops["sentrepresentingaddt"]]) ? $representingprops[$appointmentprops["sentrepresentingaddt"]] : $props[$appointmentprops["sentrepresentingaddt"]]; |
|
| 1617 | - $org[PR_SMTP_ADDRESS] = $org[PR_EMAIL_ADDRESS] = isset($representingprops[$appointmentprops["sentrepresentingemail"]]) ? $representingprops[$appointmentprops["sentrepresentingemail"]] : $props[$appointmentprops["sentrepresentingemail"]]; |
|
| 1618 | - $org[PR_SEARCH_KEY] = isset($representingprops[$appointmentprops["sentrepresentinsrchk"]]) ? $representingprops[$appointmentprops["sentrepresentinsrchk"]] : $props[$appointmentprops["sentrepresentinsrchk"]]; |
|
| 1619 | - $org[PR_RECIPIENT_FLAGS] = recipOrganizer | recipSendable; |
|
| 1620 | - $org[PR_RECIPIENT_TYPE] = MAPI_ORIG; |
|
| 1621 | - |
|
| 1622 | - array_push($recips, $org); |
|
| 1623 | - |
|
| 1624 | - // Open address book for user resolve |
|
| 1625 | - $addrbook = $this->getAddressbook(); |
|
| 1626 | - foreach ($appointment->attendees as $attendee) { |
|
| 1627 | - $recip = []; |
|
| 1628 | - $recip[PR_EMAIL_ADDRESS] = u2w($attendee->email); |
|
| 1629 | - $recip[PR_SMTP_ADDRESS] = u2w($attendee->email); |
|
| 1630 | - |
|
| 1631 | - // lookup information in GAB if possible so we have up-to-date name for given address |
|
| 1632 | - $userinfo = [[PR_DISPLAY_NAME => $recip[PR_EMAIL_ADDRESS]]]; |
|
| 1633 | - $userinfo = mapi_ab_resolvename($addrbook, $userinfo, EMS_AB_ADDRESS_LOOKUP); |
|
| 1634 | - if (mapi_last_hresult() == NOERROR) { |
|
| 1635 | - $recip[PR_DISPLAY_NAME] = $userinfo[0][PR_DISPLAY_NAME]; |
|
| 1636 | - $recip[PR_EMAIL_ADDRESS] = $userinfo[0][PR_EMAIL_ADDRESS]; |
|
| 1637 | - $recip[PR_SEARCH_KEY] = $userinfo[0][PR_SEARCH_KEY]; |
|
| 1638 | - $recip[PR_ADDRTYPE] = $userinfo[0][PR_ADDRTYPE]; |
|
| 1639 | - $recip[PR_ENTRYID] = $userinfo[0][PR_ENTRYID]; |
|
| 1640 | - $recip[PR_RECIPIENT_TYPE] = isset($attendee->attendeetype) ? $attendee->attendeetype : MAPI_TO; |
|
| 1641 | - $recip[PR_RECIPIENT_FLAGS] = recipSendable; |
|
| 1642 | - $recip[PR_RECIPIENT_TRACKSTATUS] = isset($attendee->attendeestatus) ? $attendee->attendeestatus : olResponseNone; |
|
| 1643 | - } |
|
| 1644 | - else { |
|
| 1645 | - $recip[PR_DISPLAY_NAME] = u2w($attendee->name); |
|
| 1646 | - $recip[PR_SEARCH_KEY] = "SMTP:" . $recip[PR_EMAIL_ADDRESS] . "\0"; |
|
| 1647 | - $recip[PR_ADDRTYPE] = "SMTP"; |
|
| 1648 | - $recip[PR_RECIPIENT_TYPE] = isset($attendee->attendeetype) ? $attendee->attendeetype : MAPI_TO; |
|
| 1649 | - $recip[PR_ENTRYID] = mapi_createoneoff($recip[PR_DISPLAY_NAME], $recip[PR_ADDRTYPE], $recip[PR_EMAIL_ADDRESS]); |
|
| 1650 | - } |
|
| 1651 | - |
|
| 1652 | - array_push($recips, $recip); |
|
| 1653 | - } |
|
| 1654 | - |
|
| 1655 | - mapi_message_modifyrecipients($mapimessage, 0, $recips); |
|
| 1656 | - } |
|
| 1657 | - mapi_setprops($mapimessage, $props); |
|
| 1658 | - |
|
| 1659 | - return true; |
|
| 1660 | - } |
|
| 1661 | - |
|
| 1662 | - /** |
|
| 1663 | - * Writes a SyncContact to MAPI. |
|
| 1664 | - * |
|
| 1665 | - * @param mixed $mapimessage |
|
| 1666 | - * @param SyncContact $contact |
|
| 1667 | - * |
|
| 1668 | - * @return bool |
|
| 1669 | - */ |
|
| 1670 | - private function setContact($mapimessage, $contact) { |
|
| 1671 | - mapi_setprops($mapimessage, [PR_MESSAGE_CLASS => "IPM.Contact"]); |
|
| 1672 | - |
|
| 1673 | - // normalize email addresses |
|
| 1674 | - if (isset($contact->email1address) && (($contact->email1address = $this->extractEmailAddress($contact->email1address)) === false)) { |
|
| 1675 | - unset($contact->email1address); |
|
| 1676 | - } |
|
| 1677 | - |
|
| 1678 | - if (isset($contact->email2address) && (($contact->email2address = $this->extractEmailAddress($contact->email2address)) === false)) { |
|
| 1679 | - unset($contact->email2address); |
|
| 1680 | - } |
|
| 1681 | - |
|
| 1682 | - if (isset($contact->email3address) && (($contact->email3address = $this->extractEmailAddress($contact->email3address)) === false)) { |
|
| 1683 | - unset($contact->email3address); |
|
| 1684 | - } |
|
| 1685 | - |
|
| 1686 | - $contactmapping = MAPIMapping::GetContactMapping(); |
|
| 1687 | - $contactprops = MAPIMapping::GetContactProperties(); |
|
| 1688 | - $this->setPropsInMAPI($mapimessage, $contact, $contactmapping); |
|
| 1689 | - |
|
| 1690 | - // /set display name from contact's properties |
|
| 1691 | - $cname = $this->composeDisplayName($contact); |
|
| 1692 | - |
|
| 1693 | - // get contact specific mapi properties and merge them with the AS properties |
|
| 1694 | - $contactprops = array_merge($this->getPropIdsFromStrings($contactmapping), $this->getPropIdsFromStrings($contactprops)); |
|
| 1695 | - |
|
| 1696 | - // contact specific properties to be set |
|
| 1697 | - $props = []; |
|
| 1698 | - |
|
| 1699 | - // need to be set in order to show contacts properly in outlook and wa |
|
| 1700 | - $nremails = []; |
|
| 1701 | - $abprovidertype = 0; |
|
| 1702 | - |
|
| 1703 | - if (isset($contact->email1address)) { |
|
| 1704 | - $this->setEmailAddress($contact->email1address, $cname, 1, $props, $contactprops, $nremails, $abprovidertype); |
|
| 1705 | - } |
|
| 1706 | - if (isset($contact->email2address)) { |
|
| 1707 | - $this->setEmailAddress($contact->email2address, $cname, 2, $props, $contactprops, $nremails, $abprovidertype); |
|
| 1708 | - } |
|
| 1709 | - if (isset($contact->email3address)) { |
|
| 1710 | - $this->setEmailAddress($contact->email3address, $cname, 3, $props, $contactprops, $nremails, $abprovidertype); |
|
| 1711 | - } |
|
| 1712 | - |
|
| 1713 | - $props[$contactprops["addressbooklong"]] = $abprovidertype; |
|
| 1714 | - $props[$contactprops["displayname"]] = $props[$contactprops["subject"]] = $cname; |
|
| 1715 | - |
|
| 1716 | - // pda multiple e-mail addresses bug fix for the contact |
|
| 1717 | - if (!empty($nremails)) { |
|
| 1718 | - $props[$contactprops["addressbookmv"]] = $nremails; |
|
| 1719 | - } |
|
| 1720 | - |
|
| 1721 | - // set addresses |
|
| 1722 | - $this->setAddress("home", $contact->homecity, $contact->homecountry, $contact->homepostalcode, $contact->homestate, $contact->homestreet, $props, $contactprops); |
|
| 1723 | - $this->setAddress("business", $contact->businesscity, $contact->businesscountry, $contact->businesspostalcode, $contact->businessstate, $contact->businessstreet, $props, $contactprops); |
|
| 1724 | - $this->setAddress("other", $contact->othercity, $contact->othercountry, $contact->otherpostalcode, $contact->otherstate, $contact->otherstreet, $props, $contactprops); |
|
| 1725 | - |
|
| 1726 | - // set the mailing address and its type |
|
| 1727 | - if (isset($props[$contactprops["businessaddress"]])) { |
|
| 1728 | - $props[$contactprops["mailingaddress"]] = 2; |
|
| 1729 | - $this->setMailingAddress($contact->businesscity, $contact->businesscountry, $contact->businesspostalcode, $contact->businessstate, $contact->businessstreet, $props[$contactprops["businessaddress"]], $props, $contactprops); |
|
| 1730 | - } |
|
| 1731 | - elseif (isset($props[$contactprops["homeaddress"]])) { |
|
| 1732 | - $props[$contactprops["mailingaddress"]] = 1; |
|
| 1733 | - $this->setMailingAddress($contact->homecity, $contact->homecountry, $contact->homepostalcode, $contact->homestate, $contact->homestreet, $props[$contactprops["homeaddress"]], $props, $contactprops); |
|
| 1734 | - } |
|
| 1735 | - elseif (isset($props[$contactprops["otheraddress"]])) { |
|
| 1736 | - $props[$contactprops["mailingaddress"]] = 3; |
|
| 1737 | - $this->setMailingAddress($contact->othercity, $contact->othercountry, $contact->otherpostalcode, $contact->otherstate, $contact->otherstreet, $props[$contactprops["otheraddress"]], $props, $contactprops); |
|
| 1738 | - } |
|
| 1739 | - |
|
| 1740 | - if (isset($contact->picture)) { |
|
| 1741 | - $picbinary = base64_decode($contact->picture); |
|
| 1742 | - $picsize = strlen($picbinary); |
|
| 1743 | - $props[$contactprops["haspic"]] = false; |
|
| 1744 | - |
|
| 1745 | - // TODO contact picture handling |
|
| 1746 | - // check if contact has already got a picture. delete it first in that case |
|
| 1747 | - // delete it also if it was removed on a mobile |
|
| 1748 | - $picprops = mapi_getprops($mapimessage, [$contactprops["haspic"]]); |
|
| 1749 | - if (isset($picprops[$contactprops["haspic"]]) && $picprops[$contactprops["haspic"]]) { |
|
| 1750 | - SLog::Write(LOGLEVEL_DEBUG, "Contact already has a picture. Delete it"); |
|
| 1751 | - |
|
| 1752 | - $attachtable = mapi_message_getattachmenttable($mapimessage); |
|
| 1753 | - mapi_table_restrict($attachtable, MAPIUtils::GetContactPicRestriction()); |
|
| 1754 | - $rows = mapi_table_queryallrows($attachtable, [PR_ATTACH_NUM]); |
|
| 1755 | - if (isset($rows) && is_array($rows)) { |
|
| 1756 | - foreach ($rows as $row) { |
|
| 1757 | - mapi_message_deleteattach($mapimessage, $row[PR_ATTACH_NUM]); |
|
| 1758 | - } |
|
| 1759 | - } |
|
| 1760 | - } |
|
| 1761 | - |
|
| 1762 | - // only set picture if there's data in the request |
|
| 1763 | - if ($picbinary !== false && $picsize > 0) { |
|
| 1764 | - $props[$contactprops["haspic"]] = true; |
|
| 1765 | - $pic = mapi_message_createattach($mapimessage); |
|
| 1766 | - // Set properties of the attachment |
|
| 1767 | - $picprops = [ |
|
| 1768 | - PR_ATTACH_LONG_FILENAME => "ContactPicture.jpg", |
|
| 1769 | - PR_DISPLAY_NAME => "ContactPicture.jpg", |
|
| 1770 | - 0x7FFF000B => true, |
|
| 1771 | - PR_ATTACHMENT_HIDDEN => false, |
|
| 1772 | - PR_ATTACHMENT_FLAGS => 1, |
|
| 1773 | - PR_ATTACH_METHOD => ATTACH_BY_VALUE, |
|
| 1774 | - PR_ATTACH_EXTENSION => ".jpg", |
|
| 1775 | - PR_ATTACH_NUM => 1, |
|
| 1776 | - PR_ATTACH_SIZE => $picsize, |
|
| 1777 | - PR_ATTACH_DATA_BIN => $picbinary, |
|
| 1778 | - ]; |
|
| 1779 | - |
|
| 1780 | - mapi_setprops($pic, $picprops); |
|
| 1781 | - mapi_savechanges($pic); |
|
| 1782 | - } |
|
| 1783 | - } |
|
| 1784 | - |
|
| 1785 | - if (isset($contact->asbody)) { |
|
| 1786 | - $this->setASbody($contact->asbody, $props, $contactprops); |
|
| 1787 | - } |
|
| 1788 | - |
|
| 1789 | - // set fileas |
|
| 1790 | - if (defined('FILEAS_ORDER')) { |
|
| 1791 | - $lastname = (isset($contact->lastname)) ? $contact->lastname : ""; |
|
| 1792 | - $firstname = (isset($contact->firstname)) ? $contact->firstname : ""; |
|
| 1793 | - $middlename = (isset($contact->middlename)) ? $contact->middlename : ""; |
|
| 1794 | - $company = (isset($contact->companyname)) ? $contact->companyname : ""; |
|
| 1795 | - $props[$contactprops["fileas"]] = Utils::BuildFileAs($lastname, $firstname, $middlename, $company); |
|
| 1796 | - } |
|
| 1797 | - else { |
|
| 1798 | - SLog::Write(LOGLEVEL_DEBUG, "FILEAS_ORDER not defined"); |
|
| 1799 | - } |
|
| 1800 | - |
|
| 1801 | - mapi_setprops($mapimessage, $props); |
|
| 1802 | - |
|
| 1803 | - return true; |
|
| 1804 | - } |
|
| 1805 | - |
|
| 1806 | - /** |
|
| 1807 | - * Writes a SyncTask to MAPI. |
|
| 1808 | - * |
|
| 1809 | - * @param mixed $mapimessage |
|
| 1810 | - * @param SyncTask $task |
|
| 1811 | - * |
|
| 1812 | - * @return bool |
|
| 1813 | - */ |
|
| 1814 | - private function setTask($mapimessage, $task) { |
|
| 1815 | - mapi_setprops($mapimessage, [PR_MESSAGE_CLASS => "IPM.Task"]); |
|
| 1816 | - |
|
| 1817 | - $taskmapping = MAPIMapping::GetTaskMapping(); |
|
| 1818 | - $taskprops = MAPIMapping::GetTaskProperties(); |
|
| 1819 | - $this->setPropsInMAPI($mapimessage, $task, $taskmapping); |
|
| 1820 | - $taskprops = array_merge($this->getPropIdsFromStrings($taskmapping), $this->getPropIdsFromStrings($taskprops)); |
|
| 1821 | - |
|
| 1822 | - // task specific properties to be set |
|
| 1823 | - $props = []; |
|
| 1824 | - |
|
| 1825 | - if (isset($task->asbody)) { |
|
| 1826 | - $this->setASbody($task->asbody, $props, $taskprops); |
|
| 1827 | - } |
|
| 1828 | - |
|
| 1829 | - if (isset($task->complete)) { |
|
| 1830 | - if ($task->complete) { |
|
| 1831 | - // Set completion to 100% |
|
| 1832 | - // Set status to 'complete' |
|
| 1833 | - $props[$taskprops["completion"]] = 1.0; |
|
| 1834 | - $props[$taskprops["status"]] = 2; |
|
| 1835 | - $props[$taskprops["reminderset"]] = false; |
|
| 1836 | - } |
|
| 1837 | - else { |
|
| 1838 | - // Set completion to 0% |
|
| 1839 | - // Set status to 'not started' |
|
| 1840 | - $props[$taskprops["completion"]] = 0.0; |
|
| 1841 | - $props[$taskprops["status"]] = 0; |
|
| 1842 | - } |
|
| 1843 | - } |
|
| 1844 | - if (isset($task->recurrence) && class_exists('TaskRecurrence')) { |
|
| 1845 | - $deadoccur = false; |
|
| 1846 | - if ((isset($task->recurrence->occurrences) && $task->recurrence->occurrences == 1) || |
|
| 1847 | - (isset($task->recurrence->deadoccur) && $task->recurrence->deadoccur == 1)) { // ios5 sends deadoccur inside the recurrence |
|
| 1848 | - $deadoccur = true; |
|
| 1849 | - } |
|
| 1850 | - |
|
| 1851 | - // Set PR_ICON_INDEX to 1281 to show correct icon in category view |
|
| 1852 | - $props[$taskprops["icon"]] = 1281; |
|
| 1853 | - // dead occur - false if new occurrences should be generated from the task |
|
| 1854 | - // true - if it is the last occurrence of the task |
|
| 1855 | - $props[$taskprops["deadoccur"]] = $deadoccur; |
|
| 1856 | - $props[$taskprops["isrecurringtag"]] = true; |
|
| 1857 | - |
|
| 1858 | - $recurrence = new TaskRecurrence($this->store, $mapimessage); |
|
| 1859 | - $recur = []; |
|
| 1860 | - $this->setRecurrence($task, $recur); |
|
| 1861 | - |
|
| 1862 | - // task specific recurrence properties which we need to set here |
|
| 1863 | - // "start" and "end" are in GMT when passing to class.recurrence |
|
| 1864 | - // set recurrence start here because it's calculated differently for tasks and appointments |
|
| 1865 | - $recur["start"] = $task->recurrence->start; |
|
| 1866 | - $recur["regen"] = (isset($task->recurrence->regenerate) && $task->recurrence->regenerate) ? 1 : 0; |
|
| 1867 | - // OL regenerates recurring task itself, but setting deleteOccurrence is required so that PHP-MAPI doesn't regenerate |
|
| 1868 | - // completed occurrence of a task. |
|
| 1869 | - if ($recur["regen"] == 0) { |
|
| 1870 | - $recur["deleteOccurrence"] = 0; |
|
| 1871 | - } |
|
| 1872 | - // Also add dates to $recur |
|
| 1873 | - $recur["duedate"] = $task->duedate; |
|
| 1874 | - $recur["complete"] = (isset($task->complete) && $task->complete) ? 1 : 0; |
|
| 1875 | - if (isset($task->datecompleted)) { |
|
| 1876 | - $recur["datecompleted"] = $task->datecompleted; |
|
| 1877 | - } |
|
| 1878 | - $recurrence->setRecurrence($recur); |
|
| 1879 | - } |
|
| 1880 | - |
|
| 1881 | - $props[$taskprops["private"]] = (isset($task->sensitivity) && $task->sensitivity >= SENSITIVITY_PRIVATE) ? true : false; |
|
| 1882 | - |
|
| 1883 | - // Open address book for user resolve to set the owner |
|
| 1884 | - $addrbook = $this->getAddressbook(); |
|
| 1885 | - |
|
| 1886 | - // check if there is already an owner for the task, set current user if not |
|
| 1887 | - $p = [$taskprops["owner"]]; |
|
| 1888 | - $owner = $this->getProps($mapimessage, $p); |
|
| 1889 | - if (!isset($owner[$taskprops["owner"]])) { |
|
| 1890 | - $userinfo = nsp_getuserinfo(Request::GetUser()); |
|
| 1891 | - if (mapi_last_hresult() == NOERROR && isset($userinfo["fullname"])) { |
|
| 1892 | - $props[$taskprops["owner"]] = $userinfo["fullname"]; |
|
| 1893 | - } |
|
| 1894 | - } |
|
| 1895 | - mapi_setprops($mapimessage, $props); |
|
| 1896 | - |
|
| 1897 | - return true; |
|
| 1898 | - } |
|
| 1899 | - |
|
| 1900 | - /** |
|
| 1901 | - * Writes a SyncNote to MAPI. |
|
| 1902 | - * |
|
| 1903 | - * @param mixed $mapimessage |
|
| 1904 | - * @param SyncNote $note |
|
| 1905 | - * |
|
| 1906 | - * @return bool |
|
| 1907 | - */ |
|
| 1908 | - private function setNote($mapimessage, $note) { |
|
| 1909 | - // Touchdown does not send categories if all are unset or there is none. |
|
| 1910 | - // Setting it to an empty array will unset the property in KC as well |
|
| 1911 | - if (!isset($note->categories)) { |
|
| 1912 | - $note->categories = []; |
|
| 1913 | - } |
|
| 1914 | - |
|
| 1915 | - // update icon index to correspond to the color |
|
| 1916 | - if (isset($note->Color) && $note->Color > -1 && $note->Color < 5) { |
|
| 1917 | - $note->Iconindex = 768 + $note->Color; |
|
| 1918 | - } |
|
| 1919 | - |
|
| 1920 | - $this->setPropsInMAPI($mapimessage, $note, MAPIMapping::GetNoteMapping()); |
|
| 1921 | - |
|
| 1922 | - $noteprops = MAPIMapping::GetNoteProperties(); |
|
| 1923 | - $noteprops = $this->getPropIdsFromStrings($noteprops); |
|
| 1924 | - |
|
| 1925 | - // note specific properties to be set |
|
| 1926 | - $props = []; |
|
| 1927 | - $props[$noteprops["messageclass"]] = "IPM.StickyNote"; |
|
| 1928 | - // set body otherwise the note will be "broken" when editing it in outlook |
|
| 1929 | - if (isset($note->asbody)) { |
|
| 1930 | - $this->setASbody($note->asbody, $props, $noteprops); |
|
| 1931 | - } |
|
| 1932 | - |
|
| 1933 | - $props[$noteprops["internetcpid"]] = INTERNET_CPID_UTF8; |
|
| 1934 | - mapi_setprops($mapimessage, $props); |
|
| 1935 | - |
|
| 1936 | - return true; |
|
| 1937 | - } |
|
| 1938 | - |
|
| 1939 | - /*---------------------------------------------------------------------------------------------------------- |
|
| 1246 | + /** |
|
| 1247 | + * Writes a SyncObject to MAPI |
|
| 1248 | + * Depending on the message class, a contact, appointment, task or email is written. |
|
| 1249 | + * |
|
| 1250 | + * @param mixed $mapimessage |
|
| 1251 | + * @param SyncObject $message |
|
| 1252 | + * |
|
| 1253 | + * @return bool |
|
| 1254 | + */ |
|
| 1255 | + public function SetMessage($mapimessage, $message) { |
|
| 1256 | + // TODO check with instanceof |
|
| 1257 | + switch (strtolower(get_class($message))) { |
|
| 1258 | + case "synccontact": |
|
| 1259 | + return $this->setContact($mapimessage, $message); |
|
| 1260 | + |
|
| 1261 | + case "syncappointment": |
|
| 1262 | + return $this->setAppointment($mapimessage, $message); |
|
| 1263 | + |
|
| 1264 | + case "synctask": |
|
| 1265 | + return $this->setTask($mapimessage, $message); |
|
| 1266 | + |
|
| 1267 | + case "syncnote": |
|
| 1268 | + return $this->setNote($mapimessage, $message); |
|
| 1269 | + |
|
| 1270 | + default: |
|
| 1271 | + // for emails only flag (read and todo) changes are possible |
|
| 1272 | + return $this->setEmail($mapimessage, $message); |
|
| 1273 | + } |
|
| 1274 | + } |
|
| 1275 | + |
|
| 1276 | + /** |
|
| 1277 | + * Writes SyncMail to MAPI (actually flags only). |
|
| 1278 | + * |
|
| 1279 | + * @param mixed $mapimessage |
|
| 1280 | + * @param SyncMail $message |
|
| 1281 | + */ |
|
| 1282 | + private function setEmail($mapimessage, $message) { |
|
| 1283 | + // update categories |
|
| 1284 | + if (!isset($message->categories)) { |
|
| 1285 | + $message->categories = []; |
|
| 1286 | + } |
|
| 1287 | + $emailmap = MAPIMapping::GetEmailMapping(); |
|
| 1288 | + $this->setPropsInMAPI($mapimessage, $message, ["categories" => $emailmap["categories"]]); |
|
| 1289 | + |
|
| 1290 | + $flagmapping = MAPIMapping::GetMailFlagsMapping(); |
|
| 1291 | + $flagprops = MAPIMapping::GetMailFlagsProperties(); |
|
| 1292 | + $flagprops = array_merge($this->getPropIdsFromStrings($flagmapping), $this->getPropIdsFromStrings($flagprops)); |
|
| 1293 | + // flag specific properties to be set |
|
| 1294 | + $props = $delprops = []; |
|
| 1295 | + // unset message flags if: |
|
| 1296 | + // flag is not set |
|
| 1297 | + if (empty($message->flag) || |
|
| 1298 | + // flag status is not set |
|
| 1299 | + !isset($message->flag->flagstatus) || |
|
| 1300 | + // flag status is 0 or empty |
|
| 1301 | + (isset($message->flag->flagstatus) && ($message->flag->flagstatus == 0 || $message->flag->flagstatus == ""))) { |
|
| 1302 | + // if message flag is empty, some properties need to be deleted |
|
| 1303 | + // and some set to 0 or false |
|
| 1304 | + |
|
| 1305 | + $props[$flagprops["todoitemsflags"]] = 0; |
|
| 1306 | + $props[$flagprops["status"]] = 0; |
|
| 1307 | + $props[$flagprops["completion"]] = 0.0; |
|
| 1308 | + $props[$flagprops["flagtype"]] = ""; |
|
| 1309 | + $props[$flagprops["ordinaldate"]] = 0x7FFFFFFF; // ordinal date is 12am 1.1.4501, set it to max possible value |
|
| 1310 | + $props[$flagprops["subordinaldate"]] = ""; |
|
| 1311 | + $props[$flagprops["replyrequested"]] = false; |
|
| 1312 | + $props[$flagprops["responserequested"]] = false; |
|
| 1313 | + $props[$flagprops["reminderset"]] = false; |
|
| 1314 | + $props[$flagprops["complete"]] = false; |
|
| 1315 | + |
|
| 1316 | + $delprops[] = $flagprops["todotitle"]; |
|
| 1317 | + $delprops[] = $flagprops["duedate"]; |
|
| 1318 | + $delprops[] = $flagprops["startdate"]; |
|
| 1319 | + $delprops[] = $flagprops["datecompleted"]; |
|
| 1320 | + $delprops[] = $flagprops["utcstartdate"]; |
|
| 1321 | + $delprops[] = $flagprops["utcduedate"]; |
|
| 1322 | + $delprops[] = $flagprops["completetime"]; |
|
| 1323 | + $delprops[] = $flagprops["flagstatus"]; |
|
| 1324 | + $delprops[] = $flagprops["flagicon"]; |
|
| 1325 | + } |
|
| 1326 | + else { |
|
| 1327 | + $this->setPropsInMAPI($mapimessage, $message->flag, $flagmapping); |
|
| 1328 | + $props[$flagprops["todoitemsflags"]] = 1; |
|
| 1329 | + if (isset($message->subject) && strlen($message->subject) > 0) { |
|
| 1330 | + $props[$flagprops["todotitle"]] = $message->subject; |
|
| 1331 | + } |
|
| 1332 | + // ordinal date is utc current time |
|
| 1333 | + if (!isset($message->flag->ordinaldate) || empty($message->flag->ordinaldate)) { |
|
| 1334 | + $props[$flagprops["ordinaldate"]] = time(); |
|
| 1335 | + } |
|
| 1336 | + // the default value |
|
| 1337 | + if (!isset($message->flag->subordinaldate) || empty($message->flag->subordinaldate)) { |
|
| 1338 | + $props[$flagprops["subordinaldate"]] = "5555555"; |
|
| 1339 | + } |
|
| 1340 | + $props[$flagprops["flagicon"]] = 6; // red flag icon |
|
| 1341 | + $props[$flagprops["replyrequested"]] = true; |
|
| 1342 | + $props[$flagprops["responserequested"]] = true; |
|
| 1343 | + |
|
| 1344 | + if ($message->flag->flagstatus == SYNC_FLAGSTATUS_COMPLETE) { |
|
| 1345 | + $props[$flagprops["status"]] = olTaskComplete; |
|
| 1346 | + $props[$flagprops["completion"]] = 1.0; |
|
| 1347 | + $props[$flagprops["complete"]] = true; |
|
| 1348 | + $props[$flagprops["replyrequested"]] = false; |
|
| 1349 | + $props[$flagprops["responserequested"]] = false; |
|
| 1350 | + unset($props[$flagprops["flagicon"]]); |
|
| 1351 | + $delprops[] = $flagprops["flagicon"]; |
|
| 1352 | + } |
|
| 1353 | + } |
|
| 1354 | + |
|
| 1355 | + if (!empty($props)) { |
|
| 1356 | + mapi_setprops($mapimessage, $props); |
|
| 1357 | + } |
|
| 1358 | + if (!empty($delprops)) { |
|
| 1359 | + mapi_deleteprops($mapimessage, $delprops); |
|
| 1360 | + } |
|
| 1361 | + } |
|
| 1362 | + |
|
| 1363 | + /** |
|
| 1364 | + * Writes a SyncAppointment to MAPI. |
|
| 1365 | + * |
|
| 1366 | + * @param mixed $mapimessage |
|
| 1367 | + * @param SyncAppointment $message |
|
| 1368 | + * @param mixed $appointment |
|
| 1369 | + * |
|
| 1370 | + * @return bool |
|
| 1371 | + */ |
|
| 1372 | + private function setAppointment($mapimessage, $appointment) { |
|
| 1373 | + // Get timezone info |
|
| 1374 | + if (isset($appointment->timezone)) { |
|
| 1375 | + $tz = $this->getTZFromSyncBlob(base64_decode($appointment->timezone)); |
|
| 1376 | + } |
|
| 1377 | + else { |
|
| 1378 | + $tz = false; |
|
| 1379 | + } |
|
| 1380 | + |
|
| 1381 | + // start and end time may not be set - try to get them from the existing appointment for further calculation - see https://jira.z-hub.io/browse/ZP-983 |
|
| 1382 | + if (!isset($appointment->starttime) || !isset($appointment->endtime)) { |
|
| 1383 | + $amapping = MAPIMapping::GetAppointmentMapping(); |
|
| 1384 | + $amapping = $this->getPropIdsFromStrings($amapping); |
|
| 1385 | + $existingstartendpropsmap = [$amapping["starttime"], $amapping["endtime"]]; |
|
| 1386 | + $existingstartendprops = $this->getProps($mapimessage, $existingstartendpropsmap); |
|
| 1387 | + |
|
| 1388 | + if (isset($existingstartendprops[$amapping["starttime"]]) && !isset($appointment->starttime)) { |
|
| 1389 | + $appointment->starttime = $existingstartendprops[$amapping["starttime"]]; |
|
| 1390 | + SLog::Write(LOGLEVEL_WBXML, sprintf("MAPIProvider->setAppointment(): Parameter 'starttime' was not set, using value from MAPI %d (%s).", $appointment->starttime, gmstrftime("%Y%m%dT%H%M%SZ", $appointment->starttime))); |
|
| 1391 | + } |
|
| 1392 | + if (isset($existingstartendprops[$amapping["endtime"]]) && !isset($appointment->endtime)) { |
|
| 1393 | + $appointment->endtime = $existingstartendprops[$amapping["endtime"]]; |
|
| 1394 | + SLog::Write(LOGLEVEL_WBXML, sprintf("MAPIProvider->setAppointment(): Parameter 'endtime' was not set, using value from MAPI %d (%s).", $appointment->endtime, gmstrftime("%Y%m%dT%H%M%SZ", $appointment->endtime))); |
|
| 1395 | + } |
|
| 1396 | + } |
|
| 1397 | + if (!isset($appointment->starttime) || !isset($appointment->endtime)) { |
|
| 1398 | + throw new StatusException("MAPIProvider->setAppointment(): Error, start and/or end time not set and can not be retrieved from MAPI.", SYNC_STATUS_SYNCCANNOTBECOMPLETED); |
|
| 1399 | + } |
|
| 1400 | + |
|
| 1401 | + // calculate duration because without it some webaccess views are broken. duration is in min |
|
| 1402 | + $localstart = $this->getLocaltimeByTZ($appointment->starttime, $tz); |
|
| 1403 | + $localend = $this->getLocaltimeByTZ($appointment->endtime, $tz); |
|
| 1404 | + $duration = ($localend - $localstart) / 60; |
|
| 1405 | + |
|
| 1406 | + // nokia sends an yearly event with 0 mins duration but as all day event, |
|
| 1407 | + // so make it end next day |
|
| 1408 | + if ($appointment->starttime == $appointment->endtime && isset($appointment->alldayevent) && $appointment->alldayevent) { |
|
| 1409 | + $duration = 1440; |
|
| 1410 | + $appointment->endtime = $appointment->starttime + 24 * 60 * 60; |
|
| 1411 | + $localend = $localstart + 24 * 60 * 60; |
|
| 1412 | + } |
|
| 1413 | + |
|
| 1414 | + // is the transmitted UID OL compatible? |
|
| 1415 | + // if not, encapsulate the transmitted uid |
|
| 1416 | + $appointment->uid = Utils::GetOLUidFromICalUid($appointment->uid); |
|
| 1417 | + |
|
| 1418 | + mapi_setprops($mapimessage, [PR_MESSAGE_CLASS => "IPM.Appointment"]); |
|
| 1419 | + |
|
| 1420 | + $appointmentmapping = MAPIMapping::GetAppointmentMapping(); |
|
| 1421 | + $this->setPropsInMAPI($mapimessage, $appointment, $appointmentmapping); |
|
| 1422 | + $appointmentprops = MAPIMapping::GetAppointmentProperties(); |
|
| 1423 | + $appointmentprops = array_merge($this->getPropIdsFromStrings($appointmentmapping), $this->getPropIdsFromStrings($appointmentprops)); |
|
| 1424 | + // appointment specific properties to be set |
|
| 1425 | + $props = []; |
|
| 1426 | + |
|
| 1427 | + // sensitivity is not enough to mark an appointment as private, so we use another mapi tag |
|
| 1428 | + $private = (isset($appointment->sensitivity) && $appointment->sensitivity >= SENSITIVITY_PRIVATE) ? true : false; |
|
| 1429 | + |
|
| 1430 | + // Set commonstart/commonend to start/end and remindertime to start, duration, private and cleanGlobalObjectId |
|
| 1431 | + $props[$appointmentprops["commonstart"]] = $appointment->starttime; |
|
| 1432 | + $props[$appointmentprops["commonend"]] = $appointment->endtime; |
|
| 1433 | + $props[$appointmentprops["reminderstart"]] = $appointment->starttime; |
|
| 1434 | + // Set reminder boolean to 'true' if reminder is set |
|
| 1435 | + $props[$appointmentprops["reminderset"]] = isset($appointment->reminder) ? true : false; |
|
| 1436 | + $props[$appointmentprops["duration"]] = $duration; |
|
| 1437 | + $props[$appointmentprops["private"]] = $private; |
|
| 1438 | + $props[$appointmentprops["uid"]] = $appointment->uid; |
|
| 1439 | + // Set named prop 8510, unknown property, but enables deleting a single occurrence of a recurring |
|
| 1440 | + // type in OLK2003. |
|
| 1441 | + $props[$appointmentprops["sideeffects"]] = 369; |
|
| 1442 | + |
|
| 1443 | + if (isset($appointment->reminder) && $appointment->reminder >= 0) { |
|
| 1444 | + // Set 'flagdueby' to correct value (start - reminderminutes) |
|
| 1445 | + $props[$appointmentprops["flagdueby"]] = $appointment->starttime - $appointment->reminder * 60; |
|
| 1446 | + $props[$appointmentprops["remindertime"]] = $appointment->reminder; |
|
| 1447 | + } |
|
| 1448 | + // unset the reminder |
|
| 1449 | + else { |
|
| 1450 | + $props[$appointmentprops["reminderset"]] = false; |
|
| 1451 | + } |
|
| 1452 | + |
|
| 1453 | + if (isset($appointment->asbody)) { |
|
| 1454 | + $this->setASbody($appointment->asbody, $props, $appointmentprops); |
|
| 1455 | + } |
|
| 1456 | + |
|
| 1457 | + if ($tz !== false) { |
|
| 1458 | + $props[$appointmentprops["timezonetag"]] = $this->getMAPIBlobFromTZ($tz); |
|
| 1459 | + } |
|
| 1460 | + |
|
| 1461 | + if (isset($appointment->recurrence)) { |
|
| 1462 | + // Set PR_ICON_INDEX to 1025 to show correct icon in category view |
|
| 1463 | + $props[$appointmentprops["icon"]] = 1025; |
|
| 1464 | + |
|
| 1465 | + // if there aren't any exceptions, use the 'old style' set recurrence |
|
| 1466 | + $noexceptions = true; |
|
| 1467 | + |
|
| 1468 | + $recurrence = new Recurrence($this->store, $mapimessage); |
|
| 1469 | + $recur = []; |
|
| 1470 | + $this->setRecurrence($appointment, $recur); |
|
| 1471 | + |
|
| 1472 | + // set the recurrence type to that of the MAPI |
|
| 1473 | + $props[$appointmentprops["recurrencetype"]] = $recur["recurrencetype"]; |
|
| 1474 | + |
|
| 1475 | + $starttime = $this->gmtime($localstart); |
|
| 1476 | + $endtime = $this->gmtime($localend); |
|
| 1477 | + |
|
| 1478 | + // set recurrence start here because it's calculated differently for tasks and appointments |
|
| 1479 | + $recur["start"] = $this->getDayStartOfTimestamp($this->getGMTTimeByTZ($localstart, $tz)); |
|
| 1480 | + |
|
| 1481 | + $recur["startocc"] = $starttime["tm_hour"] * 60 + $starttime["tm_min"]; |
|
| 1482 | + $recur["endocc"] = $recur["startocc"] + $duration; // Note that this may be > 24*60 if multi-day |
|
| 1483 | + |
|
| 1484 | + // only tasks can regenerate |
|
| 1485 | + $recur["regen"] = false; |
|
| 1486 | + |
|
| 1487 | + // Process exceptions. The PDA will send all exceptions for this recurring item. |
|
| 1488 | + if (isset($appointment->exceptions)) { |
|
| 1489 | + foreach ($appointment->exceptions as $exception) { |
|
| 1490 | + // we always need the base date |
|
| 1491 | + if (!isset($exception->exceptionstarttime)) { |
|
| 1492 | + continue; |
|
| 1493 | + } |
|
| 1494 | + |
|
| 1495 | + $basedate = $this->getDayStartOfTimestamp($exception->exceptionstarttime); |
|
| 1496 | + if (isset($exception->deleted) && $exception->deleted) { |
|
| 1497 | + $noexceptions = false; |
|
| 1498 | + // Delete exception |
|
| 1499 | + $recurrence->createException([], $basedate, true); |
|
| 1500 | + } |
|
| 1501 | + else { |
|
| 1502 | + // Change exception |
|
| 1503 | + $mapiexception = ["basedate" => $basedate]; |
|
| 1504 | + // other exception properties which are not handled in recurrence |
|
| 1505 | + $exceptionprops = []; |
|
| 1506 | + |
|
| 1507 | + if (isset($exception->starttime)) { |
|
| 1508 | + $mapiexception["start"] = $this->getLocaltimeByTZ($exception->starttime, $tz); |
|
| 1509 | + $exceptionprops[$appointmentprops["starttime"]] = $exception->starttime; |
|
| 1510 | + } |
|
| 1511 | + if (isset($exception->endtime)) { |
|
| 1512 | + $mapiexception["end"] = $this->getLocaltimeByTZ($exception->endtime, $tz); |
|
| 1513 | + $exceptionprops[$appointmentprops["endtime"]] = $exception->endtime; |
|
| 1514 | + } |
|
| 1515 | + if (isset($exception->subject)) { |
|
| 1516 | + $exceptionprops[$appointmentprops["subject"]] = $mapiexception["subject"] = u2w($exception->subject); |
|
| 1517 | + } |
|
| 1518 | + if (isset($exception->location)) { |
|
| 1519 | + $exceptionprops[$appointmentprops["location"]] = $mapiexception["location"] = u2w($exception->location); |
|
| 1520 | + } |
|
| 1521 | + if (isset($exception->busystatus)) { |
|
| 1522 | + $exceptionprops[$appointmentprops["busystatus"]] = $mapiexception["busystatus"] = $exception->busystatus; |
|
| 1523 | + } |
|
| 1524 | + if (isset($exception->reminder)) { |
|
| 1525 | + $exceptionprops[$appointmentprops["reminderset"]] = $mapiexception["reminder_set"] = 1; |
|
| 1526 | + $exceptionprops[$appointmentprops["remindertime"]] = $mapiexception["remind_before"] = $exception->reminder; |
|
| 1527 | + } |
|
| 1528 | + if (isset($exception->alldayevent)) { |
|
| 1529 | + $exceptionprops[$appointmentprops["alldayevent"]] = $mapiexception["alldayevent"] = $exception->alldayevent; |
|
| 1530 | + } |
|
| 1531 | + |
|
| 1532 | + if (!isset($recur["changed_occurrences"])) { |
|
| 1533 | + $recur["changed_occurrences"] = []; |
|
| 1534 | + } |
|
| 1535 | + |
|
| 1536 | + if (isset($exception->body)) { |
|
| 1537 | + $exceptionprops[$appointmentprops["body"]] = u2w($exception->body); |
|
| 1538 | + } |
|
| 1539 | + |
|
| 1540 | + if (isset($exception->asbody)) { |
|
| 1541 | + $this->setASbody($exception->asbody, $exceptionprops, $appointmentprops); |
|
| 1542 | + $mapiexception["body"] = $exceptionprops[$appointmentprops["body"]] = |
|
| 1543 | + (isset($exceptionprops[$appointmentprops["body"]])) ? $exceptionprops[$appointmentprops["body"]] : |
|
| 1544 | + ((isset($exceptionprops[$appointmentprops["html"]])) ? $exceptionprops[$appointmentprops["html"]] : ""); |
|
| 1545 | + } |
|
| 1546 | + |
|
| 1547 | + array_push($recur["changed_occurrences"], $mapiexception); |
|
| 1548 | + |
|
| 1549 | + if (!empty($exceptionprops)) { |
|
| 1550 | + $noexceptions = false; |
|
| 1551 | + if ($recurrence->isException($basedate)) { |
|
| 1552 | + $recurrence->modifyException($exceptionprops, $basedate); |
|
| 1553 | + } |
|
| 1554 | + else { |
|
| 1555 | + $recurrence->createException($exceptionprops, $basedate); |
|
| 1556 | + } |
|
| 1557 | + } |
|
| 1558 | + } |
|
| 1559 | + } |
|
| 1560 | + } |
|
| 1561 | + |
|
| 1562 | + // setRecurrence deletes the attachments from an appointment |
|
| 1563 | + if ($noexceptions) { |
|
| 1564 | + $recurrence->setRecurrence($tz, $recur); |
|
| 1565 | + } |
|
| 1566 | + } |
|
| 1567 | + else { |
|
| 1568 | + $props[$appointmentprops["isrecurring"]] = false; |
|
| 1569 | + } |
|
| 1570 | + |
|
| 1571 | + // always set the PR_SENT_REPRESENTING_* props so that the attendee status update also works with the webaccess |
|
| 1572 | + $p = [$appointmentprops["representingentryid"], $appointmentprops["representingname"], $appointmentprops["sentrepresentingaddt"], |
|
| 1573 | + $appointmentprops["sentrepresentingemail"], $appointmentprops["sentrepresentinsrchk"], $appointmentprops["responsestatus"], ]; |
|
| 1574 | + $representingprops = $this->getProps($mapimessage, $p); |
|
| 1575 | + |
|
| 1576 | + if (!isset($representingprops[$appointmentprops["representingentryid"]])) { |
|
| 1577 | + // TODO use GetStoreProps |
|
| 1578 | + $storeProps = mapi_getprops($this->store, [PR_MAILBOX_OWNER_ENTRYID]); |
|
| 1579 | + $props[$appointmentprops["representingentryid"]] = $storeProps[PR_MAILBOX_OWNER_ENTRYID]; |
|
| 1580 | + $displayname = $this->getFullnameFromEntryID($storeProps[PR_MAILBOX_OWNER_ENTRYID]); |
|
| 1581 | + |
|
| 1582 | + $props[$appointmentprops["representingname"]] = ($displayname !== false) ? $displayname : Request::GetUser(); |
|
| 1583 | + $props[$appointmentprops["sentrepresentingemail"]] = Request::GetUser(); |
|
| 1584 | + $props[$appointmentprops["sentrepresentingaddt"]] = "ZARAFA"; |
|
| 1585 | + $props[$appointmentprops["sentrepresentinsrchk"]] = $props[$appointmentprops["sentrepresentingaddt"]] . ":" . $props[$appointmentprops["sentrepresentingemail"]]; |
|
| 1586 | + |
|
| 1587 | + if (isset($appointment->attendees) && is_array($appointment->attendees) && !empty($appointment->attendees)) { |
|
| 1588 | + $props[$appointmentprops["icon"]] = 1026; |
|
| 1589 | + // the user is the organizer |
|
| 1590 | + // set these properties to show tracking tab in webapp |
|
| 1591 | + |
|
| 1592 | + $props[$appointmentprops["mrwassent"]] = true; |
|
| 1593 | + $props[$appointmentprops["responsestatus"]] = olResponseOrganized; |
|
| 1594 | + $props[$appointmentprops["meetingstatus"]] = olMeeting; |
|
| 1595 | + } |
|
| 1596 | + } |
|
| 1597 | + // we also have to set the responsestatus and not only meetingstatus, so we use another mapi tag |
|
| 1598 | + if (!isset($props[$appointmentprops["responsestatus"]])) { |
|
| 1599 | + if (isset($appointment->responsetype)) { |
|
| 1600 | + $props[$appointmentprops["responsestatus"]] = $appointment->responsetype; |
|
| 1601 | + } |
|
| 1602 | + // only set responsestatus to none if it is not set on the server |
|
| 1603 | + elseif (!isset($representingprops[$appointmentprops["responsestatus"]])) { |
|
| 1604 | + $props[$appointmentprops["responsestatus"]] = olResponseNone; |
|
| 1605 | + } |
|
| 1606 | + } |
|
| 1607 | + |
|
| 1608 | + // Do attendees |
|
| 1609 | + if (isset($appointment->attendees) && is_array($appointment->attendees)) { |
|
| 1610 | + $recips = []; |
|
| 1611 | + |
|
| 1612 | + // Outlook XP requires organizer in the attendee list as well |
|
| 1613 | + $org = []; |
|
| 1614 | + $org[PR_ENTRYID] = isset($representingprops[$appointmentprops["representingentryid"]]) ? $representingprops[$appointmentprops["representingentryid"]] : $props[$appointmentprops["representingentryid"]]; |
|
| 1615 | + $org[PR_DISPLAY_NAME] = isset($representingprops[$appointmentprops["representingname"]]) ? $representingprops[$appointmentprops["representingname"]] : $props[$appointmentprops["representingname"]]; |
|
| 1616 | + $org[PR_ADDRTYPE] = isset($representingprops[$appointmentprops["sentrepresentingaddt"]]) ? $representingprops[$appointmentprops["sentrepresentingaddt"]] : $props[$appointmentprops["sentrepresentingaddt"]]; |
|
| 1617 | + $org[PR_SMTP_ADDRESS] = $org[PR_EMAIL_ADDRESS] = isset($representingprops[$appointmentprops["sentrepresentingemail"]]) ? $representingprops[$appointmentprops["sentrepresentingemail"]] : $props[$appointmentprops["sentrepresentingemail"]]; |
|
| 1618 | + $org[PR_SEARCH_KEY] = isset($representingprops[$appointmentprops["sentrepresentinsrchk"]]) ? $representingprops[$appointmentprops["sentrepresentinsrchk"]] : $props[$appointmentprops["sentrepresentinsrchk"]]; |
|
| 1619 | + $org[PR_RECIPIENT_FLAGS] = recipOrganizer | recipSendable; |
|
| 1620 | + $org[PR_RECIPIENT_TYPE] = MAPI_ORIG; |
|
| 1621 | + |
|
| 1622 | + array_push($recips, $org); |
|
| 1623 | + |
|
| 1624 | + // Open address book for user resolve |
|
| 1625 | + $addrbook = $this->getAddressbook(); |
|
| 1626 | + foreach ($appointment->attendees as $attendee) { |
|
| 1627 | + $recip = []; |
|
| 1628 | + $recip[PR_EMAIL_ADDRESS] = u2w($attendee->email); |
|
| 1629 | + $recip[PR_SMTP_ADDRESS] = u2w($attendee->email); |
|
| 1630 | + |
|
| 1631 | + // lookup information in GAB if possible so we have up-to-date name for given address |
|
| 1632 | + $userinfo = [[PR_DISPLAY_NAME => $recip[PR_EMAIL_ADDRESS]]]; |
|
| 1633 | + $userinfo = mapi_ab_resolvename($addrbook, $userinfo, EMS_AB_ADDRESS_LOOKUP); |
|
| 1634 | + if (mapi_last_hresult() == NOERROR) { |
|
| 1635 | + $recip[PR_DISPLAY_NAME] = $userinfo[0][PR_DISPLAY_NAME]; |
|
| 1636 | + $recip[PR_EMAIL_ADDRESS] = $userinfo[0][PR_EMAIL_ADDRESS]; |
|
| 1637 | + $recip[PR_SEARCH_KEY] = $userinfo[0][PR_SEARCH_KEY]; |
|
| 1638 | + $recip[PR_ADDRTYPE] = $userinfo[0][PR_ADDRTYPE]; |
|
| 1639 | + $recip[PR_ENTRYID] = $userinfo[0][PR_ENTRYID]; |
|
| 1640 | + $recip[PR_RECIPIENT_TYPE] = isset($attendee->attendeetype) ? $attendee->attendeetype : MAPI_TO; |
|
| 1641 | + $recip[PR_RECIPIENT_FLAGS] = recipSendable; |
|
| 1642 | + $recip[PR_RECIPIENT_TRACKSTATUS] = isset($attendee->attendeestatus) ? $attendee->attendeestatus : olResponseNone; |
|
| 1643 | + } |
|
| 1644 | + else { |
|
| 1645 | + $recip[PR_DISPLAY_NAME] = u2w($attendee->name); |
|
| 1646 | + $recip[PR_SEARCH_KEY] = "SMTP:" . $recip[PR_EMAIL_ADDRESS] . "\0"; |
|
| 1647 | + $recip[PR_ADDRTYPE] = "SMTP"; |
|
| 1648 | + $recip[PR_RECIPIENT_TYPE] = isset($attendee->attendeetype) ? $attendee->attendeetype : MAPI_TO; |
|
| 1649 | + $recip[PR_ENTRYID] = mapi_createoneoff($recip[PR_DISPLAY_NAME], $recip[PR_ADDRTYPE], $recip[PR_EMAIL_ADDRESS]); |
|
| 1650 | + } |
|
| 1651 | + |
|
| 1652 | + array_push($recips, $recip); |
|
| 1653 | + } |
|
| 1654 | + |
|
| 1655 | + mapi_message_modifyrecipients($mapimessage, 0, $recips); |
|
| 1656 | + } |
|
| 1657 | + mapi_setprops($mapimessage, $props); |
|
| 1658 | + |
|
| 1659 | + return true; |
|
| 1660 | + } |
|
| 1661 | + |
|
| 1662 | + /** |
|
| 1663 | + * Writes a SyncContact to MAPI. |
|
| 1664 | + * |
|
| 1665 | + * @param mixed $mapimessage |
|
| 1666 | + * @param SyncContact $contact |
|
| 1667 | + * |
|
| 1668 | + * @return bool |
|
| 1669 | + */ |
|
| 1670 | + private function setContact($mapimessage, $contact) { |
|
| 1671 | + mapi_setprops($mapimessage, [PR_MESSAGE_CLASS => "IPM.Contact"]); |
|
| 1672 | + |
|
| 1673 | + // normalize email addresses |
|
| 1674 | + if (isset($contact->email1address) && (($contact->email1address = $this->extractEmailAddress($contact->email1address)) === false)) { |
|
| 1675 | + unset($contact->email1address); |
|
| 1676 | + } |
|
| 1677 | + |
|
| 1678 | + if (isset($contact->email2address) && (($contact->email2address = $this->extractEmailAddress($contact->email2address)) === false)) { |
|
| 1679 | + unset($contact->email2address); |
|
| 1680 | + } |
|
| 1681 | + |
|
| 1682 | + if (isset($contact->email3address) && (($contact->email3address = $this->extractEmailAddress($contact->email3address)) === false)) { |
|
| 1683 | + unset($contact->email3address); |
|
| 1684 | + } |
|
| 1685 | + |
|
| 1686 | + $contactmapping = MAPIMapping::GetContactMapping(); |
|
| 1687 | + $contactprops = MAPIMapping::GetContactProperties(); |
|
| 1688 | + $this->setPropsInMAPI($mapimessage, $contact, $contactmapping); |
|
| 1689 | + |
|
| 1690 | + // /set display name from contact's properties |
|
| 1691 | + $cname = $this->composeDisplayName($contact); |
|
| 1692 | + |
|
| 1693 | + // get contact specific mapi properties and merge them with the AS properties |
|
| 1694 | + $contactprops = array_merge($this->getPropIdsFromStrings($contactmapping), $this->getPropIdsFromStrings($contactprops)); |
|
| 1695 | + |
|
| 1696 | + // contact specific properties to be set |
|
| 1697 | + $props = []; |
|
| 1698 | + |
|
| 1699 | + // need to be set in order to show contacts properly in outlook and wa |
|
| 1700 | + $nremails = []; |
|
| 1701 | + $abprovidertype = 0; |
|
| 1702 | + |
|
| 1703 | + if (isset($contact->email1address)) { |
|
| 1704 | + $this->setEmailAddress($contact->email1address, $cname, 1, $props, $contactprops, $nremails, $abprovidertype); |
|
| 1705 | + } |
|
| 1706 | + if (isset($contact->email2address)) { |
|
| 1707 | + $this->setEmailAddress($contact->email2address, $cname, 2, $props, $contactprops, $nremails, $abprovidertype); |
|
| 1708 | + } |
|
| 1709 | + if (isset($contact->email3address)) { |
|
| 1710 | + $this->setEmailAddress($contact->email3address, $cname, 3, $props, $contactprops, $nremails, $abprovidertype); |
|
| 1711 | + } |
|
| 1712 | + |
|
| 1713 | + $props[$contactprops["addressbooklong"]] = $abprovidertype; |
|
| 1714 | + $props[$contactprops["displayname"]] = $props[$contactprops["subject"]] = $cname; |
|
| 1715 | + |
|
| 1716 | + // pda multiple e-mail addresses bug fix for the contact |
|
| 1717 | + if (!empty($nremails)) { |
|
| 1718 | + $props[$contactprops["addressbookmv"]] = $nremails; |
|
| 1719 | + } |
|
| 1720 | + |
|
| 1721 | + // set addresses |
|
| 1722 | + $this->setAddress("home", $contact->homecity, $contact->homecountry, $contact->homepostalcode, $contact->homestate, $contact->homestreet, $props, $contactprops); |
|
| 1723 | + $this->setAddress("business", $contact->businesscity, $contact->businesscountry, $contact->businesspostalcode, $contact->businessstate, $contact->businessstreet, $props, $contactprops); |
|
| 1724 | + $this->setAddress("other", $contact->othercity, $contact->othercountry, $contact->otherpostalcode, $contact->otherstate, $contact->otherstreet, $props, $contactprops); |
|
| 1725 | + |
|
| 1726 | + // set the mailing address and its type |
|
| 1727 | + if (isset($props[$contactprops["businessaddress"]])) { |
|
| 1728 | + $props[$contactprops["mailingaddress"]] = 2; |
|
| 1729 | + $this->setMailingAddress($contact->businesscity, $contact->businesscountry, $contact->businesspostalcode, $contact->businessstate, $contact->businessstreet, $props[$contactprops["businessaddress"]], $props, $contactprops); |
|
| 1730 | + } |
|
| 1731 | + elseif (isset($props[$contactprops["homeaddress"]])) { |
|
| 1732 | + $props[$contactprops["mailingaddress"]] = 1; |
|
| 1733 | + $this->setMailingAddress($contact->homecity, $contact->homecountry, $contact->homepostalcode, $contact->homestate, $contact->homestreet, $props[$contactprops["homeaddress"]], $props, $contactprops); |
|
| 1734 | + } |
|
| 1735 | + elseif (isset($props[$contactprops["otheraddress"]])) { |
|
| 1736 | + $props[$contactprops["mailingaddress"]] = 3; |
|
| 1737 | + $this->setMailingAddress($contact->othercity, $contact->othercountry, $contact->otherpostalcode, $contact->otherstate, $contact->otherstreet, $props[$contactprops["otheraddress"]], $props, $contactprops); |
|
| 1738 | + } |
|
| 1739 | + |
|
| 1740 | + if (isset($contact->picture)) { |
|
| 1741 | + $picbinary = base64_decode($contact->picture); |
|
| 1742 | + $picsize = strlen($picbinary); |
|
| 1743 | + $props[$contactprops["haspic"]] = false; |
|
| 1744 | + |
|
| 1745 | + // TODO contact picture handling |
|
| 1746 | + // check if contact has already got a picture. delete it first in that case |
|
| 1747 | + // delete it also if it was removed on a mobile |
|
| 1748 | + $picprops = mapi_getprops($mapimessage, [$contactprops["haspic"]]); |
|
| 1749 | + if (isset($picprops[$contactprops["haspic"]]) && $picprops[$contactprops["haspic"]]) { |
|
| 1750 | + SLog::Write(LOGLEVEL_DEBUG, "Contact already has a picture. Delete it"); |
|
| 1751 | + |
|
| 1752 | + $attachtable = mapi_message_getattachmenttable($mapimessage); |
|
| 1753 | + mapi_table_restrict($attachtable, MAPIUtils::GetContactPicRestriction()); |
|
| 1754 | + $rows = mapi_table_queryallrows($attachtable, [PR_ATTACH_NUM]); |
|
| 1755 | + if (isset($rows) && is_array($rows)) { |
|
| 1756 | + foreach ($rows as $row) { |
|
| 1757 | + mapi_message_deleteattach($mapimessage, $row[PR_ATTACH_NUM]); |
|
| 1758 | + } |
|
| 1759 | + } |
|
| 1760 | + } |
|
| 1761 | + |
|
| 1762 | + // only set picture if there's data in the request |
|
| 1763 | + if ($picbinary !== false && $picsize > 0) { |
|
| 1764 | + $props[$contactprops["haspic"]] = true; |
|
| 1765 | + $pic = mapi_message_createattach($mapimessage); |
|
| 1766 | + // Set properties of the attachment |
|
| 1767 | + $picprops = [ |
|
| 1768 | + PR_ATTACH_LONG_FILENAME => "ContactPicture.jpg", |
|
| 1769 | + PR_DISPLAY_NAME => "ContactPicture.jpg", |
|
| 1770 | + 0x7FFF000B => true, |
|
| 1771 | + PR_ATTACHMENT_HIDDEN => false, |
|
| 1772 | + PR_ATTACHMENT_FLAGS => 1, |
|
| 1773 | + PR_ATTACH_METHOD => ATTACH_BY_VALUE, |
|
| 1774 | + PR_ATTACH_EXTENSION => ".jpg", |
|
| 1775 | + PR_ATTACH_NUM => 1, |
|
| 1776 | + PR_ATTACH_SIZE => $picsize, |
|
| 1777 | + PR_ATTACH_DATA_BIN => $picbinary, |
|
| 1778 | + ]; |
|
| 1779 | + |
|
| 1780 | + mapi_setprops($pic, $picprops); |
|
| 1781 | + mapi_savechanges($pic); |
|
| 1782 | + } |
|
| 1783 | + } |
|
| 1784 | + |
|
| 1785 | + if (isset($contact->asbody)) { |
|
| 1786 | + $this->setASbody($contact->asbody, $props, $contactprops); |
|
| 1787 | + } |
|
| 1788 | + |
|
| 1789 | + // set fileas |
|
| 1790 | + if (defined('FILEAS_ORDER')) { |
|
| 1791 | + $lastname = (isset($contact->lastname)) ? $contact->lastname : ""; |
|
| 1792 | + $firstname = (isset($contact->firstname)) ? $contact->firstname : ""; |
|
| 1793 | + $middlename = (isset($contact->middlename)) ? $contact->middlename : ""; |
|
| 1794 | + $company = (isset($contact->companyname)) ? $contact->companyname : ""; |
|
| 1795 | + $props[$contactprops["fileas"]] = Utils::BuildFileAs($lastname, $firstname, $middlename, $company); |
|
| 1796 | + } |
|
| 1797 | + else { |
|
| 1798 | + SLog::Write(LOGLEVEL_DEBUG, "FILEAS_ORDER not defined"); |
|
| 1799 | + } |
|
| 1800 | + |
|
| 1801 | + mapi_setprops($mapimessage, $props); |
|
| 1802 | + |
|
| 1803 | + return true; |
|
| 1804 | + } |
|
| 1805 | + |
|
| 1806 | + /** |
|
| 1807 | + * Writes a SyncTask to MAPI. |
|
| 1808 | + * |
|
| 1809 | + * @param mixed $mapimessage |
|
| 1810 | + * @param SyncTask $task |
|
| 1811 | + * |
|
| 1812 | + * @return bool |
|
| 1813 | + */ |
|
| 1814 | + private function setTask($mapimessage, $task) { |
|
| 1815 | + mapi_setprops($mapimessage, [PR_MESSAGE_CLASS => "IPM.Task"]); |
|
| 1816 | + |
|
| 1817 | + $taskmapping = MAPIMapping::GetTaskMapping(); |
|
| 1818 | + $taskprops = MAPIMapping::GetTaskProperties(); |
|
| 1819 | + $this->setPropsInMAPI($mapimessage, $task, $taskmapping); |
|
| 1820 | + $taskprops = array_merge($this->getPropIdsFromStrings($taskmapping), $this->getPropIdsFromStrings($taskprops)); |
|
| 1821 | + |
|
| 1822 | + // task specific properties to be set |
|
| 1823 | + $props = []; |
|
| 1824 | + |
|
| 1825 | + if (isset($task->asbody)) { |
|
| 1826 | + $this->setASbody($task->asbody, $props, $taskprops); |
|
| 1827 | + } |
|
| 1828 | + |
|
| 1829 | + if (isset($task->complete)) { |
|
| 1830 | + if ($task->complete) { |
|
| 1831 | + // Set completion to 100% |
|
| 1832 | + // Set status to 'complete' |
|
| 1833 | + $props[$taskprops["completion"]] = 1.0; |
|
| 1834 | + $props[$taskprops["status"]] = 2; |
|
| 1835 | + $props[$taskprops["reminderset"]] = false; |
|
| 1836 | + } |
|
| 1837 | + else { |
|
| 1838 | + // Set completion to 0% |
|
| 1839 | + // Set status to 'not started' |
|
| 1840 | + $props[$taskprops["completion"]] = 0.0; |
|
| 1841 | + $props[$taskprops["status"]] = 0; |
|
| 1842 | + } |
|
| 1843 | + } |
|
| 1844 | + if (isset($task->recurrence) && class_exists('TaskRecurrence')) { |
|
| 1845 | + $deadoccur = false; |
|
| 1846 | + if ((isset($task->recurrence->occurrences) && $task->recurrence->occurrences == 1) || |
|
| 1847 | + (isset($task->recurrence->deadoccur) && $task->recurrence->deadoccur == 1)) { // ios5 sends deadoccur inside the recurrence |
|
| 1848 | + $deadoccur = true; |
|
| 1849 | + } |
|
| 1850 | + |
|
| 1851 | + // Set PR_ICON_INDEX to 1281 to show correct icon in category view |
|
| 1852 | + $props[$taskprops["icon"]] = 1281; |
|
| 1853 | + // dead occur - false if new occurrences should be generated from the task |
|
| 1854 | + // true - if it is the last occurrence of the task |
|
| 1855 | + $props[$taskprops["deadoccur"]] = $deadoccur; |
|
| 1856 | + $props[$taskprops["isrecurringtag"]] = true; |
|
| 1857 | + |
|
| 1858 | + $recurrence = new TaskRecurrence($this->store, $mapimessage); |
|
| 1859 | + $recur = []; |
|
| 1860 | + $this->setRecurrence($task, $recur); |
|
| 1861 | + |
|
| 1862 | + // task specific recurrence properties which we need to set here |
|
| 1863 | + // "start" and "end" are in GMT when passing to class.recurrence |
|
| 1864 | + // set recurrence start here because it's calculated differently for tasks and appointments |
|
| 1865 | + $recur["start"] = $task->recurrence->start; |
|
| 1866 | + $recur["regen"] = (isset($task->recurrence->regenerate) && $task->recurrence->regenerate) ? 1 : 0; |
|
| 1867 | + // OL regenerates recurring task itself, but setting deleteOccurrence is required so that PHP-MAPI doesn't regenerate |
|
| 1868 | + // completed occurrence of a task. |
|
| 1869 | + if ($recur["regen"] == 0) { |
|
| 1870 | + $recur["deleteOccurrence"] = 0; |
|
| 1871 | + } |
|
| 1872 | + // Also add dates to $recur |
|
| 1873 | + $recur["duedate"] = $task->duedate; |
|
| 1874 | + $recur["complete"] = (isset($task->complete) && $task->complete) ? 1 : 0; |
|
| 1875 | + if (isset($task->datecompleted)) { |
|
| 1876 | + $recur["datecompleted"] = $task->datecompleted; |
|
| 1877 | + } |
|
| 1878 | + $recurrence->setRecurrence($recur); |
|
| 1879 | + } |
|
| 1880 | + |
|
| 1881 | + $props[$taskprops["private"]] = (isset($task->sensitivity) && $task->sensitivity >= SENSITIVITY_PRIVATE) ? true : false; |
|
| 1882 | + |
|
| 1883 | + // Open address book for user resolve to set the owner |
|
| 1884 | + $addrbook = $this->getAddressbook(); |
|
| 1885 | + |
|
| 1886 | + // check if there is already an owner for the task, set current user if not |
|
| 1887 | + $p = [$taskprops["owner"]]; |
|
| 1888 | + $owner = $this->getProps($mapimessage, $p); |
|
| 1889 | + if (!isset($owner[$taskprops["owner"]])) { |
|
| 1890 | + $userinfo = nsp_getuserinfo(Request::GetUser()); |
|
| 1891 | + if (mapi_last_hresult() == NOERROR && isset($userinfo["fullname"])) { |
|
| 1892 | + $props[$taskprops["owner"]] = $userinfo["fullname"]; |
|
| 1893 | + } |
|
| 1894 | + } |
|
| 1895 | + mapi_setprops($mapimessage, $props); |
|
| 1896 | + |
|
| 1897 | + return true; |
|
| 1898 | + } |
|
| 1899 | + |
|
| 1900 | + /** |
|
| 1901 | + * Writes a SyncNote to MAPI. |
|
| 1902 | + * |
|
| 1903 | + * @param mixed $mapimessage |
|
| 1904 | + * @param SyncNote $note |
|
| 1905 | + * |
|
| 1906 | + * @return bool |
|
| 1907 | + */ |
|
| 1908 | + private function setNote($mapimessage, $note) { |
|
| 1909 | + // Touchdown does not send categories if all are unset or there is none. |
|
| 1910 | + // Setting it to an empty array will unset the property in KC as well |
|
| 1911 | + if (!isset($note->categories)) { |
|
| 1912 | + $note->categories = []; |
|
| 1913 | + } |
|
| 1914 | + |
|
| 1915 | + // update icon index to correspond to the color |
|
| 1916 | + if (isset($note->Color) && $note->Color > -1 && $note->Color < 5) { |
|
| 1917 | + $note->Iconindex = 768 + $note->Color; |
|
| 1918 | + } |
|
| 1919 | + |
|
| 1920 | + $this->setPropsInMAPI($mapimessage, $note, MAPIMapping::GetNoteMapping()); |
|
| 1921 | + |
|
| 1922 | + $noteprops = MAPIMapping::GetNoteProperties(); |
|
| 1923 | + $noteprops = $this->getPropIdsFromStrings($noteprops); |
|
| 1924 | + |
|
| 1925 | + // note specific properties to be set |
|
| 1926 | + $props = []; |
|
| 1927 | + $props[$noteprops["messageclass"]] = "IPM.StickyNote"; |
|
| 1928 | + // set body otherwise the note will be "broken" when editing it in outlook |
|
| 1929 | + if (isset($note->asbody)) { |
|
| 1930 | + $this->setASbody($note->asbody, $props, $noteprops); |
|
| 1931 | + } |
|
| 1932 | + |
|
| 1933 | + $props[$noteprops["internetcpid"]] = INTERNET_CPID_UTF8; |
|
| 1934 | + mapi_setprops($mapimessage, $props); |
|
| 1935 | + |
|
| 1936 | + return true; |
|
| 1937 | + } |
|
| 1938 | + |
|
| 1939 | + /*---------------------------------------------------------------------------------------------------------- |
|
| 1940 | 1940 | * HELPER |
| 1941 | 1941 | */ |
| 1942 | 1942 | |
| 1943 | - /** |
|
| 1944 | - * Returns the timestamp offset. |
|
| 1945 | - * |
|
| 1946 | - * @param string $ts |
|
| 1947 | - * |
|
| 1948 | - * @return long |
|
| 1949 | - */ |
|
| 1950 | - private function GetTZOffset($ts) { |
|
| 1951 | - $Offset = date("O", $ts); |
|
| 1952 | - |
|
| 1953 | - $Parity = $Offset < 0 ? -1 : 1; |
|
| 1954 | - $Offset = $Parity * $Offset; |
|
| 1955 | - $Offset = ($Offset - ($Offset % 100)) / 100 * 60 + $Offset % 100; |
|
| 1956 | - |
|
| 1957 | - return $Parity * $Offset; |
|
| 1958 | - } |
|
| 1959 | - |
|
| 1960 | - /** |
|
| 1961 | - * Localtime of the timestamp. |
|
| 1962 | - * |
|
| 1963 | - * @param long $time |
|
| 1964 | - * |
|
| 1965 | - * @return array |
|
| 1966 | - */ |
|
| 1967 | - private function gmtime($time) { |
|
| 1968 | - $TZOffset = $this->GetTZOffset($time); |
|
| 1969 | - |
|
| 1970 | - $t_time = $time - $TZOffset * 60; # Counter adjust for localtime() |
|
| 1971 | - |
|
| 1972 | - return localtime($t_time, 1); |
|
| 1973 | - } |
|
| 1974 | - |
|
| 1975 | - /** |
|
| 1976 | - * Sets the properties in a MAPI object according to an Sync object and a property mapping. |
|
| 1977 | - * |
|
| 1978 | - * @param mixed $mapimessage |
|
| 1979 | - * @param SyncObject $message |
|
| 1980 | - * @param array $mapping |
|
| 1981 | - * |
|
| 1982 | - * @return |
|
| 1983 | - */ |
|
| 1984 | - private function setPropsInMAPI($mapimessage, $message, $mapping) { |
|
| 1985 | - $mapiprops = $this->getPropIdsFromStrings($mapping); |
|
| 1986 | - $unsetVars = $message->getUnsetVars(); |
|
| 1987 | - $propsToDelete = []; |
|
| 1988 | - $propsToSet = []; |
|
| 1989 | - |
|
| 1990 | - foreach ($mapiprops as $asprop => $mapiprop) { |
|
| 1991 | - if (isset($message->{$asprop})) { |
|
| 1992 | - // UTF8->windows1252.. this is ok for all numerical values |
|
| 1993 | - if (mapi_prop_type($mapiprop) != PT_BINARY && mapi_prop_type($mapiprop) != PT_MV_BINARY) { |
|
| 1994 | - if (is_array($message->{$asprop})) { |
|
| 1995 | - $value = array_map("u2wi", $message->{$asprop}); |
|
| 1996 | - } |
|
| 1997 | - else { |
|
| 1998 | - $value = u2wi($message->{$asprop}); |
|
| 1999 | - } |
|
| 2000 | - } |
|
| 2001 | - else { |
|
| 2002 | - $value = $message->{$asprop}; |
|
| 2003 | - } |
|
| 2004 | - |
|
| 2005 | - // Make sure the php values are the correct type |
|
| 2006 | - switch (mapi_prop_type($mapiprop)) { |
|
| 2007 | - case PT_BINARY: |
|
| 2008 | - case PT_STRING8: |
|
| 2009 | - settype($value, "string"); |
|
| 2010 | - break; |
|
| 2011 | - |
|
| 2012 | - case PT_BOOLEAN: |
|
| 2013 | - settype($value, "boolean"); |
|
| 2014 | - break; |
|
| 2015 | - |
|
| 2016 | - case PT_SYSTIME: |
|
| 2017 | - case PT_LONG: |
|
| 2018 | - settype($value, "integer"); |
|
| 2019 | - break; |
|
| 2020 | - } |
|
| 2021 | - |
|
| 2022 | - // decode base64 value |
|
| 2023 | - if ($mapiprop == PR_RTF_COMPRESSED) { |
|
| 2024 | - $value = base64_decode($value); |
|
| 2025 | - if (strlen($value) == 0) { |
|
| 2026 | - continue; |
|
| 2027 | - } // PDA will sometimes give us an empty RTF, which we'll ignore. |
|
| 2028 | - |
|
| 2029 | - // Note that you can still remove notes because when you remove notes it gives |
|
| 2030 | - // a valid compressed RTF with nothing in it. |
|
| 2031 | - } |
|
| 2032 | - // if an "empty array" is to be saved, it the mvprop should be deleted - fixes Mantis #468 |
|
| 2033 | - if (is_array($value) && empty($value)) { |
|
| 2034 | - $propsToDelete[] = $mapiprop; |
|
| 2035 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->setPropsInMAPI(): Property '%s' to be deleted as it is an empty array", $asprop)); |
|
| 2036 | - } |
|
| 2037 | - else { |
|
| 2038 | - // all properties will be set at once |
|
| 2039 | - $propsToSet[$mapiprop] = $value; |
|
| 2040 | - } |
|
| 2041 | - } |
|
| 2042 | - elseif (in_array($asprop, $unsetVars)) { |
|
| 2043 | - $propsToDelete[] = $mapiprop; |
|
| 2044 | - } |
|
| 2045 | - } |
|
| 2046 | - |
|
| 2047 | - mapi_setprops($mapimessage, $propsToSet); |
|
| 2048 | - if (mapi_last_hresult()) { |
|
| 2049 | - SLog::Write(LOGLEVEL_WARN, sprintf("Failed to set properties, trying to set them separately. Error code was:%x", mapi_last_hresult())); |
|
| 2050 | - $this->setPropsIndividually($mapimessage, $propsToSet, $mapiprops); |
|
| 2051 | - } |
|
| 2052 | - |
|
| 2053 | - mapi_deleteprops($mapimessage, $propsToDelete); |
|
| 2054 | - |
|
| 2055 | - // clean up |
|
| 2056 | - unset($unsetVars, $propsToDelete); |
|
| 2057 | - } |
|
| 2058 | - |
|
| 2059 | - /** |
|
| 2060 | - * Sets the properties one by one in a MAPI object. |
|
| 2061 | - * |
|
| 2062 | - * @param mixed &$mapimessage |
|
| 2063 | - * @param array &$propsToSet |
|
| 2064 | - * @param array &$mapiprops |
|
| 2065 | - * |
|
| 2066 | - * @return |
|
| 2067 | - */ |
|
| 2068 | - private function setPropsIndividually(&$mapimessage, &$propsToSet, &$mapiprops) { |
|
| 2069 | - foreach ($propsToSet as $prop => $value) { |
|
| 2070 | - mapi_setprops($mapimessage, [$prop => $value]); |
|
| 2071 | - if (mapi_last_hresult()) { |
|
| 2072 | - SLog::Write(LOGLEVEL_ERROR, sprintf("Failed setting property [%s] with value [%s], error code was:%x", array_search($prop, $mapiprops), $value, mapi_last_hresult())); |
|
| 2073 | - } |
|
| 2074 | - } |
|
| 2075 | - } |
|
| 2076 | - |
|
| 2077 | - /** |
|
| 2078 | - * Gets the properties from a MAPI object and sets them in the Sync object according to mapping. |
|
| 2079 | - * |
|
| 2080 | - * @param SyncObject &$message |
|
| 2081 | - * @param mixed $mapimessage |
|
| 2082 | - * @param array $mapping |
|
| 2083 | - * |
|
| 2084 | - * @return |
|
| 2085 | - */ |
|
| 2086 | - private function getPropsFromMAPI(&$message, $mapimessage, $mapping) { |
|
| 2087 | - $messageprops = $this->getProps($mapimessage, $mapping); |
|
| 2088 | - foreach ($mapping as $asprop => $mapiprop) { |
|
| 2089 | - // Get long strings via openproperty |
|
| 2090 | - if (isset($messageprops[mapi_prop_tag(PT_ERROR, mapi_prop_id($mapiprop))])) { |
|
| 2091 | - if ($messageprops[mapi_prop_tag(PT_ERROR, mapi_prop_id($mapiprop))] == MAPI_E_NOT_ENOUGH_MEMORY_32BIT || |
|
| 2092 | - $messageprops[mapi_prop_tag(PT_ERROR, mapi_prop_id($mapiprop))] == MAPI_E_NOT_ENOUGH_MEMORY_64BIT) { |
|
| 2093 | - $messageprops[$mapiprop] = MAPIUtils::readPropStream($mapimessage, $mapiprop); |
|
| 2094 | - } |
|
| 2095 | - } |
|
| 2096 | - |
|
| 2097 | - if (isset($messageprops[$mapiprop])) { |
|
| 2098 | - if (mapi_prop_type($mapiprop) == PT_BOOLEAN) { |
|
| 2099 | - // Force to actual '0' or '1' |
|
| 2100 | - if ($messageprops[$mapiprop]) { |
|
| 2101 | - $message->{$asprop} = 1; |
|
| 2102 | - } |
|
| 2103 | - else { |
|
| 2104 | - $message->{$asprop} = 0; |
|
| 2105 | - } |
|
| 2106 | - } |
|
| 2107 | - else { |
|
| 2108 | - // Special handling for PR_MESSAGE_FLAGS |
|
| 2109 | - if ($mapiprop == PR_MESSAGE_FLAGS) { |
|
| 2110 | - $message->{$asprop} = $messageprops[$mapiprop] & 1; |
|
| 2111 | - } // only look at 'read' flag |
|
| 2112 | - elseif ($mapiprop == PR_RTF_COMPRESSED) { |
|
| 2113 | - // do not send rtf to the mobile |
|
| 2114 | - continue; |
|
| 2115 | - } |
|
| 2116 | - elseif (is_array($messageprops[$mapiprop])) { |
|
| 2117 | - $message->{$asprop} = array_map("w2u", $messageprops[$mapiprop]); |
|
| 2118 | - } |
|
| 2119 | - else { |
|
| 2120 | - if (mapi_prop_type($mapiprop) != PT_BINARY && mapi_prop_type($mapiprop) != PT_MV_BINARY) { |
|
| 2121 | - $message->{$asprop} = w2u($messageprops[$mapiprop]); |
|
| 2122 | - } |
|
| 2123 | - else { |
|
| 2124 | - $message->{$asprop} = $messageprops[$mapiprop]; |
|
| 2125 | - } |
|
| 2126 | - } |
|
| 2127 | - } |
|
| 2128 | - } |
|
| 2129 | - } |
|
| 2130 | - } |
|
| 2131 | - |
|
| 2132 | - /** |
|
| 2133 | - * Wraps getPropIdsFromStrings() calls. |
|
| 2134 | - * |
|
| 2135 | - * @param mixed &$mapiprops |
|
| 2136 | - * |
|
| 2137 | - * @return |
|
| 2138 | - */ |
|
| 2139 | - private function getPropIdsFromStrings(&$mapiprops) { |
|
| 2140 | - return getPropIdsFromStrings($this->store, $mapiprops); |
|
| 2141 | - } |
|
| 2142 | - |
|
| 2143 | - /** |
|
| 2144 | - * Wraps mapi_getprops() calls. |
|
| 2145 | - * |
|
| 2146 | - * @param mixed &$mapiprops |
|
| 2147 | - * @param mixed $mapimessage |
|
| 2148 | - * @param mixed $mapiproperties |
|
| 2149 | - * |
|
| 2150 | - * @return |
|
| 2151 | - */ |
|
| 2152 | - protected function getProps($mapimessage, &$mapiproperties) { |
|
| 2153 | - $mapiproperties = $this->getPropIdsFromStrings($mapiproperties); |
|
| 2154 | - |
|
| 2155 | - return mapi_getprops($mapimessage, $mapiproperties); |
|
| 2156 | - } |
|
| 2157 | - |
|
| 2158 | - /** |
|
| 2159 | - * Returns an GMT timezone array. |
|
| 2160 | - * |
|
| 2161 | - * @return array |
|
| 2162 | - */ |
|
| 2163 | - private function getGMTTZ() { |
|
| 2164 | - return [ |
|
| 2165 | - "bias" => 0, |
|
| 2166 | - "tzname" => "", |
|
| 2167 | - "dstendyear" => 0, |
|
| 2168 | - "dstendmonth" => 10, |
|
| 2169 | - "dstendday" => 0, |
|
| 2170 | - "dstendweek" => 5, |
|
| 2171 | - "dstendhour" => 2, |
|
| 2172 | - "dstendminute" => 0, |
|
| 2173 | - "dstendsecond" => 0, |
|
| 2174 | - "dstendmillis" => 0, |
|
| 2175 | - "stdbias" => 0, |
|
| 2176 | - "tznamedst" => "", |
|
| 2177 | - "dststartyear" => 0, |
|
| 2178 | - "dststartmonth" => 3, |
|
| 2179 | - "dststartday" => 0, |
|
| 2180 | - "dststartweek" => 5, |
|
| 2181 | - "dststarthour" => 1, |
|
| 2182 | - "dststartminute" => 0, |
|
| 2183 | - "dststartsecond" => 0, |
|
| 2184 | - "dststartmillis" => 0, |
|
| 2185 | - "dstbias" => -60, |
|
| 2186 | - ]; |
|
| 2187 | - } |
|
| 2188 | - |
|
| 2189 | - /** |
|
| 2190 | - * Unpack timezone info from MAPI. |
|
| 2191 | - * |
|
| 2192 | - * @param string $data |
|
| 2193 | - * |
|
| 2194 | - * @return array |
|
| 2195 | - */ |
|
| 2196 | - private function getTZFromMAPIBlob($data) { |
|
| 2197 | - return unpack("lbias/lstdbias/ldstbias/" . |
|
| 2198 | - "vconst1/vdstendyear/vdstendmonth/vdstendday/vdstendweek/vdstendhour/vdstendminute/vdstendsecond/vdstendmillis/" . |
|
| 2199 | - "vconst2/vdststartyear/vdststartmonth/vdststartday/vdststartweek/vdststarthour/vdststartminute/vdststartsecond/vdststartmillis", $data); |
|
| 2200 | - } |
|
| 2201 | - |
|
| 2202 | - /** |
|
| 2203 | - * Unpack timezone info from Sync. |
|
| 2204 | - * |
|
| 2205 | - * @param string $data |
|
| 2206 | - * |
|
| 2207 | - * @return array |
|
| 2208 | - */ |
|
| 2209 | - private function getTZFromSyncBlob($data) { |
|
| 2210 | - $tz = unpack("lbias/a64tzname/vdstendyear/vdstendmonth/vdstendday/vdstendweek/vdstendhour/vdstendminute/vdstendsecond/vdstendmillis/" . |
|
| 2211 | - "lstdbias/a64tznamedst/vdststartyear/vdststartmonth/vdststartday/vdststartweek/vdststarthour/vdststartminute/vdststartsecond/vdststartmillis/" . |
|
| 2212 | - "ldstbias", $data); |
|
| 2213 | - |
|
| 2214 | - // Make the structure compatible with class.recurrence.php |
|
| 2215 | - $tz["timezone"] = $tz["bias"]; |
|
| 2216 | - $tz["timezonedst"] = $tz["dstbias"]; |
|
| 2217 | - |
|
| 2218 | - return $tz; |
|
| 2219 | - } |
|
| 2220 | - |
|
| 2221 | - /** |
|
| 2222 | - * Pack timezone info for MAPI. |
|
| 2223 | - * |
|
| 2224 | - * @param array $tz |
|
| 2225 | - * |
|
| 2226 | - * @return string |
|
| 2227 | - */ |
|
| 2228 | - private function getMAPIBlobFromTZ($tz) { |
|
| 2229 | - return pack( |
|
| 2230 | - "lll" . "vvvvvvvvv" . "vvvvvvvvv", |
|
| 2231 | - $tz["bias"], |
|
| 2232 | - $tz["stdbias"], |
|
| 2233 | - $tz["dstbias"], |
|
| 2234 | - 0, |
|
| 2235 | - 0, |
|
| 2236 | - $tz["dstendmonth"], |
|
| 2237 | - $tz["dstendday"], |
|
| 2238 | - $tz["dstendweek"], |
|
| 2239 | - $tz["dstendhour"], |
|
| 2240 | - $tz["dstendminute"], |
|
| 2241 | - $tz["dstendsecond"], |
|
| 2242 | - $tz["dstendmillis"], |
|
| 2243 | - 0, |
|
| 2244 | - 0, |
|
| 2245 | - $tz["dststartmonth"], |
|
| 2246 | - $tz["dststartday"], |
|
| 2247 | - $tz["dststartweek"], |
|
| 2248 | - $tz["dststarthour"], |
|
| 2249 | - $tz["dststartminute"], |
|
| 2250 | - $tz["dststartsecond"], |
|
| 2251 | - $tz["dststartmillis"] |
|
| 2252 | - ); |
|
| 2253 | - } |
|
| 2254 | - |
|
| 2255 | - /** |
|
| 2256 | - * Checks the date to see if it is in DST, and returns correct GMT date accordingly. |
|
| 2257 | - * |
|
| 2258 | - * @param long $localtime |
|
| 2259 | - * @param array $tz |
|
| 2260 | - * |
|
| 2261 | - * @return long |
|
| 2262 | - */ |
|
| 2263 | - private function getGMTTimeByTZ($localtime, $tz) { |
|
| 2264 | - if (!isset($tz) || !is_array($tz)) { |
|
| 2265 | - return $localtime; |
|
| 2266 | - } |
|
| 2267 | - |
|
| 2268 | - if ($this->isDST($localtime, $tz)) { |
|
| 2269 | - return $localtime + $tz["bias"] * 60 + $tz["dstbias"] * 60; |
|
| 2270 | - } |
|
| 2271 | - |
|
| 2272 | - return $localtime + $tz["bias"] * 60; |
|
| 2273 | - } |
|
| 2274 | - |
|
| 2275 | - /** |
|
| 2276 | - * Returns the local time for the given GMT time, taking account of the given timezone. |
|
| 2277 | - * |
|
| 2278 | - * @param long $gmttime |
|
| 2279 | - * @param array $tz |
|
| 2280 | - * |
|
| 2281 | - * @return long |
|
| 2282 | - */ |
|
| 2283 | - private function getLocaltimeByTZ($gmttime, $tz) { |
|
| 2284 | - if (!isset($tz) || !is_array($tz)) { |
|
| 2285 | - return $gmttime; |
|
| 2286 | - } |
|
| 2287 | - |
|
| 2288 | - if ($this->isDST($gmttime - $tz["bias"] * 60, $tz)) { // may bug around the switch time because it may have to be 'gmttime - bias - dstbias' |
|
| 2289 | - return $gmttime - $tz["bias"] * 60 - $tz["dstbias"] * 60; |
|
| 2290 | - } |
|
| 2291 | - |
|
| 2292 | - return $gmttime - $tz["bias"] * 60; |
|
| 2293 | - } |
|
| 2294 | - |
|
| 2295 | - /** |
|
| 2296 | - * Returns TRUE if it is the summer and therefore DST is in effect. |
|
| 2297 | - * |
|
| 2298 | - * @param long $localtime |
|
| 2299 | - * @param array $tz |
|
| 2300 | - * |
|
| 2301 | - * @return bool |
|
| 2302 | - */ |
|
| 2303 | - private function isDST($localtime, $tz) { |
|
| 2304 | - if (!isset($tz) || !is_array($tz) || |
|
| 2305 | - !isset($tz["dstbias"]) || $tz["dstbias"] == 0 || |
|
| 2306 | - !isset($tz["dststartmonth"]) || $tz["dststartmonth"] == 0 || |
|
| 2307 | - !isset($tz["dstendmonth"]) || $tz["dstendmonth"] == 0) { |
|
| 2308 | - return false; |
|
| 2309 | - } |
|
| 2310 | - |
|
| 2311 | - $year = gmdate("Y", $localtime); |
|
| 2312 | - $start = $this->getTimestampOfWeek($year, $tz["dststartmonth"], $tz["dststartweek"], $tz["dststartday"], $tz["dststarthour"], $tz["dststartminute"], $tz["dststartsecond"]); |
|
| 2313 | - $end = $this->getTimestampOfWeek($year, $tz["dstendmonth"], $tz["dstendweek"], $tz["dstendday"], $tz["dstendhour"], $tz["dstendminute"], $tz["dstendsecond"]); |
|
| 2314 | - |
|
| 2315 | - if ($start < $end) { |
|
| 2316 | - // northern hemisphere (july = dst) |
|
| 2317 | - if ($localtime >= $start && $localtime < $end) { |
|
| 2318 | - $dst = true; |
|
| 2319 | - } |
|
| 2320 | - else { |
|
| 2321 | - $dst = false; |
|
| 2322 | - } |
|
| 2323 | - } |
|
| 2324 | - else { |
|
| 2325 | - // southern hemisphere (january = dst) |
|
| 2326 | - if ($localtime >= $end && $localtime < $start) { |
|
| 2327 | - $dst = false; |
|
| 2328 | - } |
|
| 2329 | - else { |
|
| 2330 | - $dst = true; |
|
| 2331 | - } |
|
| 2332 | - } |
|
| 2333 | - |
|
| 2334 | - return $dst; |
|
| 2335 | - } |
|
| 2336 | - |
|
| 2337 | - /** |
|
| 2338 | - * Returns the local timestamp for the $week'th $wday of $month in $year at $hour:$minute:$second. |
|
| 2339 | - * |
|
| 2340 | - * @param int $year |
|
| 2341 | - * @param int $month |
|
| 2342 | - * @param int $week |
|
| 2343 | - * @param int $wday |
|
| 2344 | - * @param int $hour |
|
| 2345 | - * @param int $minute |
|
| 2346 | - * @param int $second |
|
| 2347 | - * |
|
| 2348 | - * @return long |
|
| 2349 | - */ |
|
| 2350 | - private function getTimestampOfWeek($year, $month, $week, $wday, $hour, $minute, $second) { |
|
| 2351 | - if ($month == 0) { |
|
| 2352 | - return; |
|
| 2353 | - } |
|
| 2354 | - |
|
| 2355 | - $date = gmmktime($hour, $minute, $second, $month, 1, $year); |
|
| 2356 | - |
|
| 2357 | - // Find first day in month which matches day of the week |
|
| 2358 | - while (1) { |
|
| 2359 | - $wdaynow = gmdate("w", $date); |
|
| 2360 | - if ($wdaynow == $wday) { |
|
| 2361 | - break; |
|
| 2362 | - } |
|
| 2363 | - $date += 24 * 60 * 60; |
|
| 2364 | - } |
|
| 2365 | - |
|
| 2366 | - // Forward $week weeks (may 'overflow' into the next month) |
|
| 2367 | - $date = $date + $week * (24 * 60 * 60 * 7); |
|
| 2368 | - |
|
| 2369 | - // Reverse 'overflow'. Eg week '10' will always be the last week of the month in which the |
|
| 2370 | - // specified weekday exists |
|
| 2371 | - while (1) { |
|
| 2372 | - $monthnow = gmdate("n", $date); // gmdate returns 1-12 |
|
| 2373 | - if ($monthnow > $month) { |
|
| 2374 | - $date = $date - (24 * 7 * 60 * 60); |
|
| 2375 | - } |
|
| 2376 | - else { |
|
| 2377 | - break; |
|
| 2378 | - } |
|
| 2379 | - } |
|
| 2380 | - |
|
| 2381 | - return $date; |
|
| 2382 | - } |
|
| 2383 | - |
|
| 2384 | - /** |
|
| 2385 | - * Normalize the given timestamp to the start of the day. |
|
| 2386 | - * |
|
| 2387 | - * @param long $timestamp |
|
| 2388 | - * |
|
| 2389 | - * @return long |
|
| 2390 | - */ |
|
| 2391 | - private function getDayStartOfTimestamp($timestamp) { |
|
| 2392 | - return $timestamp - ($timestamp % (60 * 60 * 24)); |
|
| 2393 | - } |
|
| 2394 | - |
|
| 2395 | - /** |
|
| 2396 | - * Returns an SMTP address from an entry id. |
|
| 2397 | - * |
|
| 2398 | - * @param string $entryid |
|
| 2399 | - * |
|
| 2400 | - * @return string |
|
| 2401 | - */ |
|
| 2402 | - private function getSMTPAddressFromEntryID($entryid) { |
|
| 2403 | - $addrbook = $this->getAddressbook(); |
|
| 2404 | - |
|
| 2405 | - $mailuser = mapi_ab_openentry($addrbook, $entryid); |
|
| 2406 | - if (!$mailuser) { |
|
| 2407 | - return ""; |
|
| 2408 | - } |
|
| 2409 | - |
|
| 2410 | - $props = mapi_getprops($mailuser, [PR_ADDRTYPE, PR_SMTP_ADDRESS, PR_EMAIL_ADDRESS]); |
|
| 2411 | - |
|
| 2412 | - $addrtype = isset($props[PR_ADDRTYPE]) ? $props[PR_ADDRTYPE] : ""; |
|
| 2413 | - |
|
| 2414 | - if (isset($props[PR_SMTP_ADDRESS])) { |
|
| 2415 | - return $props[PR_SMTP_ADDRESS]; |
|
| 2416 | - } |
|
| 2417 | - |
|
| 2418 | - if ($addrtype == "SMTP" && isset($props[PR_EMAIL_ADDRESS])) { |
|
| 2419 | - return $props[PR_EMAIL_ADDRESS]; |
|
| 2420 | - } |
|
| 2421 | - if ($addrtype == "ZARAFA" && isset($props[PR_EMAIL_ADDRESS])) { |
|
| 2422 | - $userinfo = nsp_getuserinfo($props[PR_EMAIL_ADDRESS]); |
|
| 2423 | - if (is_array($userinfo) && isset($userinfo["primary_email"])) { |
|
| 2424 | - return $userinfo["primary_email"]; |
|
| 2425 | - } |
|
| 2426 | - } |
|
| 2427 | - |
|
| 2428 | - return ""; |
|
| 2429 | - } |
|
| 2430 | - |
|
| 2431 | - /** |
|
| 2432 | - * Returns fullname from an entryid. |
|
| 2433 | - * |
|
| 2434 | - * @param binary $entryid |
|
| 2435 | - * |
|
| 2436 | - * @return string fullname or false on error |
|
| 2437 | - */ |
|
| 2438 | - private function getFullnameFromEntryID($entryid) { |
|
| 2439 | - $addrbook = $this->getAddressbook(); |
|
| 2440 | - $mailuser = mapi_ab_openentry($addrbook, $entryid); |
|
| 2441 | - if (!$mailuser) { |
|
| 2442 | - SLog::Write(LOGLEVEL_ERROR, sprintf("Unable to get mailuser for getFullnameFromEntryID (0x%X)", mapi_last_hresult())); |
|
| 2443 | - |
|
| 2444 | - return false; |
|
| 2445 | - } |
|
| 2446 | - |
|
| 2447 | - $props = mapi_getprops($mailuser, [PR_DISPLAY_NAME]); |
|
| 2448 | - if (isset($props[PR_DISPLAY_NAME])) { |
|
| 2449 | - return $props[PR_DISPLAY_NAME]; |
|
| 2450 | - } |
|
| 2451 | - SLog::Write(LOGLEVEL_ERROR, sprintf("Unable to get fullname for getFullnameFromEntryID (0x%X)", mapi_last_hresult())); |
|
| 2452 | - |
|
| 2453 | - return false; |
|
| 2454 | - } |
|
| 2455 | - |
|
| 2456 | - /** |
|
| 2457 | - * Builds a displayname from several separated values. |
|
| 2458 | - * |
|
| 2459 | - * @param SyncContact $contact |
|
| 2460 | - * |
|
| 2461 | - * @return string |
|
| 2462 | - */ |
|
| 2463 | - private function composeDisplayName(&$contact) { |
|
| 2464 | - // Set display name and subject to a combined value of firstname and lastname |
|
| 2465 | - $cname = (isset($contact->prefix)) ? u2w($contact->prefix) . " " : ""; |
|
| 2466 | - $cname .= u2w($contact->firstname); |
|
| 2467 | - $cname .= (isset($contact->middlename)) ? " " . u2w($contact->middlename) : ""; |
|
| 2468 | - $cname .= " " . u2w($contact->lastname); |
|
| 2469 | - $cname .= (isset($contact->suffix)) ? " " . u2w($contact->suffix) : ""; |
|
| 2470 | - |
|
| 2471 | - return trim($cname); |
|
| 2472 | - } |
|
| 2473 | - |
|
| 2474 | - /** |
|
| 2475 | - * Sets all dependent properties for an email address. |
|
| 2476 | - * |
|
| 2477 | - * @param string $emailAddress |
|
| 2478 | - * @param string $displayName |
|
| 2479 | - * @param int $cnt |
|
| 2480 | - * @param array &$props |
|
| 2481 | - * @param array &$properties |
|
| 2482 | - * @param array &$nremails |
|
| 2483 | - * @param int &$abprovidertype |
|
| 2484 | - * |
|
| 2485 | - * @return |
|
| 2486 | - */ |
|
| 2487 | - private function setEmailAddress($emailAddress, $displayName, $cnt, &$props, &$properties, &$nremails, &$abprovidertype) { |
|
| 2488 | - if (isset($emailAddress)) { |
|
| 2489 | - $name = (isset($displayName)) ? $displayName : $emailAddress; |
|
| 2490 | - |
|
| 2491 | - $props[$properties["emailaddress{$cnt}"]] = $emailAddress; |
|
| 2492 | - $props[$properties["emailaddressdemail{$cnt}"]] = $emailAddress; |
|
| 2493 | - $props[$properties["emailaddressdname{$cnt}"]] = $name; |
|
| 2494 | - $props[$properties["emailaddresstype{$cnt}"]] = "SMTP"; |
|
| 2495 | - $props[$properties["emailaddressentryid{$cnt}"]] = mapi_createoneoff($name, "SMTP", $emailAddress); |
|
| 2496 | - $nremails[] = $cnt - 1; |
|
| 2497 | - $abprovidertype |= 2 ^ ($cnt - 1); |
|
| 2498 | - } |
|
| 2499 | - } |
|
| 2500 | - |
|
| 2501 | - /** |
|
| 2502 | - * Sets the properties for an address string. |
|
| 2503 | - * |
|
| 2504 | - * @param string $type which address is being set |
|
| 2505 | - * @param string $city |
|
| 2506 | - * @param string $country |
|
| 2507 | - * @param string $postalcode |
|
| 2508 | - * @param string $state |
|
| 2509 | - * @param string $street |
|
| 2510 | - * @param array &$props |
|
| 2511 | - * @param array &$properties |
|
| 2512 | - * |
|
| 2513 | - * @return |
|
| 2514 | - */ |
|
| 2515 | - private function setAddress($type, &$city, &$country, &$postalcode, &$state, &$street, &$props, &$properties) { |
|
| 2516 | - if (isset($city)) { |
|
| 2517 | - $props[$properties[$type . "city"]] = $city = u2w($city); |
|
| 2518 | - } |
|
| 2519 | - |
|
| 2520 | - if (isset($country)) { |
|
| 2521 | - $props[$properties[$type . "country"]] = $country = u2w($country); |
|
| 2522 | - } |
|
| 2523 | - |
|
| 2524 | - if (isset($postalcode)) { |
|
| 2525 | - $props[$properties[$type . "postalcode"]] = $postalcode = u2w($postalcode); |
|
| 2526 | - } |
|
| 2527 | - |
|
| 2528 | - if (isset($state)) { |
|
| 2529 | - $props[$properties[$type . "state"]] = $state = u2w($state); |
|
| 2530 | - } |
|
| 2531 | - |
|
| 2532 | - if (isset($street)) { |
|
| 2533 | - $props[$properties[$type . "street"]] = $street = u2w($street); |
|
| 2534 | - } |
|
| 2535 | - |
|
| 2536 | - // set composed address |
|
| 2537 | - $address = Utils::BuildAddressString($street, $postalcode, $city, $state, $country); |
|
| 2538 | - if ($address) { |
|
| 2539 | - $props[$properties[$type . "address"]] = $address; |
|
| 2540 | - } |
|
| 2541 | - } |
|
| 2542 | - |
|
| 2543 | - /** |
|
| 2544 | - * Sets the properties for a mailing address. |
|
| 2545 | - * |
|
| 2546 | - * @param string $city |
|
| 2547 | - * @param string $country |
|
| 2548 | - * @param string $postalcode |
|
| 2549 | - * @param string $state |
|
| 2550 | - * @param string $street |
|
| 2551 | - * @param string $address |
|
| 2552 | - * @param array &$props |
|
| 2553 | - * @param array &$properties |
|
| 2554 | - * |
|
| 2555 | - * @return |
|
| 2556 | - */ |
|
| 2557 | - private function setMailingAddress($city, $country, $postalcode, $state, $street, $address, &$props, &$properties) { |
|
| 2558 | - if (isset($city)) { |
|
| 2559 | - $props[$properties["city"]] = $city; |
|
| 2560 | - } |
|
| 2561 | - if (isset($country)) { |
|
| 2562 | - $props[$properties["country"]] = $country; |
|
| 2563 | - } |
|
| 2564 | - if (isset($postalcode)) { |
|
| 2565 | - $props[$properties["postalcode"]] = $postalcode; |
|
| 2566 | - } |
|
| 2567 | - if (isset($state)) { |
|
| 2568 | - $props[$properties["state"]] = $state; |
|
| 2569 | - } |
|
| 2570 | - if (isset($street)) { |
|
| 2571 | - $props[$properties["street"]] = $street; |
|
| 2572 | - } |
|
| 2573 | - if (isset($address)) { |
|
| 2574 | - $props[$properties["postaladdress"]] = $address; |
|
| 2575 | - } |
|
| 2576 | - } |
|
| 2577 | - |
|
| 2578 | - /** |
|
| 2579 | - * Sets data in a recurrence array. |
|
| 2580 | - * |
|
| 2581 | - * @param SyncObject $message |
|
| 2582 | - * @param array &$recur |
|
| 2583 | - * |
|
| 2584 | - * @return |
|
| 2585 | - */ |
|
| 2586 | - private function setRecurrence($message, &$recur) { |
|
| 2587 | - if (isset($message->complete)) { |
|
| 2588 | - $recur["complete"] = $message->complete; |
|
| 2589 | - } |
|
| 2590 | - |
|
| 2591 | - if (!isset($message->recurrence->interval)) { |
|
| 2592 | - $message->recurrence->interval = 1; |
|
| 2593 | - } |
|
| 2594 | - |
|
| 2595 | - // set the default value of numoccur |
|
| 2596 | - $recur["numoccur"] = 0; |
|
| 2597 | - // a place holder for recurrencetype property |
|
| 2598 | - $recur["recurrencetype"] = 0; |
|
| 2599 | - |
|
| 2600 | - switch ($message->recurrence->type) { |
|
| 2601 | - case 0: |
|
| 2602 | - $recur["type"] = 10; |
|
| 2603 | - if (isset($message->recurrence->dayofweek)) { |
|
| 2604 | - $recur["subtype"] = 1; |
|
| 2605 | - } |
|
| 2606 | - else { |
|
| 2607 | - $recur["subtype"] = 0; |
|
| 2608 | - } |
|
| 2609 | - |
|
| 2610 | - $recur["everyn"] = $message->recurrence->interval * (60 * 24); |
|
| 2611 | - $recur["recurrencetype"] = 1; |
|
| 2612 | - break; |
|
| 2613 | - |
|
| 2614 | - case 1: |
|
| 2615 | - $recur["type"] = 11; |
|
| 2616 | - $recur["subtype"] = 1; |
|
| 2617 | - $recur["everyn"] = $message->recurrence->interval; |
|
| 2618 | - $recur["recurrencetype"] = 2; |
|
| 2619 | - break; |
|
| 2620 | - |
|
| 2621 | - case 2: |
|
| 2622 | - $recur["type"] = 12; |
|
| 2623 | - $recur["subtype"] = 2; |
|
| 2624 | - $recur["everyn"] = $message->recurrence->interval; |
|
| 2625 | - $recur["recurrencetype"] = 3; |
|
| 2626 | - break; |
|
| 2627 | - |
|
| 2628 | - case 3: |
|
| 2629 | - $recur["type"] = 12; |
|
| 2630 | - $recur["subtype"] = 3; |
|
| 2631 | - $recur["everyn"] = $message->recurrence->interval; |
|
| 2632 | - $recur["recurrencetype"] = 3; |
|
| 2633 | - break; |
|
| 2634 | - |
|
| 2635 | - case 4: |
|
| 2636 | - $recur["type"] = 13; |
|
| 2637 | - $recur["subtype"] = 1; |
|
| 2638 | - $recur["everyn"] = $message->recurrence->interval * 12; |
|
| 2639 | - $recur["recurrencetype"] = 4; |
|
| 2640 | - break; |
|
| 2641 | - |
|
| 2642 | - case 5: |
|
| 2643 | - $recur["type"] = 13; |
|
| 2644 | - $recur["subtype"] = 2; |
|
| 2645 | - $recur["everyn"] = $message->recurrence->interval * 12; |
|
| 2646 | - $recur["recurrencetype"] = 4; |
|
| 2647 | - break; |
|
| 2648 | - |
|
| 2649 | - case 6: |
|
| 2650 | - $recur["type"] = 13; |
|
| 2651 | - $recur["subtype"] = 3; |
|
| 2652 | - $recur["everyn"] = $message->recurrence->interval * 12; |
|
| 2653 | - $recur["recurrencetype"] = 4; |
|
| 2654 | - break; |
|
| 2655 | - } |
|
| 2656 | - |
|
| 2657 | - // "start" and "end" are in GMT when passing to class.recurrence |
|
| 2658 | - $recur["end"] = $this->getDayStartOfTimestamp(0x7FFFFFFF); // Maximum GMT value for end by default |
|
| 2659 | - |
|
| 2660 | - if (isset($message->recurrence->until)) { |
|
| 2661 | - $recur["term"] = 0x21; |
|
| 2662 | - $recur["end"] = $message->recurrence->until; |
|
| 2663 | - } |
|
| 2664 | - elseif (isset($message->recurrence->occurrences)) { |
|
| 2665 | - $recur["term"] = 0x22; |
|
| 2666 | - $recur["numoccur"] = $message->recurrence->occurrences; |
|
| 2667 | - } |
|
| 2668 | - else { |
|
| 2669 | - $recur["term"] = 0x23; |
|
| 2670 | - } |
|
| 2671 | - |
|
| 2672 | - if (isset($message->recurrence->dayofweek)) { |
|
| 2673 | - $recur["weekdays"] = $message->recurrence->dayofweek; |
|
| 2674 | - } |
|
| 2675 | - if (isset($message->recurrence->weekofmonth)) { |
|
| 2676 | - $recur["nday"] = $message->recurrence->weekofmonth; |
|
| 2677 | - } |
|
| 2678 | - if (isset($message->recurrence->monthofyear)) { |
|
| 2679 | - // MAPI stores months as the amount of minutes until the beginning of the month in a |
|
| 2680 | - // non-leapyear. Why this is, is totally unclear. |
|
| 2681 | - $monthminutes = [0, 44640, 84960, 129600, 172800, 217440, 260640, 305280, 348480, 393120, 437760, 480960]; |
|
| 2682 | - $recur["month"] = $monthminutes[$message->recurrence->monthofyear - 1]; |
|
| 2683 | - } |
|
| 2684 | - if (isset($message->recurrence->dayofmonth)) { |
|
| 2685 | - $recur["monthday"] = $message->recurrence->dayofmonth; |
|
| 2686 | - } |
|
| 2687 | - } |
|
| 2688 | - |
|
| 2689 | - /** |
|
| 2690 | - * Extracts the email address (mailbox@host) from an email address because |
|
| 2691 | - * some devices send email address as "Firstname Lastname" <[email protected]>. |
|
| 2692 | - * |
|
| 2693 | - * @see http://developer.berlios.de/mantis/view.php?id=486 |
|
| 2694 | - * |
|
| 2695 | - * @param string $email |
|
| 2696 | - * |
|
| 2697 | - * @return string or false on error |
|
| 2698 | - */ |
|
| 2699 | - private function extractEmailAddress($email) { |
|
| 2700 | - if (!isset($this->zRFC822)) { |
|
| 2701 | - $this->zRFC822 = new Mail_RFC822(); |
|
| 2702 | - } |
|
| 2703 | - $parsedAddress = $this->zRFC822->parseAddressList($email); |
|
| 2704 | - if (!isset($parsedAddress[0]->mailbox) || !isset($parsedAddress[0]->host)) { |
|
| 2705 | - return false; |
|
| 2706 | - } |
|
| 2707 | - |
|
| 2708 | - return $parsedAddress[0]->mailbox . '@' . $parsedAddress[0]->host; |
|
| 2709 | - } |
|
| 2710 | - |
|
| 2711 | - /** |
|
| 2712 | - * Returns the message body for a required format. |
|
| 2713 | - * |
|
| 2714 | - * @param MAPIMessage $mapimessage |
|
| 2715 | - * @param int $bpReturnType |
|
| 2716 | - * @param SyncObject $message |
|
| 2717 | - * |
|
| 2718 | - * @return bool |
|
| 2719 | - */ |
|
| 2720 | - private function setMessageBodyForType($mapimessage, $bpReturnType, &$message) { |
|
| 2721 | - $truncateHtmlSafe = false; |
|
| 2722 | - // default value is PR_BODY |
|
| 2723 | - $property = PR_BODY; |
|
| 2724 | - |
|
| 2725 | - switch ($bpReturnType) { |
|
| 2726 | - case SYNC_BODYPREFERENCE_HTML: |
|
| 2727 | - $property = PR_HTML; |
|
| 2728 | - $truncateHtmlSafe = true; |
|
| 2729 | - break; |
|
| 2730 | - |
|
| 2731 | - case SYNC_BODYPREFERENCE_RTF: |
|
| 2732 | - $property = PR_RTF_COMPRESSED; |
|
| 2733 | - break; |
|
| 2734 | - |
|
| 2735 | - case SYNC_BODYPREFERENCE_MIME: |
|
| 2736 | - $stat = $this->imtoinet($mapimessage, $message); |
|
| 2737 | - if (isset($message->asbody)) { |
|
| 2738 | - $message->asbody->type = $bpReturnType; |
|
| 2739 | - } |
|
| 2740 | - |
|
| 2741 | - return $stat; |
|
| 2742 | - } |
|
| 2743 | - |
|
| 2744 | - $stream = mapi_openproperty($mapimessage, $property, IID_IStream, 0, 0); |
|
| 2745 | - if ($stream) { |
|
| 2746 | - $stat = mapi_stream_stat($stream); |
|
| 2747 | - $streamsize = $stat['cb']; |
|
| 2748 | - } |
|
| 2749 | - else { |
|
| 2750 | - $streamsize = 0; |
|
| 2751 | - } |
|
| 2752 | - |
|
| 2753 | - // set the properties according to supported AS version |
|
| 2754 | - if (Request::GetProtocolVersion() >= 12.0) { |
|
| 2755 | - $message->asbody = new SyncBaseBody(); |
|
| 2756 | - $message->asbody->type = $bpReturnType; |
|
| 2757 | - if ($bpReturnType == SYNC_BODYPREFERENCE_RTF) { |
|
| 2758 | - $body = $this->mapiReadStream($stream, $streamsize); |
|
| 2759 | - $message->asbody->data = StringStreamWrapper::Open(base64_encode($body)); |
|
| 2760 | - } |
|
| 2761 | - elseif (isset($message->internetcpid) && $bpReturnType == SYNC_BODYPREFERENCE_HTML) { |
|
| 2762 | - // if PR_HTML is UTF-8 we can stream it directly, else we have to convert to UTF-8 & wrap it |
|
| 2763 | - if ($message->internetcpid == INTERNET_CPID_UTF8) { |
|
| 2764 | - $message->asbody->data = MAPIStreamWrapper::Open($stream, $truncateHtmlSafe); |
|
| 2765 | - } |
|
| 2766 | - else { |
|
| 2767 | - $body = $this->mapiReadStream($stream, $streamsize); |
|
| 2768 | - $message->asbody->data = StringStreamWrapper::Open(Utils::ConvertCodepageStringToUtf8($message->internetcpid, $body), $truncateHtmlSafe); |
|
| 2769 | - $message->internetcpid = INTERNET_CPID_UTF8; |
|
| 2770 | - } |
|
| 2771 | - } |
|
| 2772 | - else { |
|
| 2773 | - $message->asbody->data = MAPIStreamWrapper::Open($stream); |
|
| 2774 | - } |
|
| 2775 | - $message->asbody->estimatedDataSize = $streamsize; |
|
| 2776 | - } |
|
| 2777 | - else { |
|
| 2778 | - $body = $this->mapiReadStream($stream, $streamsize); |
|
| 2779 | - $message->body = str_replace("\n", "\r\n", w2u(str_replace("\r", "", $body))); |
|
| 2780 | - $message->bodysize = $streamsize; |
|
| 2781 | - $message->bodytruncated = 0; |
|
| 2782 | - } |
|
| 2783 | - |
|
| 2784 | - return true; |
|
| 2785 | - } |
|
| 2786 | - |
|
| 2787 | - /** |
|
| 2788 | - * Reads from a mapi stream, if it's set. If not, returns an empty string. |
|
| 2789 | - * |
|
| 2790 | - * @param resource $stream |
|
| 2791 | - * @param int $size |
|
| 2792 | - * |
|
| 2793 | - * @return string |
|
| 2794 | - */ |
|
| 2795 | - private function mapiReadStream($stream, $size) { |
|
| 2796 | - if (!$stream || $size == 0) { |
|
| 2797 | - return ""; |
|
| 2798 | - } |
|
| 2799 | - |
|
| 2800 | - return mapi_stream_read($stream, $size); |
|
| 2801 | - } |
|
| 2802 | - |
|
| 2803 | - /** |
|
| 2804 | - * A wrapper for mapi_inetmapi_imtoinet function. |
|
| 2805 | - * |
|
| 2806 | - * @param MAPIMessage $mapimessage |
|
| 2807 | - * @param SyncObject $message |
|
| 2808 | - * |
|
| 2809 | - * @return bool |
|
| 2810 | - */ |
|
| 2811 | - private function imtoinet($mapimessage, &$message) { |
|
| 2812 | - $mapiEmail = mapi_getprops($mapimessage, [PR_EC_IMAP_EMAIL]); |
|
| 2813 | - $stream = false; |
|
| 2814 | - if (isset($mapiEmail[PR_EC_IMAP_EMAIL]) || MAPIUtils::GetError(PR_EC_IMAP_EMAIL, $mapiEmail) == MAPI_E_NOT_ENOUGH_MEMORY) { |
|
| 2815 | - $stream = mapi_openproperty($mapimessage, PR_EC_IMAP_EMAIL, IID_IStream, 0, 0); |
|
| 2816 | - SLog::Write(LOGLEVEL_DEBUG, "MAPIProvider->imtoinet(): using PR_EC_IMAP_EMAIL as full RFC822 message"); |
|
| 2817 | - } |
|
| 2818 | - else { |
|
| 2819 | - $addrbook = $this->getAddressbook(); |
|
| 2820 | - $stream = mapi_inetmapi_imtoinet($this->session, $addrbook, $mapimessage, ['use_tnef' => -1, 'ignore_missing_attachments' => 1]); |
|
| 2821 | - } |
|
| 2822 | - if (is_resource($stream)) { |
|
| 2823 | - $mstreamstat = mapi_stream_stat($stream); |
|
| 2824 | - $streamsize = $mstreamstat["cb"]; |
|
| 2825 | - if (isset($streamsize)) { |
|
| 2826 | - if (Request::GetProtocolVersion() >= 12.0) { |
|
| 2827 | - if (!isset($message->asbody)) { |
|
| 2828 | - $message->asbody = new SyncBaseBody(); |
|
| 2829 | - } |
|
| 2830 | - $message->asbody->data = MAPIStreamWrapper::Open($stream); |
|
| 2831 | - $message->asbody->estimatedDataSize = $streamsize; |
|
| 2832 | - $message->asbody->truncated = 0; |
|
| 2833 | - } |
|
| 2834 | - else { |
|
| 2835 | - $message->mimedata = MAPIStreamWrapper::Open($stream); |
|
| 2836 | - $message->mimesize = $streamsize; |
|
| 2837 | - $message->mimetruncated = 0; |
|
| 2838 | - } |
|
| 2839 | - unset($message->body, $message->bodytruncated); |
|
| 2840 | - |
|
| 2841 | - return true; |
|
| 2842 | - } |
|
| 2843 | - } |
|
| 2844 | - SLog::Write(LOGLEVEL_ERROR, "MAPIProvider->imtoinet(): got no stream or content from mapi_inetmapi_imtoinet()"); |
|
| 2845 | - |
|
| 2846 | - return false; |
|
| 2847 | - } |
|
| 2848 | - |
|
| 2849 | - /** |
|
| 2850 | - * Sets the message body. |
|
| 2851 | - * |
|
| 2852 | - * @param MAPIMessage $mapimessage |
|
| 2853 | - * @param ContentParameters $contentparameters |
|
| 2854 | - * @param SyncObject $message |
|
| 2855 | - */ |
|
| 2856 | - private function setMessageBody($mapimessage, $contentparameters, &$message) { |
|
| 2857 | - // get the available body preference types |
|
| 2858 | - $bpTypes = $contentparameters->GetBodyPreference(); |
|
| 2859 | - if ($bpTypes !== false) { |
|
| 2860 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("BodyPreference types: %s", implode(', ', $bpTypes))); |
|
| 2861 | - // do not send mime data if the client requests it |
|
| 2862 | - if (($contentparameters->GetMimeSupport() == SYNC_MIMESUPPORT_NEVER) && ($key = array_search(SYNC_BODYPREFERENCE_MIME, $bpTypes) !== false)) { |
|
| 2863 | - unset($bpTypes[$key]); |
|
| 2864 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("Remove mime body preference type because the device required no mime support. BodyPreference types: %s", implode(', ', $bpTypes))); |
|
| 2865 | - } |
|
| 2866 | - // get the best fitting preference type |
|
| 2867 | - $bpReturnType = Utils::GetBodyPreferenceBestMatch($bpTypes); |
|
| 2868 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("GetBodyPreferenceBestMatch: %d", $bpReturnType)); |
|
| 2869 | - $bpo = $contentparameters->BodyPreference($bpReturnType); |
|
| 2870 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("bpo: truncation size:'%d', allornone:'%d', preview:'%d'", $bpo->GetTruncationSize(), $bpo->GetAllOrNone(), $bpo->GetPreview())); |
|
| 2871 | - |
|
| 2872 | - // Android Blackberry expects a full mime message for signed emails |
|
| 2873 | - // @see https://jira.z-hub.io/projects/ZP/issues/ZP-1154 |
|
| 2874 | - // @TODO change this when refactoring |
|
| 2875 | - $props = mapi_getprops($mapimessage, [PR_MESSAGE_CLASS]); |
|
| 2876 | - if (isset($props[PR_MESSAGE_CLASS]) && |
|
| 2877 | - stripos($props[PR_MESSAGE_CLASS], 'IPM.Note.SMIME.MultipartSigned') !== false && |
|
| 2878 | - ($key = array_search(SYNC_BODYPREFERENCE_MIME, $bpTypes) !== false)) { |
|
| 2879 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->setMessageBody(): enforcing SYNC_BODYPREFERENCE_MIME type for a signed message")); |
|
| 2880 | - $bpReturnType = SYNC_BODYPREFERENCE_MIME; |
|
| 2881 | - } |
|
| 2882 | - |
|
| 2883 | - $this->setMessageBodyForType($mapimessage, $bpReturnType, $message); |
|
| 2884 | - // only set the truncation size data if device set it in request |
|
| 2885 | - if ($bpo->GetTruncationSize() != false && |
|
| 2886 | - $bpReturnType != SYNC_BODYPREFERENCE_MIME && |
|
| 2887 | - $message->asbody->estimatedDataSize > $bpo->GetTruncationSize() |
|
| 2888 | - ) { |
|
| 2889 | - // Truncated plaintext requests are used on iOS for the preview in the email list. All images and links should be removed - see https://jira.z-hub.io/browse/ZP-1025 |
|
| 2890 | - if ($bpReturnType == SYNC_BODYPREFERENCE_PLAIN) { |
|
| 2891 | - SLog::Write(LOGLEVEL_DEBUG, "MAPIProvider->setMessageBody(): truncated plain-text body requested, stripping all links and images"); |
|
| 2892 | - // Get more data because of the filtering it's most probably going down in size. It's going to be truncated to the correct size below. |
|
| 2893 | - $plainbody = stream_get_contents($message->asbody->data, $bpo->GetTruncationSize() * 5); |
|
| 2894 | - $message->asbody->data = StringStreamWrapper::Open(preg_replace('/<http(s){0,1}:\/\/.*?>/i', '', $plainbody)); |
|
| 2895 | - } |
|
| 2896 | - |
|
| 2897 | - // truncate data stream |
|
| 2898 | - ftruncate($message->asbody->data, $bpo->GetTruncationSize()); |
|
| 2899 | - $message->asbody->truncated = 1; |
|
| 2900 | - } |
|
| 2901 | - // set the preview or windows phones won't show the preview of an email |
|
| 2902 | - if (Request::GetProtocolVersion() >= 14.0 && $bpo->GetPreview()) { |
|
| 2903 | - $message->asbody->preview = Utils::Utf8_truncate(MAPIUtils::readPropStream($mapimessage, PR_BODY), $bpo->GetPreview()); |
|
| 2904 | - } |
|
| 2905 | - } |
|
| 2906 | - else { |
|
| 2907 | - // Override 'body' for truncation |
|
| 2908 | - $truncsize = Utils::GetTruncSize($contentparameters->GetTruncation()); |
|
| 2909 | - $this->setMessageBodyForType($mapimessage, SYNC_BODYPREFERENCE_PLAIN, $message); |
|
| 2910 | - |
|
| 2911 | - if ($message->bodysize > $truncsize) { |
|
| 2912 | - $message->body = Utils::Utf8_truncate($message->body, $truncsize); |
|
| 2913 | - $message->bodytruncated = 1; |
|
| 2914 | - } |
|
| 2915 | - |
|
| 2916 | - if (!isset($message->body) || strlen($message->body) == 0) { |
|
| 2917 | - $message->body = " "; |
|
| 2918 | - } |
|
| 2919 | - |
|
| 2920 | - if ($contentparameters->GetMimeSupport() == SYNC_MIMESUPPORT_ALWAYS) { |
|
| 2921 | - // set the html body for iphone in AS 2.5 version |
|
| 2922 | - $this->imtoinet($mapimessage, $message); |
|
| 2923 | - } |
|
| 2924 | - } |
|
| 2925 | - } |
|
| 2926 | - |
|
| 2927 | - /** |
|
| 2928 | - * Sets properties for an email message. |
|
| 2929 | - * |
|
| 2930 | - * @param mixed $mapimessage |
|
| 2931 | - * @param SyncMail $message |
|
| 2932 | - */ |
|
| 2933 | - private function setFlag($mapimessage, &$message) { |
|
| 2934 | - // do nothing if protocol version is lower than 12.0 as flags haven't been defined before |
|
| 2935 | - if (Request::GetProtocolVersion() < 12.0) { |
|
| 2936 | - return; |
|
| 2937 | - } |
|
| 2938 | - |
|
| 2939 | - $message->flag = new SyncMailFlags(); |
|
| 2940 | - |
|
| 2941 | - $this->getPropsFromMAPI($message->flag, $mapimessage, MAPIMapping::GetMailFlagsMapping()); |
|
| 2942 | - } |
|
| 2943 | - |
|
| 2944 | - /** |
|
| 2945 | - * Sets information from SyncBaseBody type for a MAPI message. |
|
| 2946 | - * |
|
| 2947 | - * @param SyncBaseBody $asbody |
|
| 2948 | - * @param array $props |
|
| 2949 | - * @param array $appointmentprops |
|
| 2950 | - */ |
|
| 2951 | - private function setASbody($asbody, &$props, $appointmentprops) { |
|
| 2952 | - // TODO: fix checking for the length |
|
| 2953 | - if (isset($asbody->type, $asbody->data) /* && strlen($asbody->data) > 0 */) { |
|
| 2954 | - switch ($asbody->type) { |
|
| 2955 | - case SYNC_BODYPREFERENCE_PLAIN: |
|
| 2956 | - default: |
|
| 2957 | - // set plain body if the type is not in valid range |
|
| 2958 | - $props[$appointmentprops["body"]] = stream_get_contents($asbody->data); |
|
| 2959 | - break; |
|
| 2960 | - |
|
| 2961 | - case SYNC_BODYPREFERENCE_HTML: |
|
| 2962 | - $props[$appointmentprops["html"]] = stream_get_contents($asbody->data); |
|
| 2963 | - break; |
|
| 2964 | - |
|
| 2965 | - case SYNC_BODYPREFERENCE_RTF: |
|
| 2966 | - break; |
|
| 2967 | - |
|
| 2968 | - case SYNC_BODYPREFERENCE_MIME: |
|
| 2969 | - break; |
|
| 2970 | - } |
|
| 2971 | - } |
|
| 2972 | - else { |
|
| 2973 | - SLog::Write(LOGLEVEL_DEBUG, "MAPIProvider->setASbody either type or data are not set. Setting to empty body"); |
|
| 2974 | - $props[$appointmentprops["body"]] = ""; |
|
| 2975 | - } |
|
| 2976 | - } |
|
| 2977 | - |
|
| 2978 | - /** |
|
| 2979 | - * Get MAPI addressbook object. |
|
| 2980 | - * |
|
| 2981 | - * @return MAPIAddressbook object to be used with mapi_ab_* or false on failure |
|
| 2982 | - */ |
|
| 2983 | - private function getAddressbook() { |
|
| 2984 | - if (isset($this->addressbook) && $this->addressbook) { |
|
| 2985 | - return $this->addressbook; |
|
| 2986 | - } |
|
| 2987 | - $this->addressbook = mapi_openaddressbook($this->session); |
|
| 2988 | - $result = mapi_last_hresult(); |
|
| 2989 | - if ($result && $this->addressbook === false) { |
|
| 2990 | - SLog::Write(LOGLEVEL_ERROR, sprintf("MAPIProvider->getAddressbook error opening addressbook 0x%X", $result)); |
|
| 2991 | - |
|
| 2992 | - return false; |
|
| 2993 | - } |
|
| 2994 | - |
|
| 2995 | - return $this->addressbook; |
|
| 2996 | - } |
|
| 2997 | - |
|
| 2998 | - /** |
|
| 2999 | - * Gets the required store properties. |
|
| 3000 | - * |
|
| 3001 | - * @return array |
|
| 3002 | - */ |
|
| 3003 | - public function GetStoreProps() { |
|
| 3004 | - if (!isset($this->storeProps) || empty($this->storeProps)) { |
|
| 3005 | - SLog::Write(LOGLEVEL_DEBUG, "MAPIProvider->GetStoreProps(): Getting store properties."); |
|
| 3006 | - $this->storeProps = mapi_getprops($this->store, [PR_IPM_SUBTREE_ENTRYID, PR_IPM_OUTBOX_ENTRYID, PR_IPM_WASTEBASKET_ENTRYID, PR_IPM_SENTMAIL_ENTRYID, PR_ENTRYID, PR_IPM_PUBLIC_FOLDERS_ENTRYID, PR_IPM_FAVORITES_ENTRYID, PR_MAILBOX_OWNER_ENTRYID]); |
|
| 3007 | - // make sure all properties are set |
|
| 3008 | - if (!isset($this->storeProps[PR_IPM_WASTEBASKET_ENTRYID])) { |
|
| 3009 | - $this->storeProps[PR_IPM_WASTEBASKET_ENTRYID] = false; |
|
| 3010 | - } |
|
| 3011 | - if (!isset($this->storeProps[PR_IPM_SENTMAIL_ENTRYID])) { |
|
| 3012 | - $this->storeProps[PR_IPM_SENTMAIL_ENTRYID] = false; |
|
| 3013 | - } |
|
| 3014 | - if (!isset($this->storeProps[PR_IPM_OUTBOX_ENTRYID])) { |
|
| 3015 | - $this->storeProps[PR_IPM_OUTBOX_ENTRYID] = false; |
|
| 3016 | - } |
|
| 3017 | - if (!isset($this->storeProps[PR_IPM_PUBLIC_FOLDERS_ENTRYID])) { |
|
| 3018 | - $this->storeProps[PR_IPM_PUBLIC_FOLDERS_ENTRYID] = false; |
|
| 3019 | - } |
|
| 3020 | - } |
|
| 3021 | - |
|
| 3022 | - return $this->storeProps; |
|
| 3023 | - } |
|
| 3024 | - |
|
| 3025 | - /** |
|
| 3026 | - * Gets the required inbox properties. |
|
| 3027 | - * |
|
| 3028 | - * @return array |
|
| 3029 | - */ |
|
| 3030 | - public function GetInboxProps() { |
|
| 3031 | - if (!isset($this->inboxProps) || empty($this->inboxProps)) { |
|
| 3032 | - SLog::Write(LOGLEVEL_DEBUG, "MAPIProvider->GetInboxProps(): Getting inbox properties."); |
|
| 3033 | - $this->inboxProps = []; |
|
| 3034 | - $inbox = mapi_msgstore_getreceivefolder($this->store); |
|
| 3035 | - if ($inbox) { |
|
| 3036 | - $this->inboxProps = mapi_getprops($inbox, [PR_ENTRYID, PR_IPM_DRAFTS_ENTRYID, PR_IPM_TASK_ENTRYID, PR_IPM_APPOINTMENT_ENTRYID, PR_IPM_CONTACT_ENTRYID, PR_IPM_NOTE_ENTRYID, PR_IPM_JOURNAL_ENTRYID]); |
|
| 3037 | - // make sure all properties are set |
|
| 3038 | - if (!isset($this->inboxProps[PR_ENTRYID])) { |
|
| 3039 | - $this->inboxProps[PR_ENTRYID] = false; |
|
| 3040 | - } |
|
| 3041 | - if (!isset($this->inboxProps[PR_IPM_DRAFTS_ENTRYID])) { |
|
| 3042 | - $this->inboxProps[PR_IPM_DRAFTS_ENTRYID] = false; |
|
| 3043 | - } |
|
| 3044 | - if (!isset($this->inboxProps[PR_IPM_TASK_ENTRYID])) { |
|
| 3045 | - $this->inboxProps[PR_IPM_TASK_ENTRYID] = false; |
|
| 3046 | - } |
|
| 3047 | - if (!isset($this->inboxProps[PR_IPM_APPOINTMENT_ENTRYID])) { |
|
| 3048 | - $this->inboxProps[PR_IPM_APPOINTMENT_ENTRYID] = false; |
|
| 3049 | - } |
|
| 3050 | - if (!isset($this->inboxProps[PR_IPM_CONTACT_ENTRYID])) { |
|
| 3051 | - $this->inboxProps[PR_IPM_CONTACT_ENTRYID] = false; |
|
| 3052 | - } |
|
| 3053 | - if (!isset($this->inboxProps[PR_IPM_NOTE_ENTRYID])) { |
|
| 3054 | - $this->inboxProps[PR_IPM_NOTE_ENTRYID] = false; |
|
| 3055 | - } |
|
| 3056 | - if (!isset($this->inboxProps[PR_IPM_JOURNAL_ENTRYID])) { |
|
| 3057 | - $this->inboxProps[PR_IPM_JOURNAL_ENTRYID] = false; |
|
| 3058 | - } |
|
| 3059 | - } |
|
| 3060 | - } |
|
| 3061 | - |
|
| 3062 | - return $this->inboxProps; |
|
| 3063 | - } |
|
| 3064 | - |
|
| 3065 | - /** |
|
| 3066 | - * Gets the required store root properties. |
|
| 3067 | - * |
|
| 3068 | - * @return array |
|
| 3069 | - */ |
|
| 3070 | - private function getRootProps() { |
|
| 3071 | - if (!isset($this->rootProps)) { |
|
| 3072 | - $root = mapi_msgstore_openentry($this->store, null); |
|
| 3073 | - $this->rootProps = mapi_getprops($root, [PR_IPM_OL2007_ENTRYIDS]); |
|
| 3074 | - } |
|
| 3075 | - |
|
| 3076 | - return $this->rootProps; |
|
| 3077 | - } |
|
| 3078 | - |
|
| 3079 | - /** |
|
| 3080 | - * Returns an array with entryids of some special folders. |
|
| 3081 | - * |
|
| 3082 | - * @return array |
|
| 3083 | - */ |
|
| 3084 | - private function getSpecialFoldersData() { |
|
| 3085 | - // The persist data of an entry in PR_IPM_OL2007_ENTRYIDS consists of: |
|
| 3086 | - // PersistId - e.g. RSF_PID_SUGGESTED_CONTACTS (2 bytes) |
|
| 3087 | - // DataElementsSize - size of DataElements field (2 bytes) |
|
| 3088 | - // DataElements - array of PersistElement structures (variable size) |
|
| 3089 | - // PersistElement Structure consists of |
|
| 3090 | - // ElementID - e.g. RSF_ELID_ENTRYID (2 bytes) |
|
| 3091 | - // ElementDataSize - size of ElementData (2 bytes) |
|
| 3092 | - // ElementData - The data for the special folder identified by the PersistID (variable size) |
|
| 3093 | - if (empty($this->specialFoldersData)) { |
|
| 3094 | - $this->specialFoldersData = []; |
|
| 3095 | - $rootProps = $this->getRootProps(); |
|
| 3096 | - if (isset($rootProps[PR_IPM_OL2007_ENTRYIDS])) { |
|
| 3097 | - $persistData = $rootProps[PR_IPM_OL2007_ENTRYIDS]; |
|
| 3098 | - while (strlen($persistData) > 0) { |
|
| 3099 | - // PERSIST_SENTINEL marks the end of the persist data |
|
| 3100 | - if (strlen($persistData) == 4 && $persistData == PERSIST_SENTINEL) { |
|
| 3101 | - break; |
|
| 3102 | - } |
|
| 3103 | - $unpackedData = unpack("vdataSize/velementID/velDataSize", substr($persistData, 2, 6)); |
|
| 3104 | - if (isset($unpackedData['dataSize'], $unpackedData['elementID']) && $unpackedData['elementID'] == RSF_ELID_ENTRYID && isset($unpackedData['elDataSize'])) { |
|
| 3105 | - $this->specialFoldersData[] = substr($persistData, 8, $unpackedData['elDataSize']); |
|
| 3106 | - // Add PersistId and DataElementsSize lengths to the data size as they're not part of it |
|
| 3107 | - $persistData = substr($persistData, $unpackedData['dataSize'] + 4); |
|
| 3108 | - } |
|
| 3109 | - else { |
|
| 3110 | - SLog::Write(LOGLEVEL_INFO, "MAPIProvider->getSpecialFoldersData(): persistent data is not valid"); |
|
| 3111 | - break; |
|
| 3112 | - } |
|
| 3113 | - } |
|
| 3114 | - } |
|
| 3115 | - } |
|
| 3116 | - |
|
| 3117 | - return $this->specialFoldersData; |
|
| 3118 | - } |
|
| 3119 | - |
|
| 3120 | - /** |
|
| 3121 | - * Extracts email address from PR_SEARCH_KEY property if possible. |
|
| 3122 | - * |
|
| 3123 | - * @param string $searchKey |
|
| 3124 | - * |
|
| 3125 | - * @see https://jira.z-hub.io/browse/ZP-1178 |
|
| 3126 | - * |
|
| 3127 | - * @return string |
|
| 3128 | - */ |
|
| 3129 | - private function getEmailAddressFromSearchKey($searchKey) { |
|
| 3130 | - if (strpos($searchKey, ':') !== false && strpos($searchKey, '@') !== false) { |
|
| 3131 | - SLog::Write(LOGLEVEL_INFO, "MAPIProvider->getEmailAddressFromSearchKey(): fall back to PR_SEARCH_KEY or PR_SENT_REPRESENTING_SEARCH_KEY to resolve user and get email address"); |
|
| 3132 | - |
|
| 3133 | - return trim(strtolower(explode(':', $searchKey)[1])); |
|
| 3134 | - } |
|
| 3135 | - |
|
| 3136 | - return ""; |
|
| 3137 | - } |
|
| 3138 | - |
|
| 3139 | - /** |
|
| 3140 | - * Returns categories for a message. |
|
| 3141 | - * |
|
| 3142 | - * @param binary $parentsourcekey |
|
| 3143 | - * @param binary $sourcekey |
|
| 3144 | - * |
|
| 3145 | - * @return array or false on failure |
|
| 3146 | - */ |
|
| 3147 | - public function GetMessageCategories($parentsourcekey, $sourcekey) { |
|
| 3148 | - $entryid = mapi_msgstore_entryidfromsourcekey($this->store, $parentsourcekey, $sourcekey); |
|
| 3149 | - if (!$entryid) { |
|
| 3150 | - SLog::Write(LOGLEVEL_INFO, sprintf("MAPIProvider->GetMessageCategories(): Couldn't retrieve message, sourcekey: '%s', parentsourcekey: '%s'", bin2hex($sourcekey), bin2hex($parentsourcekey))); |
|
| 3151 | - |
|
| 3152 | - return false; |
|
| 3153 | - } |
|
| 3154 | - $mapimessage = mapi_msgstore_openentry($this->store, $entryid); |
|
| 3155 | - $emailMapping = MAPIMapping::GetEmailMapping(); |
|
| 3156 | - $emailMapping = ["categories" => $emailMapping["categories"]]; |
|
| 3157 | - $messageCategories = $this->getProps($mapimessage, $emailMapping); |
|
| 3158 | - if (isset($messageCategories[$emailMapping["categories"]]) && is_array($messageCategories[$emailMapping["categories"]])) { |
|
| 3159 | - return $messageCategories[$emailMapping["categories"]]; |
|
| 3160 | - } |
|
| 3161 | - |
|
| 3162 | - return false; |
|
| 3163 | - } |
|
| 1943 | + /** |
|
| 1944 | + * Returns the timestamp offset. |
|
| 1945 | + * |
|
| 1946 | + * @param string $ts |
|
| 1947 | + * |
|
| 1948 | + * @return long |
|
| 1949 | + */ |
|
| 1950 | + private function GetTZOffset($ts) { |
|
| 1951 | + $Offset = date("O", $ts); |
|
| 1952 | + |
|
| 1953 | + $Parity = $Offset < 0 ? -1 : 1; |
|
| 1954 | + $Offset = $Parity * $Offset; |
|
| 1955 | + $Offset = ($Offset - ($Offset % 100)) / 100 * 60 + $Offset % 100; |
|
| 1956 | + |
|
| 1957 | + return $Parity * $Offset; |
|
| 1958 | + } |
|
| 1959 | + |
|
| 1960 | + /** |
|
| 1961 | + * Localtime of the timestamp. |
|
| 1962 | + * |
|
| 1963 | + * @param long $time |
|
| 1964 | + * |
|
| 1965 | + * @return array |
|
| 1966 | + */ |
|
| 1967 | + private function gmtime($time) { |
|
| 1968 | + $TZOffset = $this->GetTZOffset($time); |
|
| 1969 | + |
|
| 1970 | + $t_time = $time - $TZOffset * 60; # Counter adjust for localtime() |
|
| 1971 | + |
|
| 1972 | + return localtime($t_time, 1); |
|
| 1973 | + } |
|
| 1974 | + |
|
| 1975 | + /** |
|
| 1976 | + * Sets the properties in a MAPI object according to an Sync object and a property mapping. |
|
| 1977 | + * |
|
| 1978 | + * @param mixed $mapimessage |
|
| 1979 | + * @param SyncObject $message |
|
| 1980 | + * @param array $mapping |
|
| 1981 | + * |
|
| 1982 | + * @return |
|
| 1983 | + */ |
|
| 1984 | + private function setPropsInMAPI($mapimessage, $message, $mapping) { |
|
| 1985 | + $mapiprops = $this->getPropIdsFromStrings($mapping); |
|
| 1986 | + $unsetVars = $message->getUnsetVars(); |
|
| 1987 | + $propsToDelete = []; |
|
| 1988 | + $propsToSet = []; |
|
| 1989 | + |
|
| 1990 | + foreach ($mapiprops as $asprop => $mapiprop) { |
|
| 1991 | + if (isset($message->{$asprop})) { |
|
| 1992 | + // UTF8->windows1252.. this is ok for all numerical values |
|
| 1993 | + if (mapi_prop_type($mapiprop) != PT_BINARY && mapi_prop_type($mapiprop) != PT_MV_BINARY) { |
|
| 1994 | + if (is_array($message->{$asprop})) { |
|
| 1995 | + $value = array_map("u2wi", $message->{$asprop}); |
|
| 1996 | + } |
|
| 1997 | + else { |
|
| 1998 | + $value = u2wi($message->{$asprop}); |
|
| 1999 | + } |
|
| 2000 | + } |
|
| 2001 | + else { |
|
| 2002 | + $value = $message->{$asprop}; |
|
| 2003 | + } |
|
| 2004 | + |
|
| 2005 | + // Make sure the php values are the correct type |
|
| 2006 | + switch (mapi_prop_type($mapiprop)) { |
|
| 2007 | + case PT_BINARY: |
|
| 2008 | + case PT_STRING8: |
|
| 2009 | + settype($value, "string"); |
|
| 2010 | + break; |
|
| 2011 | + |
|
| 2012 | + case PT_BOOLEAN: |
|
| 2013 | + settype($value, "boolean"); |
|
| 2014 | + break; |
|
| 2015 | + |
|
| 2016 | + case PT_SYSTIME: |
|
| 2017 | + case PT_LONG: |
|
| 2018 | + settype($value, "integer"); |
|
| 2019 | + break; |
|
| 2020 | + } |
|
| 2021 | + |
|
| 2022 | + // decode base64 value |
|
| 2023 | + if ($mapiprop == PR_RTF_COMPRESSED) { |
|
| 2024 | + $value = base64_decode($value); |
|
| 2025 | + if (strlen($value) == 0) { |
|
| 2026 | + continue; |
|
| 2027 | + } // PDA will sometimes give us an empty RTF, which we'll ignore. |
|
| 2028 | + |
|
| 2029 | + // Note that you can still remove notes because when you remove notes it gives |
|
| 2030 | + // a valid compressed RTF with nothing in it. |
|
| 2031 | + } |
|
| 2032 | + // if an "empty array" is to be saved, it the mvprop should be deleted - fixes Mantis #468 |
|
| 2033 | + if (is_array($value) && empty($value)) { |
|
| 2034 | + $propsToDelete[] = $mapiprop; |
|
| 2035 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->setPropsInMAPI(): Property '%s' to be deleted as it is an empty array", $asprop)); |
|
| 2036 | + } |
|
| 2037 | + else { |
|
| 2038 | + // all properties will be set at once |
|
| 2039 | + $propsToSet[$mapiprop] = $value; |
|
| 2040 | + } |
|
| 2041 | + } |
|
| 2042 | + elseif (in_array($asprop, $unsetVars)) { |
|
| 2043 | + $propsToDelete[] = $mapiprop; |
|
| 2044 | + } |
|
| 2045 | + } |
|
| 2046 | + |
|
| 2047 | + mapi_setprops($mapimessage, $propsToSet); |
|
| 2048 | + if (mapi_last_hresult()) { |
|
| 2049 | + SLog::Write(LOGLEVEL_WARN, sprintf("Failed to set properties, trying to set them separately. Error code was:%x", mapi_last_hresult())); |
|
| 2050 | + $this->setPropsIndividually($mapimessage, $propsToSet, $mapiprops); |
|
| 2051 | + } |
|
| 2052 | + |
|
| 2053 | + mapi_deleteprops($mapimessage, $propsToDelete); |
|
| 2054 | + |
|
| 2055 | + // clean up |
|
| 2056 | + unset($unsetVars, $propsToDelete); |
|
| 2057 | + } |
|
| 2058 | + |
|
| 2059 | + /** |
|
| 2060 | + * Sets the properties one by one in a MAPI object. |
|
| 2061 | + * |
|
| 2062 | + * @param mixed &$mapimessage |
|
| 2063 | + * @param array &$propsToSet |
|
| 2064 | + * @param array &$mapiprops |
|
| 2065 | + * |
|
| 2066 | + * @return |
|
| 2067 | + */ |
|
| 2068 | + private function setPropsIndividually(&$mapimessage, &$propsToSet, &$mapiprops) { |
|
| 2069 | + foreach ($propsToSet as $prop => $value) { |
|
| 2070 | + mapi_setprops($mapimessage, [$prop => $value]); |
|
| 2071 | + if (mapi_last_hresult()) { |
|
| 2072 | + SLog::Write(LOGLEVEL_ERROR, sprintf("Failed setting property [%s] with value [%s], error code was:%x", array_search($prop, $mapiprops), $value, mapi_last_hresult())); |
|
| 2073 | + } |
|
| 2074 | + } |
|
| 2075 | + } |
|
| 2076 | + |
|
| 2077 | + /** |
|
| 2078 | + * Gets the properties from a MAPI object and sets them in the Sync object according to mapping. |
|
| 2079 | + * |
|
| 2080 | + * @param SyncObject &$message |
|
| 2081 | + * @param mixed $mapimessage |
|
| 2082 | + * @param array $mapping |
|
| 2083 | + * |
|
| 2084 | + * @return |
|
| 2085 | + */ |
|
| 2086 | + private function getPropsFromMAPI(&$message, $mapimessage, $mapping) { |
|
| 2087 | + $messageprops = $this->getProps($mapimessage, $mapping); |
|
| 2088 | + foreach ($mapping as $asprop => $mapiprop) { |
|
| 2089 | + // Get long strings via openproperty |
|
| 2090 | + if (isset($messageprops[mapi_prop_tag(PT_ERROR, mapi_prop_id($mapiprop))])) { |
|
| 2091 | + if ($messageprops[mapi_prop_tag(PT_ERROR, mapi_prop_id($mapiprop))] == MAPI_E_NOT_ENOUGH_MEMORY_32BIT || |
|
| 2092 | + $messageprops[mapi_prop_tag(PT_ERROR, mapi_prop_id($mapiprop))] == MAPI_E_NOT_ENOUGH_MEMORY_64BIT) { |
|
| 2093 | + $messageprops[$mapiprop] = MAPIUtils::readPropStream($mapimessage, $mapiprop); |
|
| 2094 | + } |
|
| 2095 | + } |
|
| 2096 | + |
|
| 2097 | + if (isset($messageprops[$mapiprop])) { |
|
| 2098 | + if (mapi_prop_type($mapiprop) == PT_BOOLEAN) { |
|
| 2099 | + // Force to actual '0' or '1' |
|
| 2100 | + if ($messageprops[$mapiprop]) { |
|
| 2101 | + $message->{$asprop} = 1; |
|
| 2102 | + } |
|
| 2103 | + else { |
|
| 2104 | + $message->{$asprop} = 0; |
|
| 2105 | + } |
|
| 2106 | + } |
|
| 2107 | + else { |
|
| 2108 | + // Special handling for PR_MESSAGE_FLAGS |
|
| 2109 | + if ($mapiprop == PR_MESSAGE_FLAGS) { |
|
| 2110 | + $message->{$asprop} = $messageprops[$mapiprop] & 1; |
|
| 2111 | + } // only look at 'read' flag |
|
| 2112 | + elseif ($mapiprop == PR_RTF_COMPRESSED) { |
|
| 2113 | + // do not send rtf to the mobile |
|
| 2114 | + continue; |
|
| 2115 | + } |
|
| 2116 | + elseif (is_array($messageprops[$mapiprop])) { |
|
| 2117 | + $message->{$asprop} = array_map("w2u", $messageprops[$mapiprop]); |
|
| 2118 | + } |
|
| 2119 | + else { |
|
| 2120 | + if (mapi_prop_type($mapiprop) != PT_BINARY && mapi_prop_type($mapiprop) != PT_MV_BINARY) { |
|
| 2121 | + $message->{$asprop} = w2u($messageprops[$mapiprop]); |
|
| 2122 | + } |
|
| 2123 | + else { |
|
| 2124 | + $message->{$asprop} = $messageprops[$mapiprop]; |
|
| 2125 | + } |
|
| 2126 | + } |
|
| 2127 | + } |
|
| 2128 | + } |
|
| 2129 | + } |
|
| 2130 | + } |
|
| 2131 | + |
|
| 2132 | + /** |
|
| 2133 | + * Wraps getPropIdsFromStrings() calls. |
|
| 2134 | + * |
|
| 2135 | + * @param mixed &$mapiprops |
|
| 2136 | + * |
|
| 2137 | + * @return |
|
| 2138 | + */ |
|
| 2139 | + private function getPropIdsFromStrings(&$mapiprops) { |
|
| 2140 | + return getPropIdsFromStrings($this->store, $mapiprops); |
|
| 2141 | + } |
|
| 2142 | + |
|
| 2143 | + /** |
|
| 2144 | + * Wraps mapi_getprops() calls. |
|
| 2145 | + * |
|
| 2146 | + * @param mixed &$mapiprops |
|
| 2147 | + * @param mixed $mapimessage |
|
| 2148 | + * @param mixed $mapiproperties |
|
| 2149 | + * |
|
| 2150 | + * @return |
|
| 2151 | + */ |
|
| 2152 | + protected function getProps($mapimessage, &$mapiproperties) { |
|
| 2153 | + $mapiproperties = $this->getPropIdsFromStrings($mapiproperties); |
|
| 2154 | + |
|
| 2155 | + return mapi_getprops($mapimessage, $mapiproperties); |
|
| 2156 | + } |
|
| 2157 | + |
|
| 2158 | + /** |
|
| 2159 | + * Returns an GMT timezone array. |
|
| 2160 | + * |
|
| 2161 | + * @return array |
|
| 2162 | + */ |
|
| 2163 | + private function getGMTTZ() { |
|
| 2164 | + return [ |
|
| 2165 | + "bias" => 0, |
|
| 2166 | + "tzname" => "", |
|
| 2167 | + "dstendyear" => 0, |
|
| 2168 | + "dstendmonth" => 10, |
|
| 2169 | + "dstendday" => 0, |
|
| 2170 | + "dstendweek" => 5, |
|
| 2171 | + "dstendhour" => 2, |
|
| 2172 | + "dstendminute" => 0, |
|
| 2173 | + "dstendsecond" => 0, |
|
| 2174 | + "dstendmillis" => 0, |
|
| 2175 | + "stdbias" => 0, |
|
| 2176 | + "tznamedst" => "", |
|
| 2177 | + "dststartyear" => 0, |
|
| 2178 | + "dststartmonth" => 3, |
|
| 2179 | + "dststartday" => 0, |
|
| 2180 | + "dststartweek" => 5, |
|
| 2181 | + "dststarthour" => 1, |
|
| 2182 | + "dststartminute" => 0, |
|
| 2183 | + "dststartsecond" => 0, |
|
| 2184 | + "dststartmillis" => 0, |
|
| 2185 | + "dstbias" => -60, |
|
| 2186 | + ]; |
|
| 2187 | + } |
|
| 2188 | + |
|
| 2189 | + /** |
|
| 2190 | + * Unpack timezone info from MAPI. |
|
| 2191 | + * |
|
| 2192 | + * @param string $data |
|
| 2193 | + * |
|
| 2194 | + * @return array |
|
| 2195 | + */ |
|
| 2196 | + private function getTZFromMAPIBlob($data) { |
|
| 2197 | + return unpack("lbias/lstdbias/ldstbias/" . |
|
| 2198 | + "vconst1/vdstendyear/vdstendmonth/vdstendday/vdstendweek/vdstendhour/vdstendminute/vdstendsecond/vdstendmillis/" . |
|
| 2199 | + "vconst2/vdststartyear/vdststartmonth/vdststartday/vdststartweek/vdststarthour/vdststartminute/vdststartsecond/vdststartmillis", $data); |
|
| 2200 | + } |
|
| 2201 | + |
|
| 2202 | + /** |
|
| 2203 | + * Unpack timezone info from Sync. |
|
| 2204 | + * |
|
| 2205 | + * @param string $data |
|
| 2206 | + * |
|
| 2207 | + * @return array |
|
| 2208 | + */ |
|
| 2209 | + private function getTZFromSyncBlob($data) { |
|
| 2210 | + $tz = unpack("lbias/a64tzname/vdstendyear/vdstendmonth/vdstendday/vdstendweek/vdstendhour/vdstendminute/vdstendsecond/vdstendmillis/" . |
|
| 2211 | + "lstdbias/a64tznamedst/vdststartyear/vdststartmonth/vdststartday/vdststartweek/vdststarthour/vdststartminute/vdststartsecond/vdststartmillis/" . |
|
| 2212 | + "ldstbias", $data); |
|
| 2213 | + |
|
| 2214 | + // Make the structure compatible with class.recurrence.php |
|
| 2215 | + $tz["timezone"] = $tz["bias"]; |
|
| 2216 | + $tz["timezonedst"] = $tz["dstbias"]; |
|
| 2217 | + |
|
| 2218 | + return $tz; |
|
| 2219 | + } |
|
| 2220 | + |
|
| 2221 | + /** |
|
| 2222 | + * Pack timezone info for MAPI. |
|
| 2223 | + * |
|
| 2224 | + * @param array $tz |
|
| 2225 | + * |
|
| 2226 | + * @return string |
|
| 2227 | + */ |
|
| 2228 | + private function getMAPIBlobFromTZ($tz) { |
|
| 2229 | + return pack( |
|
| 2230 | + "lll" . "vvvvvvvvv" . "vvvvvvvvv", |
|
| 2231 | + $tz["bias"], |
|
| 2232 | + $tz["stdbias"], |
|
| 2233 | + $tz["dstbias"], |
|
| 2234 | + 0, |
|
| 2235 | + 0, |
|
| 2236 | + $tz["dstendmonth"], |
|
| 2237 | + $tz["dstendday"], |
|
| 2238 | + $tz["dstendweek"], |
|
| 2239 | + $tz["dstendhour"], |
|
| 2240 | + $tz["dstendminute"], |
|
| 2241 | + $tz["dstendsecond"], |
|
| 2242 | + $tz["dstendmillis"], |
|
| 2243 | + 0, |
|
| 2244 | + 0, |
|
| 2245 | + $tz["dststartmonth"], |
|
| 2246 | + $tz["dststartday"], |
|
| 2247 | + $tz["dststartweek"], |
|
| 2248 | + $tz["dststarthour"], |
|
| 2249 | + $tz["dststartminute"], |
|
| 2250 | + $tz["dststartsecond"], |
|
| 2251 | + $tz["dststartmillis"] |
|
| 2252 | + ); |
|
| 2253 | + } |
|
| 2254 | + |
|
| 2255 | + /** |
|
| 2256 | + * Checks the date to see if it is in DST, and returns correct GMT date accordingly. |
|
| 2257 | + * |
|
| 2258 | + * @param long $localtime |
|
| 2259 | + * @param array $tz |
|
| 2260 | + * |
|
| 2261 | + * @return long |
|
| 2262 | + */ |
|
| 2263 | + private function getGMTTimeByTZ($localtime, $tz) { |
|
| 2264 | + if (!isset($tz) || !is_array($tz)) { |
|
| 2265 | + return $localtime; |
|
| 2266 | + } |
|
| 2267 | + |
|
| 2268 | + if ($this->isDST($localtime, $tz)) { |
|
| 2269 | + return $localtime + $tz["bias"] * 60 + $tz["dstbias"] * 60; |
|
| 2270 | + } |
|
| 2271 | + |
|
| 2272 | + return $localtime + $tz["bias"] * 60; |
|
| 2273 | + } |
|
| 2274 | + |
|
| 2275 | + /** |
|
| 2276 | + * Returns the local time for the given GMT time, taking account of the given timezone. |
|
| 2277 | + * |
|
| 2278 | + * @param long $gmttime |
|
| 2279 | + * @param array $tz |
|
| 2280 | + * |
|
| 2281 | + * @return long |
|
| 2282 | + */ |
|
| 2283 | + private function getLocaltimeByTZ($gmttime, $tz) { |
|
| 2284 | + if (!isset($tz) || !is_array($tz)) { |
|
| 2285 | + return $gmttime; |
|
| 2286 | + } |
|
| 2287 | + |
|
| 2288 | + if ($this->isDST($gmttime - $tz["bias"] * 60, $tz)) { // may bug around the switch time because it may have to be 'gmttime - bias - dstbias' |
|
| 2289 | + return $gmttime - $tz["bias"] * 60 - $tz["dstbias"] * 60; |
|
| 2290 | + } |
|
| 2291 | + |
|
| 2292 | + return $gmttime - $tz["bias"] * 60; |
|
| 2293 | + } |
|
| 2294 | + |
|
| 2295 | + /** |
|
| 2296 | + * Returns TRUE if it is the summer and therefore DST is in effect. |
|
| 2297 | + * |
|
| 2298 | + * @param long $localtime |
|
| 2299 | + * @param array $tz |
|
| 2300 | + * |
|
| 2301 | + * @return bool |
|
| 2302 | + */ |
|
| 2303 | + private function isDST($localtime, $tz) { |
|
| 2304 | + if (!isset($tz) || !is_array($tz) || |
|
| 2305 | + !isset($tz["dstbias"]) || $tz["dstbias"] == 0 || |
|
| 2306 | + !isset($tz["dststartmonth"]) || $tz["dststartmonth"] == 0 || |
|
| 2307 | + !isset($tz["dstendmonth"]) || $tz["dstendmonth"] == 0) { |
|
| 2308 | + return false; |
|
| 2309 | + } |
|
| 2310 | + |
|
| 2311 | + $year = gmdate("Y", $localtime); |
|
| 2312 | + $start = $this->getTimestampOfWeek($year, $tz["dststartmonth"], $tz["dststartweek"], $tz["dststartday"], $tz["dststarthour"], $tz["dststartminute"], $tz["dststartsecond"]); |
|
| 2313 | + $end = $this->getTimestampOfWeek($year, $tz["dstendmonth"], $tz["dstendweek"], $tz["dstendday"], $tz["dstendhour"], $tz["dstendminute"], $tz["dstendsecond"]); |
|
| 2314 | + |
|
| 2315 | + if ($start < $end) { |
|
| 2316 | + // northern hemisphere (july = dst) |
|
| 2317 | + if ($localtime >= $start && $localtime < $end) { |
|
| 2318 | + $dst = true; |
|
| 2319 | + } |
|
| 2320 | + else { |
|
| 2321 | + $dst = false; |
|
| 2322 | + } |
|
| 2323 | + } |
|
| 2324 | + else { |
|
| 2325 | + // southern hemisphere (january = dst) |
|
| 2326 | + if ($localtime >= $end && $localtime < $start) { |
|
| 2327 | + $dst = false; |
|
| 2328 | + } |
|
| 2329 | + else { |
|
| 2330 | + $dst = true; |
|
| 2331 | + } |
|
| 2332 | + } |
|
| 2333 | + |
|
| 2334 | + return $dst; |
|
| 2335 | + } |
|
| 2336 | + |
|
| 2337 | + /** |
|
| 2338 | + * Returns the local timestamp for the $week'th $wday of $month in $year at $hour:$minute:$second. |
|
| 2339 | + * |
|
| 2340 | + * @param int $year |
|
| 2341 | + * @param int $month |
|
| 2342 | + * @param int $week |
|
| 2343 | + * @param int $wday |
|
| 2344 | + * @param int $hour |
|
| 2345 | + * @param int $minute |
|
| 2346 | + * @param int $second |
|
| 2347 | + * |
|
| 2348 | + * @return long |
|
| 2349 | + */ |
|
| 2350 | + private function getTimestampOfWeek($year, $month, $week, $wday, $hour, $minute, $second) { |
|
| 2351 | + if ($month == 0) { |
|
| 2352 | + return; |
|
| 2353 | + } |
|
| 2354 | + |
|
| 2355 | + $date = gmmktime($hour, $minute, $second, $month, 1, $year); |
|
| 2356 | + |
|
| 2357 | + // Find first day in month which matches day of the week |
|
| 2358 | + while (1) { |
|
| 2359 | + $wdaynow = gmdate("w", $date); |
|
| 2360 | + if ($wdaynow == $wday) { |
|
| 2361 | + break; |
|
| 2362 | + } |
|
| 2363 | + $date += 24 * 60 * 60; |
|
| 2364 | + } |
|
| 2365 | + |
|
| 2366 | + // Forward $week weeks (may 'overflow' into the next month) |
|
| 2367 | + $date = $date + $week * (24 * 60 * 60 * 7); |
|
| 2368 | + |
|
| 2369 | + // Reverse 'overflow'. Eg week '10' will always be the last week of the month in which the |
|
| 2370 | + // specified weekday exists |
|
| 2371 | + while (1) { |
|
| 2372 | + $monthnow = gmdate("n", $date); // gmdate returns 1-12 |
|
| 2373 | + if ($monthnow > $month) { |
|
| 2374 | + $date = $date - (24 * 7 * 60 * 60); |
|
| 2375 | + } |
|
| 2376 | + else { |
|
| 2377 | + break; |
|
| 2378 | + } |
|
| 2379 | + } |
|
| 2380 | + |
|
| 2381 | + return $date; |
|
| 2382 | + } |
|
| 2383 | + |
|
| 2384 | + /** |
|
| 2385 | + * Normalize the given timestamp to the start of the day. |
|
| 2386 | + * |
|
| 2387 | + * @param long $timestamp |
|
| 2388 | + * |
|
| 2389 | + * @return long |
|
| 2390 | + */ |
|
| 2391 | + private function getDayStartOfTimestamp($timestamp) { |
|
| 2392 | + return $timestamp - ($timestamp % (60 * 60 * 24)); |
|
| 2393 | + } |
|
| 2394 | + |
|
| 2395 | + /** |
|
| 2396 | + * Returns an SMTP address from an entry id. |
|
| 2397 | + * |
|
| 2398 | + * @param string $entryid |
|
| 2399 | + * |
|
| 2400 | + * @return string |
|
| 2401 | + */ |
|
| 2402 | + private function getSMTPAddressFromEntryID($entryid) { |
|
| 2403 | + $addrbook = $this->getAddressbook(); |
|
| 2404 | + |
|
| 2405 | + $mailuser = mapi_ab_openentry($addrbook, $entryid); |
|
| 2406 | + if (!$mailuser) { |
|
| 2407 | + return ""; |
|
| 2408 | + } |
|
| 2409 | + |
|
| 2410 | + $props = mapi_getprops($mailuser, [PR_ADDRTYPE, PR_SMTP_ADDRESS, PR_EMAIL_ADDRESS]); |
|
| 2411 | + |
|
| 2412 | + $addrtype = isset($props[PR_ADDRTYPE]) ? $props[PR_ADDRTYPE] : ""; |
|
| 2413 | + |
|
| 2414 | + if (isset($props[PR_SMTP_ADDRESS])) { |
|
| 2415 | + return $props[PR_SMTP_ADDRESS]; |
|
| 2416 | + } |
|
| 2417 | + |
|
| 2418 | + if ($addrtype == "SMTP" && isset($props[PR_EMAIL_ADDRESS])) { |
|
| 2419 | + return $props[PR_EMAIL_ADDRESS]; |
|
| 2420 | + } |
|
| 2421 | + if ($addrtype == "ZARAFA" && isset($props[PR_EMAIL_ADDRESS])) { |
|
| 2422 | + $userinfo = nsp_getuserinfo($props[PR_EMAIL_ADDRESS]); |
|
| 2423 | + if (is_array($userinfo) && isset($userinfo["primary_email"])) { |
|
| 2424 | + return $userinfo["primary_email"]; |
|
| 2425 | + } |
|
| 2426 | + } |
|
| 2427 | + |
|
| 2428 | + return ""; |
|
| 2429 | + } |
|
| 2430 | + |
|
| 2431 | + /** |
|
| 2432 | + * Returns fullname from an entryid. |
|
| 2433 | + * |
|
| 2434 | + * @param binary $entryid |
|
| 2435 | + * |
|
| 2436 | + * @return string fullname or false on error |
|
| 2437 | + */ |
|
| 2438 | + private function getFullnameFromEntryID($entryid) { |
|
| 2439 | + $addrbook = $this->getAddressbook(); |
|
| 2440 | + $mailuser = mapi_ab_openentry($addrbook, $entryid); |
|
| 2441 | + if (!$mailuser) { |
|
| 2442 | + SLog::Write(LOGLEVEL_ERROR, sprintf("Unable to get mailuser for getFullnameFromEntryID (0x%X)", mapi_last_hresult())); |
|
| 2443 | + |
|
| 2444 | + return false; |
|
| 2445 | + } |
|
| 2446 | + |
|
| 2447 | + $props = mapi_getprops($mailuser, [PR_DISPLAY_NAME]); |
|
| 2448 | + if (isset($props[PR_DISPLAY_NAME])) { |
|
| 2449 | + return $props[PR_DISPLAY_NAME]; |
|
| 2450 | + } |
|
| 2451 | + SLog::Write(LOGLEVEL_ERROR, sprintf("Unable to get fullname for getFullnameFromEntryID (0x%X)", mapi_last_hresult())); |
|
| 2452 | + |
|
| 2453 | + return false; |
|
| 2454 | + } |
|
| 2455 | + |
|
| 2456 | + /** |
|
| 2457 | + * Builds a displayname from several separated values. |
|
| 2458 | + * |
|
| 2459 | + * @param SyncContact $contact |
|
| 2460 | + * |
|
| 2461 | + * @return string |
|
| 2462 | + */ |
|
| 2463 | + private function composeDisplayName(&$contact) { |
|
| 2464 | + // Set display name and subject to a combined value of firstname and lastname |
|
| 2465 | + $cname = (isset($contact->prefix)) ? u2w($contact->prefix) . " " : ""; |
|
| 2466 | + $cname .= u2w($contact->firstname); |
|
| 2467 | + $cname .= (isset($contact->middlename)) ? " " . u2w($contact->middlename) : ""; |
|
| 2468 | + $cname .= " " . u2w($contact->lastname); |
|
| 2469 | + $cname .= (isset($contact->suffix)) ? " " . u2w($contact->suffix) : ""; |
|
| 2470 | + |
|
| 2471 | + return trim($cname); |
|
| 2472 | + } |
|
| 2473 | + |
|
| 2474 | + /** |
|
| 2475 | + * Sets all dependent properties for an email address. |
|
| 2476 | + * |
|
| 2477 | + * @param string $emailAddress |
|
| 2478 | + * @param string $displayName |
|
| 2479 | + * @param int $cnt |
|
| 2480 | + * @param array &$props |
|
| 2481 | + * @param array &$properties |
|
| 2482 | + * @param array &$nremails |
|
| 2483 | + * @param int &$abprovidertype |
|
| 2484 | + * |
|
| 2485 | + * @return |
|
| 2486 | + */ |
|
| 2487 | + private function setEmailAddress($emailAddress, $displayName, $cnt, &$props, &$properties, &$nremails, &$abprovidertype) { |
|
| 2488 | + if (isset($emailAddress)) { |
|
| 2489 | + $name = (isset($displayName)) ? $displayName : $emailAddress; |
|
| 2490 | + |
|
| 2491 | + $props[$properties["emailaddress{$cnt}"]] = $emailAddress; |
|
| 2492 | + $props[$properties["emailaddressdemail{$cnt}"]] = $emailAddress; |
|
| 2493 | + $props[$properties["emailaddressdname{$cnt}"]] = $name; |
|
| 2494 | + $props[$properties["emailaddresstype{$cnt}"]] = "SMTP"; |
|
| 2495 | + $props[$properties["emailaddressentryid{$cnt}"]] = mapi_createoneoff($name, "SMTP", $emailAddress); |
|
| 2496 | + $nremails[] = $cnt - 1; |
|
| 2497 | + $abprovidertype |= 2 ^ ($cnt - 1); |
|
| 2498 | + } |
|
| 2499 | + } |
|
| 2500 | + |
|
| 2501 | + /** |
|
| 2502 | + * Sets the properties for an address string. |
|
| 2503 | + * |
|
| 2504 | + * @param string $type which address is being set |
|
| 2505 | + * @param string $city |
|
| 2506 | + * @param string $country |
|
| 2507 | + * @param string $postalcode |
|
| 2508 | + * @param string $state |
|
| 2509 | + * @param string $street |
|
| 2510 | + * @param array &$props |
|
| 2511 | + * @param array &$properties |
|
| 2512 | + * |
|
| 2513 | + * @return |
|
| 2514 | + */ |
|
| 2515 | + private function setAddress($type, &$city, &$country, &$postalcode, &$state, &$street, &$props, &$properties) { |
|
| 2516 | + if (isset($city)) { |
|
| 2517 | + $props[$properties[$type . "city"]] = $city = u2w($city); |
|
| 2518 | + } |
|
| 2519 | + |
|
| 2520 | + if (isset($country)) { |
|
| 2521 | + $props[$properties[$type . "country"]] = $country = u2w($country); |
|
| 2522 | + } |
|
| 2523 | + |
|
| 2524 | + if (isset($postalcode)) { |
|
| 2525 | + $props[$properties[$type . "postalcode"]] = $postalcode = u2w($postalcode); |
|
| 2526 | + } |
|
| 2527 | + |
|
| 2528 | + if (isset($state)) { |
|
| 2529 | + $props[$properties[$type . "state"]] = $state = u2w($state); |
|
| 2530 | + } |
|
| 2531 | + |
|
| 2532 | + if (isset($street)) { |
|
| 2533 | + $props[$properties[$type . "street"]] = $street = u2w($street); |
|
| 2534 | + } |
|
| 2535 | + |
|
| 2536 | + // set composed address |
|
| 2537 | + $address = Utils::BuildAddressString($street, $postalcode, $city, $state, $country); |
|
| 2538 | + if ($address) { |
|
| 2539 | + $props[$properties[$type . "address"]] = $address; |
|
| 2540 | + } |
|
| 2541 | + } |
|
| 2542 | + |
|
| 2543 | + /** |
|
| 2544 | + * Sets the properties for a mailing address. |
|
| 2545 | + * |
|
| 2546 | + * @param string $city |
|
| 2547 | + * @param string $country |
|
| 2548 | + * @param string $postalcode |
|
| 2549 | + * @param string $state |
|
| 2550 | + * @param string $street |
|
| 2551 | + * @param string $address |
|
| 2552 | + * @param array &$props |
|
| 2553 | + * @param array &$properties |
|
| 2554 | + * |
|
| 2555 | + * @return |
|
| 2556 | + */ |
|
| 2557 | + private function setMailingAddress($city, $country, $postalcode, $state, $street, $address, &$props, &$properties) { |
|
| 2558 | + if (isset($city)) { |
|
| 2559 | + $props[$properties["city"]] = $city; |
|
| 2560 | + } |
|
| 2561 | + if (isset($country)) { |
|
| 2562 | + $props[$properties["country"]] = $country; |
|
| 2563 | + } |
|
| 2564 | + if (isset($postalcode)) { |
|
| 2565 | + $props[$properties["postalcode"]] = $postalcode; |
|
| 2566 | + } |
|
| 2567 | + if (isset($state)) { |
|
| 2568 | + $props[$properties["state"]] = $state; |
|
| 2569 | + } |
|
| 2570 | + if (isset($street)) { |
|
| 2571 | + $props[$properties["street"]] = $street; |
|
| 2572 | + } |
|
| 2573 | + if (isset($address)) { |
|
| 2574 | + $props[$properties["postaladdress"]] = $address; |
|
| 2575 | + } |
|
| 2576 | + } |
|
| 2577 | + |
|
| 2578 | + /** |
|
| 2579 | + * Sets data in a recurrence array. |
|
| 2580 | + * |
|
| 2581 | + * @param SyncObject $message |
|
| 2582 | + * @param array &$recur |
|
| 2583 | + * |
|
| 2584 | + * @return |
|
| 2585 | + */ |
|
| 2586 | + private function setRecurrence($message, &$recur) { |
|
| 2587 | + if (isset($message->complete)) { |
|
| 2588 | + $recur["complete"] = $message->complete; |
|
| 2589 | + } |
|
| 2590 | + |
|
| 2591 | + if (!isset($message->recurrence->interval)) { |
|
| 2592 | + $message->recurrence->interval = 1; |
|
| 2593 | + } |
|
| 2594 | + |
|
| 2595 | + // set the default value of numoccur |
|
| 2596 | + $recur["numoccur"] = 0; |
|
| 2597 | + // a place holder for recurrencetype property |
|
| 2598 | + $recur["recurrencetype"] = 0; |
|
| 2599 | + |
|
| 2600 | + switch ($message->recurrence->type) { |
|
| 2601 | + case 0: |
|
| 2602 | + $recur["type"] = 10; |
|
| 2603 | + if (isset($message->recurrence->dayofweek)) { |
|
| 2604 | + $recur["subtype"] = 1; |
|
| 2605 | + } |
|
| 2606 | + else { |
|
| 2607 | + $recur["subtype"] = 0; |
|
| 2608 | + } |
|
| 2609 | + |
|
| 2610 | + $recur["everyn"] = $message->recurrence->interval * (60 * 24); |
|
| 2611 | + $recur["recurrencetype"] = 1; |
|
| 2612 | + break; |
|
| 2613 | + |
|
| 2614 | + case 1: |
|
| 2615 | + $recur["type"] = 11; |
|
| 2616 | + $recur["subtype"] = 1; |
|
| 2617 | + $recur["everyn"] = $message->recurrence->interval; |
|
| 2618 | + $recur["recurrencetype"] = 2; |
|
| 2619 | + break; |
|
| 2620 | + |
|
| 2621 | + case 2: |
|
| 2622 | + $recur["type"] = 12; |
|
| 2623 | + $recur["subtype"] = 2; |
|
| 2624 | + $recur["everyn"] = $message->recurrence->interval; |
|
| 2625 | + $recur["recurrencetype"] = 3; |
|
| 2626 | + break; |
|
| 2627 | + |
|
| 2628 | + case 3: |
|
| 2629 | + $recur["type"] = 12; |
|
| 2630 | + $recur["subtype"] = 3; |
|
| 2631 | + $recur["everyn"] = $message->recurrence->interval; |
|
| 2632 | + $recur["recurrencetype"] = 3; |
|
| 2633 | + break; |
|
| 2634 | + |
|
| 2635 | + case 4: |
|
| 2636 | + $recur["type"] = 13; |
|
| 2637 | + $recur["subtype"] = 1; |
|
| 2638 | + $recur["everyn"] = $message->recurrence->interval * 12; |
|
| 2639 | + $recur["recurrencetype"] = 4; |
|
| 2640 | + break; |
|
| 2641 | + |
|
| 2642 | + case 5: |
|
| 2643 | + $recur["type"] = 13; |
|
| 2644 | + $recur["subtype"] = 2; |
|
| 2645 | + $recur["everyn"] = $message->recurrence->interval * 12; |
|
| 2646 | + $recur["recurrencetype"] = 4; |
|
| 2647 | + break; |
|
| 2648 | + |
|
| 2649 | + case 6: |
|
| 2650 | + $recur["type"] = 13; |
|
| 2651 | + $recur["subtype"] = 3; |
|
| 2652 | + $recur["everyn"] = $message->recurrence->interval * 12; |
|
| 2653 | + $recur["recurrencetype"] = 4; |
|
| 2654 | + break; |
|
| 2655 | + } |
|
| 2656 | + |
|
| 2657 | + // "start" and "end" are in GMT when passing to class.recurrence |
|
| 2658 | + $recur["end"] = $this->getDayStartOfTimestamp(0x7FFFFFFF); // Maximum GMT value for end by default |
|
| 2659 | + |
|
| 2660 | + if (isset($message->recurrence->until)) { |
|
| 2661 | + $recur["term"] = 0x21; |
|
| 2662 | + $recur["end"] = $message->recurrence->until; |
|
| 2663 | + } |
|
| 2664 | + elseif (isset($message->recurrence->occurrences)) { |
|
| 2665 | + $recur["term"] = 0x22; |
|
| 2666 | + $recur["numoccur"] = $message->recurrence->occurrences; |
|
| 2667 | + } |
|
| 2668 | + else { |
|
| 2669 | + $recur["term"] = 0x23; |
|
| 2670 | + } |
|
| 2671 | + |
|
| 2672 | + if (isset($message->recurrence->dayofweek)) { |
|
| 2673 | + $recur["weekdays"] = $message->recurrence->dayofweek; |
|
| 2674 | + } |
|
| 2675 | + if (isset($message->recurrence->weekofmonth)) { |
|
| 2676 | + $recur["nday"] = $message->recurrence->weekofmonth; |
|
| 2677 | + } |
|
| 2678 | + if (isset($message->recurrence->monthofyear)) { |
|
| 2679 | + // MAPI stores months as the amount of minutes until the beginning of the month in a |
|
| 2680 | + // non-leapyear. Why this is, is totally unclear. |
|
| 2681 | + $monthminutes = [0, 44640, 84960, 129600, 172800, 217440, 260640, 305280, 348480, 393120, 437760, 480960]; |
|
| 2682 | + $recur["month"] = $monthminutes[$message->recurrence->monthofyear - 1]; |
|
| 2683 | + } |
|
| 2684 | + if (isset($message->recurrence->dayofmonth)) { |
|
| 2685 | + $recur["monthday"] = $message->recurrence->dayofmonth; |
|
| 2686 | + } |
|
| 2687 | + } |
|
| 2688 | + |
|
| 2689 | + /** |
|
| 2690 | + * Extracts the email address (mailbox@host) from an email address because |
|
| 2691 | + * some devices send email address as "Firstname Lastname" <[email protected]>. |
|
| 2692 | + * |
|
| 2693 | + * @see http://developer.berlios.de/mantis/view.php?id=486 |
|
| 2694 | + * |
|
| 2695 | + * @param string $email |
|
| 2696 | + * |
|
| 2697 | + * @return string or false on error |
|
| 2698 | + */ |
|
| 2699 | + private function extractEmailAddress($email) { |
|
| 2700 | + if (!isset($this->zRFC822)) { |
|
| 2701 | + $this->zRFC822 = new Mail_RFC822(); |
|
| 2702 | + } |
|
| 2703 | + $parsedAddress = $this->zRFC822->parseAddressList($email); |
|
| 2704 | + if (!isset($parsedAddress[0]->mailbox) || !isset($parsedAddress[0]->host)) { |
|
| 2705 | + return false; |
|
| 2706 | + } |
|
| 2707 | + |
|
| 2708 | + return $parsedAddress[0]->mailbox . '@' . $parsedAddress[0]->host; |
|
| 2709 | + } |
|
| 2710 | + |
|
| 2711 | + /** |
|
| 2712 | + * Returns the message body for a required format. |
|
| 2713 | + * |
|
| 2714 | + * @param MAPIMessage $mapimessage |
|
| 2715 | + * @param int $bpReturnType |
|
| 2716 | + * @param SyncObject $message |
|
| 2717 | + * |
|
| 2718 | + * @return bool |
|
| 2719 | + */ |
|
| 2720 | + private function setMessageBodyForType($mapimessage, $bpReturnType, &$message) { |
|
| 2721 | + $truncateHtmlSafe = false; |
|
| 2722 | + // default value is PR_BODY |
|
| 2723 | + $property = PR_BODY; |
|
| 2724 | + |
|
| 2725 | + switch ($bpReturnType) { |
|
| 2726 | + case SYNC_BODYPREFERENCE_HTML: |
|
| 2727 | + $property = PR_HTML; |
|
| 2728 | + $truncateHtmlSafe = true; |
|
| 2729 | + break; |
|
| 2730 | + |
|
| 2731 | + case SYNC_BODYPREFERENCE_RTF: |
|
| 2732 | + $property = PR_RTF_COMPRESSED; |
|
| 2733 | + break; |
|
| 2734 | + |
|
| 2735 | + case SYNC_BODYPREFERENCE_MIME: |
|
| 2736 | + $stat = $this->imtoinet($mapimessage, $message); |
|
| 2737 | + if (isset($message->asbody)) { |
|
| 2738 | + $message->asbody->type = $bpReturnType; |
|
| 2739 | + } |
|
| 2740 | + |
|
| 2741 | + return $stat; |
|
| 2742 | + } |
|
| 2743 | + |
|
| 2744 | + $stream = mapi_openproperty($mapimessage, $property, IID_IStream, 0, 0); |
|
| 2745 | + if ($stream) { |
|
| 2746 | + $stat = mapi_stream_stat($stream); |
|
| 2747 | + $streamsize = $stat['cb']; |
|
| 2748 | + } |
|
| 2749 | + else { |
|
| 2750 | + $streamsize = 0; |
|
| 2751 | + } |
|
| 2752 | + |
|
| 2753 | + // set the properties according to supported AS version |
|
| 2754 | + if (Request::GetProtocolVersion() >= 12.0) { |
|
| 2755 | + $message->asbody = new SyncBaseBody(); |
|
| 2756 | + $message->asbody->type = $bpReturnType; |
|
| 2757 | + if ($bpReturnType == SYNC_BODYPREFERENCE_RTF) { |
|
| 2758 | + $body = $this->mapiReadStream($stream, $streamsize); |
|
| 2759 | + $message->asbody->data = StringStreamWrapper::Open(base64_encode($body)); |
|
| 2760 | + } |
|
| 2761 | + elseif (isset($message->internetcpid) && $bpReturnType == SYNC_BODYPREFERENCE_HTML) { |
|
| 2762 | + // if PR_HTML is UTF-8 we can stream it directly, else we have to convert to UTF-8 & wrap it |
|
| 2763 | + if ($message->internetcpid == INTERNET_CPID_UTF8) { |
|
| 2764 | + $message->asbody->data = MAPIStreamWrapper::Open($stream, $truncateHtmlSafe); |
|
| 2765 | + } |
|
| 2766 | + else { |
|
| 2767 | + $body = $this->mapiReadStream($stream, $streamsize); |
|
| 2768 | + $message->asbody->data = StringStreamWrapper::Open(Utils::ConvertCodepageStringToUtf8($message->internetcpid, $body), $truncateHtmlSafe); |
|
| 2769 | + $message->internetcpid = INTERNET_CPID_UTF8; |
|
| 2770 | + } |
|
| 2771 | + } |
|
| 2772 | + else { |
|
| 2773 | + $message->asbody->data = MAPIStreamWrapper::Open($stream); |
|
| 2774 | + } |
|
| 2775 | + $message->asbody->estimatedDataSize = $streamsize; |
|
| 2776 | + } |
|
| 2777 | + else { |
|
| 2778 | + $body = $this->mapiReadStream($stream, $streamsize); |
|
| 2779 | + $message->body = str_replace("\n", "\r\n", w2u(str_replace("\r", "", $body))); |
|
| 2780 | + $message->bodysize = $streamsize; |
|
| 2781 | + $message->bodytruncated = 0; |
|
| 2782 | + } |
|
| 2783 | + |
|
| 2784 | + return true; |
|
| 2785 | + } |
|
| 2786 | + |
|
| 2787 | + /** |
|
| 2788 | + * Reads from a mapi stream, if it's set. If not, returns an empty string. |
|
| 2789 | + * |
|
| 2790 | + * @param resource $stream |
|
| 2791 | + * @param int $size |
|
| 2792 | + * |
|
| 2793 | + * @return string |
|
| 2794 | + */ |
|
| 2795 | + private function mapiReadStream($stream, $size) { |
|
| 2796 | + if (!$stream || $size == 0) { |
|
| 2797 | + return ""; |
|
| 2798 | + } |
|
| 2799 | + |
|
| 2800 | + return mapi_stream_read($stream, $size); |
|
| 2801 | + } |
|
| 2802 | + |
|
| 2803 | + /** |
|
| 2804 | + * A wrapper for mapi_inetmapi_imtoinet function. |
|
| 2805 | + * |
|
| 2806 | + * @param MAPIMessage $mapimessage |
|
| 2807 | + * @param SyncObject $message |
|
| 2808 | + * |
|
| 2809 | + * @return bool |
|
| 2810 | + */ |
|
| 2811 | + private function imtoinet($mapimessage, &$message) { |
|
| 2812 | + $mapiEmail = mapi_getprops($mapimessage, [PR_EC_IMAP_EMAIL]); |
|
| 2813 | + $stream = false; |
|
| 2814 | + if (isset($mapiEmail[PR_EC_IMAP_EMAIL]) || MAPIUtils::GetError(PR_EC_IMAP_EMAIL, $mapiEmail) == MAPI_E_NOT_ENOUGH_MEMORY) { |
|
| 2815 | + $stream = mapi_openproperty($mapimessage, PR_EC_IMAP_EMAIL, IID_IStream, 0, 0); |
|
| 2816 | + SLog::Write(LOGLEVEL_DEBUG, "MAPIProvider->imtoinet(): using PR_EC_IMAP_EMAIL as full RFC822 message"); |
|
| 2817 | + } |
|
| 2818 | + else { |
|
| 2819 | + $addrbook = $this->getAddressbook(); |
|
| 2820 | + $stream = mapi_inetmapi_imtoinet($this->session, $addrbook, $mapimessage, ['use_tnef' => -1, 'ignore_missing_attachments' => 1]); |
|
| 2821 | + } |
|
| 2822 | + if (is_resource($stream)) { |
|
| 2823 | + $mstreamstat = mapi_stream_stat($stream); |
|
| 2824 | + $streamsize = $mstreamstat["cb"]; |
|
| 2825 | + if (isset($streamsize)) { |
|
| 2826 | + if (Request::GetProtocolVersion() >= 12.0) { |
|
| 2827 | + if (!isset($message->asbody)) { |
|
| 2828 | + $message->asbody = new SyncBaseBody(); |
|
| 2829 | + } |
|
| 2830 | + $message->asbody->data = MAPIStreamWrapper::Open($stream); |
|
| 2831 | + $message->asbody->estimatedDataSize = $streamsize; |
|
| 2832 | + $message->asbody->truncated = 0; |
|
| 2833 | + } |
|
| 2834 | + else { |
|
| 2835 | + $message->mimedata = MAPIStreamWrapper::Open($stream); |
|
| 2836 | + $message->mimesize = $streamsize; |
|
| 2837 | + $message->mimetruncated = 0; |
|
| 2838 | + } |
|
| 2839 | + unset($message->body, $message->bodytruncated); |
|
| 2840 | + |
|
| 2841 | + return true; |
|
| 2842 | + } |
|
| 2843 | + } |
|
| 2844 | + SLog::Write(LOGLEVEL_ERROR, "MAPIProvider->imtoinet(): got no stream or content from mapi_inetmapi_imtoinet()"); |
|
| 2845 | + |
|
| 2846 | + return false; |
|
| 2847 | + } |
|
| 2848 | + |
|
| 2849 | + /** |
|
| 2850 | + * Sets the message body. |
|
| 2851 | + * |
|
| 2852 | + * @param MAPIMessage $mapimessage |
|
| 2853 | + * @param ContentParameters $contentparameters |
|
| 2854 | + * @param SyncObject $message |
|
| 2855 | + */ |
|
| 2856 | + private function setMessageBody($mapimessage, $contentparameters, &$message) { |
|
| 2857 | + // get the available body preference types |
|
| 2858 | + $bpTypes = $contentparameters->GetBodyPreference(); |
|
| 2859 | + if ($bpTypes !== false) { |
|
| 2860 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("BodyPreference types: %s", implode(', ', $bpTypes))); |
|
| 2861 | + // do not send mime data if the client requests it |
|
| 2862 | + if (($contentparameters->GetMimeSupport() == SYNC_MIMESUPPORT_NEVER) && ($key = array_search(SYNC_BODYPREFERENCE_MIME, $bpTypes) !== false)) { |
|
| 2863 | + unset($bpTypes[$key]); |
|
| 2864 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("Remove mime body preference type because the device required no mime support. BodyPreference types: %s", implode(', ', $bpTypes))); |
|
| 2865 | + } |
|
| 2866 | + // get the best fitting preference type |
|
| 2867 | + $bpReturnType = Utils::GetBodyPreferenceBestMatch($bpTypes); |
|
| 2868 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("GetBodyPreferenceBestMatch: %d", $bpReturnType)); |
|
| 2869 | + $bpo = $contentparameters->BodyPreference($bpReturnType); |
|
| 2870 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("bpo: truncation size:'%d', allornone:'%d', preview:'%d'", $bpo->GetTruncationSize(), $bpo->GetAllOrNone(), $bpo->GetPreview())); |
|
| 2871 | + |
|
| 2872 | + // Android Blackberry expects a full mime message for signed emails |
|
| 2873 | + // @see https://jira.z-hub.io/projects/ZP/issues/ZP-1154 |
|
| 2874 | + // @TODO change this when refactoring |
|
| 2875 | + $props = mapi_getprops($mapimessage, [PR_MESSAGE_CLASS]); |
|
| 2876 | + if (isset($props[PR_MESSAGE_CLASS]) && |
|
| 2877 | + stripos($props[PR_MESSAGE_CLASS], 'IPM.Note.SMIME.MultipartSigned') !== false && |
|
| 2878 | + ($key = array_search(SYNC_BODYPREFERENCE_MIME, $bpTypes) !== false)) { |
|
| 2879 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->setMessageBody(): enforcing SYNC_BODYPREFERENCE_MIME type for a signed message")); |
|
| 2880 | + $bpReturnType = SYNC_BODYPREFERENCE_MIME; |
|
| 2881 | + } |
|
| 2882 | + |
|
| 2883 | + $this->setMessageBodyForType($mapimessage, $bpReturnType, $message); |
|
| 2884 | + // only set the truncation size data if device set it in request |
|
| 2885 | + if ($bpo->GetTruncationSize() != false && |
|
| 2886 | + $bpReturnType != SYNC_BODYPREFERENCE_MIME && |
|
| 2887 | + $message->asbody->estimatedDataSize > $bpo->GetTruncationSize() |
|
| 2888 | + ) { |
|
| 2889 | + // Truncated plaintext requests are used on iOS for the preview in the email list. All images and links should be removed - see https://jira.z-hub.io/browse/ZP-1025 |
|
| 2890 | + if ($bpReturnType == SYNC_BODYPREFERENCE_PLAIN) { |
|
| 2891 | + SLog::Write(LOGLEVEL_DEBUG, "MAPIProvider->setMessageBody(): truncated plain-text body requested, stripping all links and images"); |
|
| 2892 | + // Get more data because of the filtering it's most probably going down in size. It's going to be truncated to the correct size below. |
|
| 2893 | + $plainbody = stream_get_contents($message->asbody->data, $bpo->GetTruncationSize() * 5); |
|
| 2894 | + $message->asbody->data = StringStreamWrapper::Open(preg_replace('/<http(s){0,1}:\/\/.*?>/i', '', $plainbody)); |
|
| 2895 | + } |
|
| 2896 | + |
|
| 2897 | + // truncate data stream |
|
| 2898 | + ftruncate($message->asbody->data, $bpo->GetTruncationSize()); |
|
| 2899 | + $message->asbody->truncated = 1; |
|
| 2900 | + } |
|
| 2901 | + // set the preview or windows phones won't show the preview of an email |
|
| 2902 | + if (Request::GetProtocolVersion() >= 14.0 && $bpo->GetPreview()) { |
|
| 2903 | + $message->asbody->preview = Utils::Utf8_truncate(MAPIUtils::readPropStream($mapimessage, PR_BODY), $bpo->GetPreview()); |
|
| 2904 | + } |
|
| 2905 | + } |
|
| 2906 | + else { |
|
| 2907 | + // Override 'body' for truncation |
|
| 2908 | + $truncsize = Utils::GetTruncSize($contentparameters->GetTruncation()); |
|
| 2909 | + $this->setMessageBodyForType($mapimessage, SYNC_BODYPREFERENCE_PLAIN, $message); |
|
| 2910 | + |
|
| 2911 | + if ($message->bodysize > $truncsize) { |
|
| 2912 | + $message->body = Utils::Utf8_truncate($message->body, $truncsize); |
|
| 2913 | + $message->bodytruncated = 1; |
|
| 2914 | + } |
|
| 2915 | + |
|
| 2916 | + if (!isset($message->body) || strlen($message->body) == 0) { |
|
| 2917 | + $message->body = " "; |
|
| 2918 | + } |
|
| 2919 | + |
|
| 2920 | + if ($contentparameters->GetMimeSupport() == SYNC_MIMESUPPORT_ALWAYS) { |
|
| 2921 | + // set the html body for iphone in AS 2.5 version |
|
| 2922 | + $this->imtoinet($mapimessage, $message); |
|
| 2923 | + } |
|
| 2924 | + } |
|
| 2925 | + } |
|
| 2926 | + |
|
| 2927 | + /** |
|
| 2928 | + * Sets properties for an email message. |
|
| 2929 | + * |
|
| 2930 | + * @param mixed $mapimessage |
|
| 2931 | + * @param SyncMail $message |
|
| 2932 | + */ |
|
| 2933 | + private function setFlag($mapimessage, &$message) { |
|
| 2934 | + // do nothing if protocol version is lower than 12.0 as flags haven't been defined before |
|
| 2935 | + if (Request::GetProtocolVersion() < 12.0) { |
|
| 2936 | + return; |
|
| 2937 | + } |
|
| 2938 | + |
|
| 2939 | + $message->flag = new SyncMailFlags(); |
|
| 2940 | + |
|
| 2941 | + $this->getPropsFromMAPI($message->flag, $mapimessage, MAPIMapping::GetMailFlagsMapping()); |
|
| 2942 | + } |
|
| 2943 | + |
|
| 2944 | + /** |
|
| 2945 | + * Sets information from SyncBaseBody type for a MAPI message. |
|
| 2946 | + * |
|
| 2947 | + * @param SyncBaseBody $asbody |
|
| 2948 | + * @param array $props |
|
| 2949 | + * @param array $appointmentprops |
|
| 2950 | + */ |
|
| 2951 | + private function setASbody($asbody, &$props, $appointmentprops) { |
|
| 2952 | + // TODO: fix checking for the length |
|
| 2953 | + if (isset($asbody->type, $asbody->data) /* && strlen($asbody->data) > 0 */) { |
|
| 2954 | + switch ($asbody->type) { |
|
| 2955 | + case SYNC_BODYPREFERENCE_PLAIN: |
|
| 2956 | + default: |
|
| 2957 | + // set plain body if the type is not in valid range |
|
| 2958 | + $props[$appointmentprops["body"]] = stream_get_contents($asbody->data); |
|
| 2959 | + break; |
|
| 2960 | + |
|
| 2961 | + case SYNC_BODYPREFERENCE_HTML: |
|
| 2962 | + $props[$appointmentprops["html"]] = stream_get_contents($asbody->data); |
|
| 2963 | + break; |
|
| 2964 | + |
|
| 2965 | + case SYNC_BODYPREFERENCE_RTF: |
|
| 2966 | + break; |
|
| 2967 | + |
|
| 2968 | + case SYNC_BODYPREFERENCE_MIME: |
|
| 2969 | + break; |
|
| 2970 | + } |
|
| 2971 | + } |
|
| 2972 | + else { |
|
| 2973 | + SLog::Write(LOGLEVEL_DEBUG, "MAPIProvider->setASbody either type or data are not set. Setting to empty body"); |
|
| 2974 | + $props[$appointmentprops["body"]] = ""; |
|
| 2975 | + } |
|
| 2976 | + } |
|
| 2977 | + |
|
| 2978 | + /** |
|
| 2979 | + * Get MAPI addressbook object. |
|
| 2980 | + * |
|
| 2981 | + * @return MAPIAddressbook object to be used with mapi_ab_* or false on failure |
|
| 2982 | + */ |
|
| 2983 | + private function getAddressbook() { |
|
| 2984 | + if (isset($this->addressbook) && $this->addressbook) { |
|
| 2985 | + return $this->addressbook; |
|
| 2986 | + } |
|
| 2987 | + $this->addressbook = mapi_openaddressbook($this->session); |
|
| 2988 | + $result = mapi_last_hresult(); |
|
| 2989 | + if ($result && $this->addressbook === false) { |
|
| 2990 | + SLog::Write(LOGLEVEL_ERROR, sprintf("MAPIProvider->getAddressbook error opening addressbook 0x%X", $result)); |
|
| 2991 | + |
|
| 2992 | + return false; |
|
| 2993 | + } |
|
| 2994 | + |
|
| 2995 | + return $this->addressbook; |
|
| 2996 | + } |
|
| 2997 | + |
|
| 2998 | + /** |
|
| 2999 | + * Gets the required store properties. |
|
| 3000 | + * |
|
| 3001 | + * @return array |
|
| 3002 | + */ |
|
| 3003 | + public function GetStoreProps() { |
|
| 3004 | + if (!isset($this->storeProps) || empty($this->storeProps)) { |
|
| 3005 | + SLog::Write(LOGLEVEL_DEBUG, "MAPIProvider->GetStoreProps(): Getting store properties."); |
|
| 3006 | + $this->storeProps = mapi_getprops($this->store, [PR_IPM_SUBTREE_ENTRYID, PR_IPM_OUTBOX_ENTRYID, PR_IPM_WASTEBASKET_ENTRYID, PR_IPM_SENTMAIL_ENTRYID, PR_ENTRYID, PR_IPM_PUBLIC_FOLDERS_ENTRYID, PR_IPM_FAVORITES_ENTRYID, PR_MAILBOX_OWNER_ENTRYID]); |
|
| 3007 | + // make sure all properties are set |
|
| 3008 | + if (!isset($this->storeProps[PR_IPM_WASTEBASKET_ENTRYID])) { |
|
| 3009 | + $this->storeProps[PR_IPM_WASTEBASKET_ENTRYID] = false; |
|
| 3010 | + } |
|
| 3011 | + if (!isset($this->storeProps[PR_IPM_SENTMAIL_ENTRYID])) { |
|
| 3012 | + $this->storeProps[PR_IPM_SENTMAIL_ENTRYID] = false; |
|
| 3013 | + } |
|
| 3014 | + if (!isset($this->storeProps[PR_IPM_OUTBOX_ENTRYID])) { |
|
| 3015 | + $this->storeProps[PR_IPM_OUTBOX_ENTRYID] = false; |
|
| 3016 | + } |
|
| 3017 | + if (!isset($this->storeProps[PR_IPM_PUBLIC_FOLDERS_ENTRYID])) { |
|
| 3018 | + $this->storeProps[PR_IPM_PUBLIC_FOLDERS_ENTRYID] = false; |
|
| 3019 | + } |
|
| 3020 | + } |
|
| 3021 | + |
|
| 3022 | + return $this->storeProps; |
|
| 3023 | + } |
|
| 3024 | + |
|
| 3025 | + /** |
|
| 3026 | + * Gets the required inbox properties. |
|
| 3027 | + * |
|
| 3028 | + * @return array |
|
| 3029 | + */ |
|
| 3030 | + public function GetInboxProps() { |
|
| 3031 | + if (!isset($this->inboxProps) || empty($this->inboxProps)) { |
|
| 3032 | + SLog::Write(LOGLEVEL_DEBUG, "MAPIProvider->GetInboxProps(): Getting inbox properties."); |
|
| 3033 | + $this->inboxProps = []; |
|
| 3034 | + $inbox = mapi_msgstore_getreceivefolder($this->store); |
|
| 3035 | + if ($inbox) { |
|
| 3036 | + $this->inboxProps = mapi_getprops($inbox, [PR_ENTRYID, PR_IPM_DRAFTS_ENTRYID, PR_IPM_TASK_ENTRYID, PR_IPM_APPOINTMENT_ENTRYID, PR_IPM_CONTACT_ENTRYID, PR_IPM_NOTE_ENTRYID, PR_IPM_JOURNAL_ENTRYID]); |
|
| 3037 | + // make sure all properties are set |
|
| 3038 | + if (!isset($this->inboxProps[PR_ENTRYID])) { |
|
| 3039 | + $this->inboxProps[PR_ENTRYID] = false; |
|
| 3040 | + } |
|
| 3041 | + if (!isset($this->inboxProps[PR_IPM_DRAFTS_ENTRYID])) { |
|
| 3042 | + $this->inboxProps[PR_IPM_DRAFTS_ENTRYID] = false; |
|
| 3043 | + } |
|
| 3044 | + if (!isset($this->inboxProps[PR_IPM_TASK_ENTRYID])) { |
|
| 3045 | + $this->inboxProps[PR_IPM_TASK_ENTRYID] = false; |
|
| 3046 | + } |
|
| 3047 | + if (!isset($this->inboxProps[PR_IPM_APPOINTMENT_ENTRYID])) { |
|
| 3048 | + $this->inboxProps[PR_IPM_APPOINTMENT_ENTRYID] = false; |
|
| 3049 | + } |
|
| 3050 | + if (!isset($this->inboxProps[PR_IPM_CONTACT_ENTRYID])) { |
|
| 3051 | + $this->inboxProps[PR_IPM_CONTACT_ENTRYID] = false; |
|
| 3052 | + } |
|
| 3053 | + if (!isset($this->inboxProps[PR_IPM_NOTE_ENTRYID])) { |
|
| 3054 | + $this->inboxProps[PR_IPM_NOTE_ENTRYID] = false; |
|
| 3055 | + } |
|
| 3056 | + if (!isset($this->inboxProps[PR_IPM_JOURNAL_ENTRYID])) { |
|
| 3057 | + $this->inboxProps[PR_IPM_JOURNAL_ENTRYID] = false; |
|
| 3058 | + } |
|
| 3059 | + } |
|
| 3060 | + } |
|
| 3061 | + |
|
| 3062 | + return $this->inboxProps; |
|
| 3063 | + } |
|
| 3064 | + |
|
| 3065 | + /** |
|
| 3066 | + * Gets the required store root properties. |
|
| 3067 | + * |
|
| 3068 | + * @return array |
|
| 3069 | + */ |
|
| 3070 | + private function getRootProps() { |
|
| 3071 | + if (!isset($this->rootProps)) { |
|
| 3072 | + $root = mapi_msgstore_openentry($this->store, null); |
|
| 3073 | + $this->rootProps = mapi_getprops($root, [PR_IPM_OL2007_ENTRYIDS]); |
|
| 3074 | + } |
|
| 3075 | + |
|
| 3076 | + return $this->rootProps; |
|
| 3077 | + } |
|
| 3078 | + |
|
| 3079 | + /** |
|
| 3080 | + * Returns an array with entryids of some special folders. |
|
| 3081 | + * |
|
| 3082 | + * @return array |
|
| 3083 | + */ |
|
| 3084 | + private function getSpecialFoldersData() { |
|
| 3085 | + // The persist data of an entry in PR_IPM_OL2007_ENTRYIDS consists of: |
|
| 3086 | + // PersistId - e.g. RSF_PID_SUGGESTED_CONTACTS (2 bytes) |
|
| 3087 | + // DataElementsSize - size of DataElements field (2 bytes) |
|
| 3088 | + // DataElements - array of PersistElement structures (variable size) |
|
| 3089 | + // PersistElement Structure consists of |
|
| 3090 | + // ElementID - e.g. RSF_ELID_ENTRYID (2 bytes) |
|
| 3091 | + // ElementDataSize - size of ElementData (2 bytes) |
|
| 3092 | + // ElementData - The data for the special folder identified by the PersistID (variable size) |
|
| 3093 | + if (empty($this->specialFoldersData)) { |
|
| 3094 | + $this->specialFoldersData = []; |
|
| 3095 | + $rootProps = $this->getRootProps(); |
|
| 3096 | + if (isset($rootProps[PR_IPM_OL2007_ENTRYIDS])) { |
|
| 3097 | + $persistData = $rootProps[PR_IPM_OL2007_ENTRYIDS]; |
|
| 3098 | + while (strlen($persistData) > 0) { |
|
| 3099 | + // PERSIST_SENTINEL marks the end of the persist data |
|
| 3100 | + if (strlen($persistData) == 4 && $persistData == PERSIST_SENTINEL) { |
|
| 3101 | + break; |
|
| 3102 | + } |
|
| 3103 | + $unpackedData = unpack("vdataSize/velementID/velDataSize", substr($persistData, 2, 6)); |
|
| 3104 | + if (isset($unpackedData['dataSize'], $unpackedData['elementID']) && $unpackedData['elementID'] == RSF_ELID_ENTRYID && isset($unpackedData['elDataSize'])) { |
|
| 3105 | + $this->specialFoldersData[] = substr($persistData, 8, $unpackedData['elDataSize']); |
|
| 3106 | + // Add PersistId and DataElementsSize lengths to the data size as they're not part of it |
|
| 3107 | + $persistData = substr($persistData, $unpackedData['dataSize'] + 4); |
|
| 3108 | + } |
|
| 3109 | + else { |
|
| 3110 | + SLog::Write(LOGLEVEL_INFO, "MAPIProvider->getSpecialFoldersData(): persistent data is not valid"); |
|
| 3111 | + break; |
|
| 3112 | + } |
|
| 3113 | + } |
|
| 3114 | + } |
|
| 3115 | + } |
|
| 3116 | + |
|
| 3117 | + return $this->specialFoldersData; |
|
| 3118 | + } |
|
| 3119 | + |
|
| 3120 | + /** |
|
| 3121 | + * Extracts email address from PR_SEARCH_KEY property if possible. |
|
| 3122 | + * |
|
| 3123 | + * @param string $searchKey |
|
| 3124 | + * |
|
| 3125 | + * @see https://jira.z-hub.io/browse/ZP-1178 |
|
| 3126 | + * |
|
| 3127 | + * @return string |
|
| 3128 | + */ |
|
| 3129 | + private function getEmailAddressFromSearchKey($searchKey) { |
|
| 3130 | + if (strpos($searchKey, ':') !== false && strpos($searchKey, '@') !== false) { |
|
| 3131 | + SLog::Write(LOGLEVEL_INFO, "MAPIProvider->getEmailAddressFromSearchKey(): fall back to PR_SEARCH_KEY or PR_SENT_REPRESENTING_SEARCH_KEY to resolve user and get email address"); |
|
| 3132 | + |
|
| 3133 | + return trim(strtolower(explode(':', $searchKey)[1])); |
|
| 3134 | + } |
|
| 3135 | + |
|
| 3136 | + return ""; |
|
| 3137 | + } |
|
| 3138 | + |
|
| 3139 | + /** |
|
| 3140 | + * Returns categories for a message. |
|
| 3141 | + * |
|
| 3142 | + * @param binary $parentsourcekey |
|
| 3143 | + * @param binary $sourcekey |
|
| 3144 | + * |
|
| 3145 | + * @return array or false on failure |
|
| 3146 | + */ |
|
| 3147 | + public function GetMessageCategories($parentsourcekey, $sourcekey) { |
|
| 3148 | + $entryid = mapi_msgstore_entryidfromsourcekey($this->store, $parentsourcekey, $sourcekey); |
|
| 3149 | + if (!$entryid) { |
|
| 3150 | + SLog::Write(LOGLEVEL_INFO, sprintf("MAPIProvider->GetMessageCategories(): Couldn't retrieve message, sourcekey: '%s', parentsourcekey: '%s'", bin2hex($sourcekey), bin2hex($parentsourcekey))); |
|
| 3151 | + |
|
| 3152 | + return false; |
|
| 3153 | + } |
|
| 3154 | + $mapimessage = mapi_msgstore_openentry($this->store, $entryid); |
|
| 3155 | + $emailMapping = MAPIMapping::GetEmailMapping(); |
|
| 3156 | + $emailMapping = ["categories" => $emailMapping["categories"]]; |
|
| 3157 | + $messageCategories = $this->getProps($mapimessage, $emailMapping); |
|
| 3158 | + if (isset($messageCategories[$emailMapping["categories"]]) && is_array($messageCategories[$emailMapping["categories"]])) { |
|
| 3159 | + return $messageCategories[$emailMapping["categories"]]; |
|
| 3160 | + } |
|
| 3161 | + |
|
| 3162 | + return false; |
|
| 3163 | + } |
|
| 3164 | 3164 | } |
@@ -462,7 +462,7 @@ discard block |
||
| 462 | 462 | switch ($recurrence->recur["type"]) { |
| 463 | 463 | case 10: |
| 464 | 464 | if ($recurrence->recur["subtype"] == 0) { |
| 465 | - $syncRecurrence->interval = (int) ($recurrence->recur["everyn"] / 1440); |
|
| 465 | + $syncRecurrence->interval = (int)($recurrence->recur["everyn"] / 1440); |
|
| 466 | 466 | } // minutes |
| 467 | 467 | break; |
| 468 | 468 | |
@@ -472,7 +472,7 @@ discard block |
||
| 472 | 472 | break; // months / weeks |
| 473 | 473 | |
| 474 | 474 | case 13: |
| 475 | - $syncRecurrence->interval = (int) ($recurrence->recur["everyn"] / 12); |
|
| 475 | + $syncRecurrence->interval = (int)($recurrence->recur["everyn"] / 12); |
|
| 476 | 476 | break; // months |
| 477 | 477 | } |
| 478 | 478 | |
@@ -483,7 +483,7 @@ discard block |
||
| 483 | 483 | $syncRecurrence->weekofmonth = $recurrence->recur["nday"]; |
| 484 | 484 | } // N'th {DAY} of {X} (0-5) |
| 485 | 485 | if (isset($recurrence->recur["month"])) { |
| 486 | - $syncRecurrence->monthofyear = (int) ($recurrence->recur["month"] / (60 * 24 * 29)) + 1; |
|
| 486 | + $syncRecurrence->monthofyear = (int)($recurrence->recur["month"] / (60 * 24 * 29)) + 1; |
|
| 487 | 487 | } // works ok due to rounding. see also $monthminutes below (1-12) |
| 488 | 488 | if (isset($recurrence->recur["monthday"])) { |
| 489 | 489 | $syncRecurrence->dayofmonth = $recurrence->recur["monthday"]; |
@@ -637,10 +637,10 @@ discard block |
||
| 637 | 637 | } |
| 638 | 638 | |
| 639 | 639 | if ($fromname) { |
| 640 | - $from = "\"" . w2u($fromname) . "\" <" . w2u($fromaddr) . ">"; |
|
| 640 | + $from = "\"".w2u($fromname)."\" <".w2u($fromaddr).">"; |
|
| 641 | 641 | } |
| 642 | 642 | else { // START CHANGED dw2412 HTC shows "error" if sender name is unknown |
| 643 | - $from = "\"" . w2u($fromaddr) . "\" <" . w2u($fromaddr) . ">"; |
|
| 643 | + $from = "\"".w2u($fromaddr)."\" <".w2u($fromaddr).">"; |
|
| 644 | 644 | } |
| 645 | 645 | // END CHANGED dw2412 HTC shows "error" if sender name is unknown |
| 646 | 646 | |
@@ -841,7 +841,7 @@ discard block |
||
| 841 | 841 | $mime = explode("/", $mimetype); |
| 842 | 842 | |
| 843 | 843 | if (count($mime) == 2 && $mime[0] == "image") { |
| 844 | - $attach->displayname = "inline." . $mime[1]; |
|
| 844 | + $attach->displayname = "inline.".$mime[1]; |
|
| 845 | 845 | } |
| 846 | 846 | } |
| 847 | 847 | |
@@ -943,10 +943,10 @@ discard block |
||
| 943 | 943 | } |
| 944 | 944 | else { |
| 945 | 945 | if (substr($name, 0, 1) != '"' && substr($name, -1) != '"') { |
| 946 | - $fulladdr = "\"" . w2u($name) . "\" <" . w2u($address) . ">"; |
|
| 946 | + $fulladdr = "\"".w2u($name)."\" <".w2u($address).">"; |
|
| 947 | 947 | } |
| 948 | 948 | else { |
| 949 | - $fulladdr = w2u($name) . "<" . w2u($address) . ">"; |
|
| 949 | + $fulladdr = w2u($name)."<".w2u($address).">"; |
|
| 950 | 950 | } |
| 951 | 951 | } |
| 952 | 952 | |
@@ -1540,8 +1540,7 @@ discard block |
||
| 1540 | 1540 | if (isset($exception->asbody)) { |
| 1541 | 1541 | $this->setASbody($exception->asbody, $exceptionprops, $appointmentprops); |
| 1542 | 1542 | $mapiexception["body"] = $exceptionprops[$appointmentprops["body"]] = |
| 1543 | - (isset($exceptionprops[$appointmentprops["body"]])) ? $exceptionprops[$appointmentprops["body"]] : |
|
| 1544 | - ((isset($exceptionprops[$appointmentprops["html"]])) ? $exceptionprops[$appointmentprops["html"]] : ""); |
|
| 1543 | + (isset($exceptionprops[$appointmentprops["body"]])) ? $exceptionprops[$appointmentprops["body"]] : ((isset($exceptionprops[$appointmentprops["html"]])) ? $exceptionprops[$appointmentprops["html"]] : ""); |
|
| 1545 | 1544 | } |
| 1546 | 1545 | |
| 1547 | 1546 | array_push($recur["changed_occurrences"], $mapiexception); |
@@ -1582,7 +1581,7 @@ discard block |
||
| 1582 | 1581 | $props[$appointmentprops["representingname"]] = ($displayname !== false) ? $displayname : Request::GetUser(); |
| 1583 | 1582 | $props[$appointmentprops["sentrepresentingemail"]] = Request::GetUser(); |
| 1584 | 1583 | $props[$appointmentprops["sentrepresentingaddt"]] = "ZARAFA"; |
| 1585 | - $props[$appointmentprops["sentrepresentinsrchk"]] = $props[$appointmentprops["sentrepresentingaddt"]] . ":" . $props[$appointmentprops["sentrepresentingemail"]]; |
|
| 1584 | + $props[$appointmentprops["sentrepresentinsrchk"]] = $props[$appointmentprops["sentrepresentingaddt"]].":".$props[$appointmentprops["sentrepresentingemail"]]; |
|
| 1586 | 1585 | |
| 1587 | 1586 | if (isset($appointment->attendees) && is_array($appointment->attendees) && !empty($appointment->attendees)) { |
| 1588 | 1587 | $props[$appointmentprops["icon"]] = 1026; |
@@ -1616,7 +1615,7 @@ discard block |
||
| 1616 | 1615 | $org[PR_ADDRTYPE] = isset($representingprops[$appointmentprops["sentrepresentingaddt"]]) ? $representingprops[$appointmentprops["sentrepresentingaddt"]] : $props[$appointmentprops["sentrepresentingaddt"]]; |
| 1617 | 1616 | $org[PR_SMTP_ADDRESS] = $org[PR_EMAIL_ADDRESS] = isset($representingprops[$appointmentprops["sentrepresentingemail"]]) ? $representingprops[$appointmentprops["sentrepresentingemail"]] : $props[$appointmentprops["sentrepresentingemail"]]; |
| 1618 | 1617 | $org[PR_SEARCH_KEY] = isset($representingprops[$appointmentprops["sentrepresentinsrchk"]]) ? $representingprops[$appointmentprops["sentrepresentinsrchk"]] : $props[$appointmentprops["sentrepresentinsrchk"]]; |
| 1619 | - $org[PR_RECIPIENT_FLAGS] = recipOrganizer | recipSendable; |
|
| 1618 | + $org[PR_RECIPIENT_FLAGS] = recipOrganizer|recipSendable; |
|
| 1620 | 1619 | $org[PR_RECIPIENT_TYPE] = MAPI_ORIG; |
| 1621 | 1620 | |
| 1622 | 1621 | array_push($recips, $org); |
@@ -1643,7 +1642,7 @@ discard block |
||
| 1643 | 1642 | } |
| 1644 | 1643 | else { |
| 1645 | 1644 | $recip[PR_DISPLAY_NAME] = u2w($attendee->name); |
| 1646 | - $recip[PR_SEARCH_KEY] = "SMTP:" . $recip[PR_EMAIL_ADDRESS] . "\0"; |
|
| 1645 | + $recip[PR_SEARCH_KEY] = "SMTP:".$recip[PR_EMAIL_ADDRESS]."\0"; |
|
| 1647 | 1646 | $recip[PR_ADDRTYPE] = "SMTP"; |
| 1648 | 1647 | $recip[PR_RECIPIENT_TYPE] = isset($attendee->attendeetype) ? $attendee->attendeetype : MAPI_TO; |
| 1649 | 1648 | $recip[PR_ENTRYID] = mapi_createoneoff($recip[PR_DISPLAY_NAME], $recip[PR_ADDRTYPE], $recip[PR_EMAIL_ADDRESS]); |
@@ -2194,8 +2193,8 @@ discard block |
||
| 2194 | 2193 | * @return array |
| 2195 | 2194 | */ |
| 2196 | 2195 | private function getTZFromMAPIBlob($data) { |
| 2197 | - return unpack("lbias/lstdbias/ldstbias/" . |
|
| 2198 | - "vconst1/vdstendyear/vdstendmonth/vdstendday/vdstendweek/vdstendhour/vdstendminute/vdstendsecond/vdstendmillis/" . |
|
| 2196 | + return unpack("lbias/lstdbias/ldstbias/". |
|
| 2197 | + "vconst1/vdstendyear/vdstendmonth/vdstendday/vdstendweek/vdstendhour/vdstendminute/vdstendsecond/vdstendmillis/". |
|
| 2199 | 2198 | "vconst2/vdststartyear/vdststartmonth/vdststartday/vdststartweek/vdststarthour/vdststartminute/vdststartsecond/vdststartmillis", $data); |
| 2200 | 2199 | } |
| 2201 | 2200 | |
@@ -2207,8 +2206,8 @@ discard block |
||
| 2207 | 2206 | * @return array |
| 2208 | 2207 | */ |
| 2209 | 2208 | private function getTZFromSyncBlob($data) { |
| 2210 | - $tz = unpack("lbias/a64tzname/vdstendyear/vdstendmonth/vdstendday/vdstendweek/vdstendhour/vdstendminute/vdstendsecond/vdstendmillis/" . |
|
| 2211 | - "lstdbias/a64tznamedst/vdststartyear/vdststartmonth/vdststartday/vdststartweek/vdststarthour/vdststartminute/vdststartsecond/vdststartmillis/" . |
|
| 2209 | + $tz = unpack("lbias/a64tzname/vdstendyear/vdstendmonth/vdstendday/vdstendweek/vdstendhour/vdstendminute/vdstendsecond/vdstendmillis/". |
|
| 2210 | + "lstdbias/a64tznamedst/vdststartyear/vdststartmonth/vdststartday/vdststartweek/vdststarthour/vdststartminute/vdststartsecond/vdststartmillis/". |
|
| 2212 | 2211 | "ldstbias", $data); |
| 2213 | 2212 | |
| 2214 | 2213 | // Make the structure compatible with class.recurrence.php |
@@ -2227,7 +2226,7 @@ discard block |
||
| 2227 | 2226 | */ |
| 2228 | 2227 | private function getMAPIBlobFromTZ($tz) { |
| 2229 | 2228 | return pack( |
| 2230 | - "lll" . "vvvvvvvvv" . "vvvvvvvvv", |
|
| 2229 | + "lll"."vvvvvvvvv"."vvvvvvvvv", |
|
| 2231 | 2230 | $tz["bias"], |
| 2232 | 2231 | $tz["stdbias"], |
| 2233 | 2232 | $tz["dstbias"], |
@@ -2462,11 +2461,11 @@ discard block |
||
| 2462 | 2461 | */ |
| 2463 | 2462 | private function composeDisplayName(&$contact) { |
| 2464 | 2463 | // Set display name and subject to a combined value of firstname and lastname |
| 2465 | - $cname = (isset($contact->prefix)) ? u2w($contact->prefix) . " " : ""; |
|
| 2464 | + $cname = (isset($contact->prefix)) ? u2w($contact->prefix)." " : ""; |
|
| 2466 | 2465 | $cname .= u2w($contact->firstname); |
| 2467 | - $cname .= (isset($contact->middlename)) ? " " . u2w($contact->middlename) : ""; |
|
| 2468 | - $cname .= " " . u2w($contact->lastname); |
|
| 2469 | - $cname .= (isset($contact->suffix)) ? " " . u2w($contact->suffix) : ""; |
|
| 2466 | + $cname .= (isset($contact->middlename)) ? " ".u2w($contact->middlename) : ""; |
|
| 2467 | + $cname .= " ".u2w($contact->lastname); |
|
| 2468 | + $cname .= (isset($contact->suffix)) ? " ".u2w($contact->suffix) : ""; |
|
| 2470 | 2469 | |
| 2471 | 2470 | return trim($cname); |
| 2472 | 2471 | } |
@@ -2494,7 +2493,7 @@ discard block |
||
| 2494 | 2493 | $props[$properties["emailaddresstype{$cnt}"]] = "SMTP"; |
| 2495 | 2494 | $props[$properties["emailaddressentryid{$cnt}"]] = mapi_createoneoff($name, "SMTP", $emailAddress); |
| 2496 | 2495 | $nremails[] = $cnt - 1; |
| 2497 | - $abprovidertype |= 2 ^ ($cnt - 1); |
|
| 2496 | + $abprovidertype |= 2^($cnt - 1); |
|
| 2498 | 2497 | } |
| 2499 | 2498 | } |
| 2500 | 2499 | |
@@ -2514,29 +2513,29 @@ discard block |
||
| 2514 | 2513 | */ |
| 2515 | 2514 | private function setAddress($type, &$city, &$country, &$postalcode, &$state, &$street, &$props, &$properties) { |
| 2516 | 2515 | if (isset($city)) { |
| 2517 | - $props[$properties[$type . "city"]] = $city = u2w($city); |
|
| 2516 | + $props[$properties[$type."city"]] = $city = u2w($city); |
|
| 2518 | 2517 | } |
| 2519 | 2518 | |
| 2520 | 2519 | if (isset($country)) { |
| 2521 | - $props[$properties[$type . "country"]] = $country = u2w($country); |
|
| 2520 | + $props[$properties[$type."country"]] = $country = u2w($country); |
|
| 2522 | 2521 | } |
| 2523 | 2522 | |
| 2524 | 2523 | if (isset($postalcode)) { |
| 2525 | - $props[$properties[$type . "postalcode"]] = $postalcode = u2w($postalcode); |
|
| 2524 | + $props[$properties[$type."postalcode"]] = $postalcode = u2w($postalcode); |
|
| 2526 | 2525 | } |
| 2527 | 2526 | |
| 2528 | 2527 | if (isset($state)) { |
| 2529 | - $props[$properties[$type . "state"]] = $state = u2w($state); |
|
| 2528 | + $props[$properties[$type."state"]] = $state = u2w($state); |
|
| 2530 | 2529 | } |
| 2531 | 2530 | |
| 2532 | 2531 | if (isset($street)) { |
| 2533 | - $props[$properties[$type . "street"]] = $street = u2w($street); |
|
| 2532 | + $props[$properties[$type."street"]] = $street = u2w($street); |
|
| 2534 | 2533 | } |
| 2535 | 2534 | |
| 2536 | 2535 | // set composed address |
| 2537 | 2536 | $address = Utils::BuildAddressString($street, $postalcode, $city, $state, $country); |
| 2538 | 2537 | if ($address) { |
| 2539 | - $props[$properties[$type . "address"]] = $address; |
|
| 2538 | + $props[$properties[$type."address"]] = $address; |
|
| 2540 | 2539 | } |
| 2541 | 2540 | } |
| 2542 | 2541 | |
@@ -2705,7 +2704,7 @@ discard block |
||
| 2705 | 2704 | return false; |
| 2706 | 2705 | } |
| 2707 | 2706 | |
| 2708 | - return $parsedAddress[0]->mailbox . '@' . $parsedAddress[0]->host; |
|
| 2707 | + return $parsedAddress[0]->mailbox.'@'.$parsedAddress[0]->host; |
|
| 2709 | 2708 | } |
| 2710 | 2709 | |
| 2711 | 2710 | /** |
@@ -46,8 +46,7 @@ discard block |
||
| 46 | 46 | $props = mapi_getprops($mapimessage, [PR_MESSAGE_CLASS]); |
| 47 | 47 | if (isset($props[PR_MESSAGE_CLASS])) { |
| 48 | 48 | $messageclass = $props[PR_MESSAGE_CLASS]; |
| 49 | - } |
|
| 50 | - else { |
|
| 49 | + } else { |
|
| 51 | 50 | $messageclass = "IPM"; |
| 52 | 51 | } |
| 53 | 52 | |
@@ -177,16 +176,14 @@ discard block |
||
| 177 | 176 | if (isset($messageprops[$appointmentprops["reminderset"]]) && $messageprops[$appointmentprops["reminderset"]] == true) { |
| 178 | 177 | if ($messageprops[$appointmentprops["remindertime"]] == 0x5AE980E1) { |
| 179 | 178 | $message->reminder = 15; |
| 180 | - } |
|
| 181 | - else { |
|
| 179 | + } else { |
|
| 182 | 180 | $message->reminder = $messageprops[$appointmentprops["remindertime"]]; |
| 183 | 181 | } |
| 184 | 182 | } |
| 185 | 183 | |
| 186 | 184 | if (!isset($message->uid)) { |
| 187 | 185 | $message->uid = bin2hex($messageprops[$appointmentprops["sourcekey"]]); |
| 188 | - } |
|
| 189 | - else { |
|
| 186 | + } else { |
|
| 190 | 187 | $message->uid = Utils::GetICalUidFromOLUid($message->uid); |
| 191 | 188 | } |
| 192 | 189 | |
@@ -205,14 +202,12 @@ discard block |
||
| 205 | 202 | if (!empty($messageprops[$appointmentprops["timezonetag"]])) { |
| 206 | 203 | $tz = $this->getTZFromMAPIBlob($messageprops[$appointmentprops["timezonetag"]]); |
| 207 | 204 | $appTz = true; |
| 208 | - } |
|
| 209 | - elseif (!empty($messageprops[$appointmentprops["timezonedesc"]])) { |
|
| 205 | + } elseif (!empty($messageprops[$appointmentprops["timezonedesc"]])) { |
|
| 210 | 206 | // Windows uses UTC in timezone description in opposite to mstzones in TimezoneUtil which uses GMT |
| 211 | 207 | $wintz = str_replace("UTC", "GMT", $messageprops[$appointmentprops["timezonedesc"]]); |
| 212 | 208 | $tz = TimezoneUtil::GetFullTZFromTZName(TimezoneUtil::GetTZNameFromWinTZ($wintz)); |
| 213 | 209 | $appTz = true; |
| 214 | - } |
|
| 215 | - else { |
|
| 210 | + } else { |
|
| 216 | 211 | // set server default timezone (correct timezone should be configured!) |
| 217 | 212 | $tz = TimezoneUtil::GetFullTZ(); |
| 218 | 213 | } |
@@ -252,8 +247,7 @@ discard block |
||
| 252 | 247 | // smtp address is always a proper email address |
| 253 | 248 | if (isset($row[PR_SMTP_ADDRESS])) { |
| 254 | 249 | $attendee->email = w2u($row[PR_SMTP_ADDRESS]); |
| 255 | - } |
|
| 256 | - elseif (isset($row[PR_ADDRTYPE], $row[PR_EMAIL_ADDRESS])) { |
|
| 250 | + } elseif (isset($row[PR_ADDRTYPE], $row[PR_EMAIL_ADDRESS])) { |
|
| 257 | 251 | // if address type is SMTP, it's also a proper email address |
| 258 | 252 | if ($row[PR_ADDRTYPE] == "SMTP") { |
| 259 | 253 | $attendee->email = w2u($row[PR_EMAIL_ADDRESS]); |
@@ -268,8 +262,7 @@ discard block |
||
| 268 | 262 | // @see https://jira.z-hub.io/browse/ZP-1178 |
| 269 | 263 | elseif (isset($row[PR_SEARCH_KEY])) { |
| 270 | 264 | $attendee->email = w2u($this->getEmailAddressFromSearchKey($row[PR_SEARCH_KEY])); |
| 271 | - } |
|
| 272 | - else { |
|
| 265 | + } else { |
|
| 273 | 266 | SLog::Write(LOGLEVEL_WARN, sprintf("MAPIProvider->getAppointment: The attendee '%s' of type ZARAFA can not be resolved. Code: 0x%X", $row[PR_EMAIL_ADDRESS], mapi_last_hresult())); |
| 274 | 267 | } |
| 275 | 268 | } |
@@ -331,8 +324,7 @@ discard block |
||
| 331 | 324 | |
| 332 | 325 | if (!isset($message->nativebodytype)) { |
| 333 | 326 | $message->nativebodytype = MAPIUtils::GetNativeBodyType($messageprops); |
| 334 | - } |
|
| 335 | - elseif ($message->nativebodytype == SYNC_BODYPREFERENCE_UNDEFINED) { |
|
| 327 | + } elseif ($message->nativebodytype == SYNC_BODYPREFERENCE_UNDEFINED) { |
|
| 336 | 328 | $nbt = MAPIUtils::GetNativeBodyType($messageprops); |
| 337 | 329 | SLog::Write(LOGLEVEL_INFO, sprintf("MAPIProvider->getAppointment(): native body type is undefined. Set it to %d.", $nbt)); |
| 338 | 330 | $message->nativebodytype = $nbt; |
@@ -381,8 +373,7 @@ discard block |
||
| 381 | 373 | private function getRecurrence($mapimessage, $recurprops, &$syncMessage, &$syncRecurrence, $tz) { |
| 382 | 374 | if ($syncRecurrence instanceof SyncTaskRecurrence) { |
| 383 | 375 | $recurrence = new TaskRecurrence($this->store, $mapimessage); |
| 384 | - } |
|
| 385 | - else { |
|
| 376 | + } else { |
|
| 386 | 377 | $recurrence = new Recurrence($this->store, $mapimessage); |
| 387 | 378 | } |
| 388 | 379 | |
@@ -606,8 +597,7 @@ discard block |
||
| 606 | 597 | |
| 607 | 598 | if (isset($messageprops[PR_SOURCE_KEY])) { |
| 608 | 599 | $sourcekey = $messageprops[PR_SOURCE_KEY]; |
| 609 | - } |
|
| 610 | - else { |
|
| 600 | + } else { |
|
| 611 | 601 | $mbe = new SyncObjectBrokenException("The message doesn't have a sourcekey"); |
| 612 | 602 | $mbe->SetSyncObject($message); |
| 613 | 603 | |
@@ -638,8 +628,7 @@ discard block |
||
| 638 | 628 | |
| 639 | 629 | if ($fromname) { |
| 640 | 630 | $from = "\"" . w2u($fromname) . "\" <" . w2u($fromaddr) . ">"; |
| 641 | - } |
|
| 642 | - else { // START CHANGED dw2412 HTC shows "error" if sender name is unknown |
|
| 631 | + } else { // START CHANGED dw2412 HTC shows "error" if sender name is unknown |
|
| 643 | 632 | $from = "\"" . w2u($fromaddr) . "\" <" . w2u($fromaddr) . ">"; |
| 644 | 633 | } |
| 645 | 634 | // END CHANGED dw2412 HTC shows "error" if sender name is unknown |
@@ -662,8 +651,7 @@ discard block |
||
| 662 | 651 | // Set Timezone |
| 663 | 652 | if (isset($props[$meetingrequestproperties["timezonetag"]])) { |
| 664 | 653 | $tz = $this->getTZFromMAPIBlob($props[$meetingrequestproperties["timezonetag"]]); |
| 665 | - } |
|
| 666 | - else { |
|
| 654 | + } else { |
|
| 667 | 655 | $tz = TimezoneUtil::GetFullTZ(); |
| 668 | 656 | } |
| 669 | 657 | |
@@ -675,12 +663,10 @@ discard block |
||
| 675 | 663 | if (isset($props[$meetingrequestproperties["recReplTime"]])) { |
| 676 | 664 | $basedate = $props[$meetingrequestproperties["recReplTime"]]; |
| 677 | 665 | $message->meetingrequest->recurrenceid = $this->getGMTTimeByTZ($basedate, $this->getGMTTZ()); |
| 678 | - } |
|
| 679 | - else { |
|
| 666 | + } else { |
|
| 680 | 667 | if (!isset($props[$meetingrequestproperties["goidtag"]]) || !isset($props[$meetingrequestproperties["recurStartTime"]]) || !isset($props[$meetingrequestproperties["timezonetag"]])) { |
| 681 | 668 | SLog::Write(LOGLEVEL_WARN, "Missing property to set correct basedate for exception"); |
| 682 | - } |
|
| 683 | - else { |
|
| 669 | + } else { |
|
| 684 | 670 | $basedate = Utils::ExtractBaseDate($props[$meetingrequestproperties["goidtag"]], $props[$meetingrequestproperties["recurStartTime"]]); |
| 685 | 671 | $message->meetingrequest->recurrenceid = $this->getGMTTimeByTZ($basedate, $tz); |
| 686 | 672 | } |
@@ -690,8 +676,7 @@ discard block |
||
| 690 | 676 | // Organizer is the sender |
| 691 | 677 | if (strpos($message->messageclass, "IPM.Schedule.Meeting.Resp") === 0) { |
| 692 | 678 | $message->meetingrequest->organizer = $message->to; |
| 693 | - } |
|
| 694 | - else { |
|
| 679 | + } else { |
|
| 695 | 680 | $message->meetingrequest->organizer = $message->from; |
| 696 | 681 | } |
| 697 | 682 | |
@@ -716,12 +701,10 @@ discard block |
||
| 716 | 701 | $message->meetingrequest->instancetype = 0; |
| 717 | 702 | if (isset($props[$meetingrequestproperties["isrecurringtag"]]) && $props[$meetingrequestproperties["isrecurringtag"]] == 1) { |
| 718 | 703 | $message->meetingrequest->instancetype = 1; |
| 719 | - } |
|
| 720 | - elseif ((!isset($props[$meetingrequestproperties["isrecurringtag"]]) || $props[$meetingrequestproperties["isrecurringtag"]] == 0) && isset($message->meetingrequest->recurrenceid)) { |
|
| 704 | + } elseif ((!isset($props[$meetingrequestproperties["isrecurringtag"]]) || $props[$meetingrequestproperties["isrecurringtag"]] == 0) && isset($message->meetingrequest->recurrenceid)) { |
|
| 721 | 705 | if (isset($props[$meetingrequestproperties["appSeqNr"]]) && $props[$meetingrequestproperties["appSeqNr"]] == 0) { |
| 722 | 706 | $message->meetingrequest->instancetype = 2; |
| 723 | - } |
|
| 724 | - else { |
|
| 707 | + } else { |
|
| 725 | 708 | $message->meetingrequest->instancetype = 3; |
| 726 | 709 | } |
| 727 | 710 | } |
@@ -735,8 +718,7 @@ discard block |
||
| 735 | 718 | // /set the default reminder time to seconds |
| 736 | 719 | if ($props[$meetingrequestproperties["remindertime"]] == 0x5AE980E1) { |
| 737 | 720 | $message->meetingrequest->reminder = 900; |
| 738 | - } |
|
| 739 | - else { |
|
| 721 | + } else { |
|
| 740 | 722 | $message->meetingrequest->reminder = $props[$meetingrequestproperties["remindertime"]] * 60; |
| 741 | 723 | } |
| 742 | 724 | } |
@@ -820,8 +802,7 @@ discard block |
||
| 820 | 802 | if (isset($row[PR_ATTACH_NUM])) { |
| 821 | 803 | if (Request::GetProtocolVersion() >= 12.0) { |
| 822 | 804 | $attach = new SyncBaseAttachment(); |
| 823 | - } |
|
| 824 | - else { |
|
| 805 | + } else { |
|
| 825 | 806 | $attach = new SyncAttachment(); |
| 826 | 807 | } |
| 827 | 808 | |
@@ -867,8 +848,7 @@ discard block |
||
| 867 | 848 | } |
| 868 | 849 | $stat = mapi_stream_stat($stream); |
| 869 | 850 | $attach->estimatedDataSize = $stat['cb']; |
| 870 | - } |
|
| 871 | - else { |
|
| 851 | + } else { |
|
| 872 | 852 | $attach->estimatedDataSize = $attachprops[PR_ATTACH_SIZE]; |
| 873 | 853 | } |
| 874 | 854 | |
@@ -893,8 +873,7 @@ discard block |
||
| 893 | 873 | } |
| 894 | 874 | |
| 895 | 875 | array_push($message->asattachments, $attach); |
| 896 | - } |
|
| 897 | - else { |
|
| 876 | + } else { |
|
| 898 | 877 | $attach->attsize = $attachprops[PR_ATTACH_SIZE]; |
| 899 | 878 | $attach->attname = sprintf("%s:%s:%s", $entryid, $row[PR_ATTACH_NUM], $parentSourcekey); |
| 900 | 879 | if (!isset($message->attachments)) { |
@@ -922,11 +901,9 @@ discard block |
||
| 922 | 901 | |
| 923 | 902 | if (isset($row[PR_SMTP_ADDRESS])) { |
| 924 | 903 | $address = $row[PR_SMTP_ADDRESS]; |
| 925 | - } |
|
| 926 | - elseif ($addrtype == "SMTP" && isset($row[PR_EMAIL_ADDRESS])) { |
|
| 904 | + } elseif ($addrtype == "SMTP" && isset($row[PR_EMAIL_ADDRESS])) { |
|
| 927 | 905 | $address = $row[PR_EMAIL_ADDRESS]; |
| 928 | - } |
|
| 929 | - elseif ($addrtype == "ZARAFA" && isset($row[PR_ENTRYID])) { |
|
| 906 | + } elseif ($addrtype == "ZARAFA" && isset($row[PR_ENTRYID])) { |
|
| 930 | 907 | $address = $this->getSMTPAddressFromEntryID($row[PR_ENTRYID]); |
| 931 | 908 | } |
| 932 | 909 | |
@@ -940,20 +917,17 @@ discard block |
||
| 940 | 917 | |
| 941 | 918 | if ($name == "" || $name == $address) { |
| 942 | 919 | $fulladdr = w2u($address); |
| 943 | - } |
|
| 944 | - else { |
|
| 920 | + } else { |
|
| 945 | 921 | if (substr($name, 0, 1) != '"' && substr($name, -1) != '"') { |
| 946 | 922 | $fulladdr = "\"" . w2u($name) . "\" <" . w2u($address) . ">"; |
| 947 | - } |
|
| 948 | - else { |
|
| 923 | + } else { |
|
| 949 | 924 | $fulladdr = w2u($name) . "<" . w2u($address) . ">"; |
| 950 | 925 | } |
| 951 | 926 | } |
| 952 | 927 | |
| 953 | 928 | if ($row[PR_RECIPIENT_TYPE] == MAPI_TO) { |
| 954 | 929 | array_push($message->to, $fulladdr); |
| 955 | - } |
|
| 956 | - elseif ($row[PR_RECIPIENT_TYPE] == MAPI_CC) { |
|
| 930 | + } elseif ($row[PR_RECIPIENT_TYPE] == MAPI_CC) { |
|
| 957 | 931 | array_push($message->cc, $fulladdr); |
| 958 | 932 | } |
| 959 | 933 | } |
@@ -981,8 +955,7 @@ discard block |
||
| 981 | 955 | |
| 982 | 956 | if (!isset($message->nativebodytype)) { |
| 983 | 957 | $message->nativebodytype = MAPIUtils::GetNativeBodyType($messageprops); |
| 984 | - } |
|
| 985 | - elseif ($message->nativebodytype == SYNC_BODYPREFERENCE_UNDEFINED) { |
|
| 958 | + } elseif ($message->nativebodytype == SYNC_BODYPREFERENCE_UNDEFINED) { |
|
| 986 | 959 | $nbt = MAPIUtils::GetNativeBodyType($messageprops); |
| 987 | 960 | SLog::Write(LOGLEVEL_INFO, sprintf("MAPIProvider->getEmail(): native body type is undefined. Set it to %d.", $nbt)); |
| 988 | 961 | $message->nativebodytype = $nbt; |
@@ -1071,8 +1044,7 @@ discard block |
||
| 1071 | 1044 | $folder->serverid = GSync::GetDeviceManager()->GetFolderIdForBackendId($folder->BackendId, true, $folderOrigin, $folderprops[PR_DISPLAY_NAME]); |
| 1072 | 1045 | if ($folderprops[PR_PARENT_ENTRYID] == $storeprops[PR_IPM_SUBTREE_ENTRYID] || $folderprops[PR_PARENT_ENTRYID] == $storeprops[PR_IPM_PUBLIC_FOLDERS_ENTRYID]) { |
| 1073 | 1046 | $folder->parentid = "0"; |
| 1074 | - } |
|
| 1075 | - else { |
|
| 1047 | + } else { |
|
| 1076 | 1048 | $folder->parentid = GSync::GetDeviceManager()->GetFolderIdForBackendId(bin2hex($folderprops[PR_PARENT_SOURCE_KEY])); |
| 1077 | 1049 | } |
| 1078 | 1050 | $folder->displayname = w2u($folderprops[PR_DISPLAY_NAME]); |
@@ -1322,8 +1294,7 @@ discard block |
||
| 1322 | 1294 | $delprops[] = $flagprops["completetime"]; |
| 1323 | 1295 | $delprops[] = $flagprops["flagstatus"]; |
| 1324 | 1296 | $delprops[] = $flagprops["flagicon"]; |
| 1325 | - } |
|
| 1326 | - else { |
|
| 1297 | + } else { |
|
| 1327 | 1298 | $this->setPropsInMAPI($mapimessage, $message->flag, $flagmapping); |
| 1328 | 1299 | $props[$flagprops["todoitemsflags"]] = 1; |
| 1329 | 1300 | if (isset($message->subject) && strlen($message->subject) > 0) { |
@@ -1373,8 +1344,7 @@ discard block |
||
| 1373 | 1344 | // Get timezone info |
| 1374 | 1345 | if (isset($appointment->timezone)) { |
| 1375 | 1346 | $tz = $this->getTZFromSyncBlob(base64_decode($appointment->timezone)); |
| 1376 | - } |
|
| 1377 | - else { |
|
| 1347 | + } else { |
|
| 1378 | 1348 | $tz = false; |
| 1379 | 1349 | } |
| 1380 | 1350 | |
@@ -1497,8 +1467,7 @@ discard block |
||
| 1497 | 1467 | $noexceptions = false; |
| 1498 | 1468 | // Delete exception |
| 1499 | 1469 | $recurrence->createException([], $basedate, true); |
| 1500 | - } |
|
| 1501 | - else { |
|
| 1470 | + } else { |
|
| 1502 | 1471 | // Change exception |
| 1503 | 1472 | $mapiexception = ["basedate" => $basedate]; |
| 1504 | 1473 | // other exception properties which are not handled in recurrence |
@@ -1550,8 +1519,7 @@ discard block |
||
| 1550 | 1519 | $noexceptions = false; |
| 1551 | 1520 | if ($recurrence->isException($basedate)) { |
| 1552 | 1521 | $recurrence->modifyException($exceptionprops, $basedate); |
| 1553 | - } |
|
| 1554 | - else { |
|
| 1522 | + } else { |
|
| 1555 | 1523 | $recurrence->createException($exceptionprops, $basedate); |
| 1556 | 1524 | } |
| 1557 | 1525 | } |
@@ -1563,8 +1531,7 @@ discard block |
||
| 1563 | 1531 | if ($noexceptions) { |
| 1564 | 1532 | $recurrence->setRecurrence($tz, $recur); |
| 1565 | 1533 | } |
| 1566 | - } |
|
| 1567 | - else { |
|
| 1534 | + } else { |
|
| 1568 | 1535 | $props[$appointmentprops["isrecurring"]] = false; |
| 1569 | 1536 | } |
| 1570 | 1537 | |
@@ -1640,8 +1607,7 @@ discard block |
||
| 1640 | 1607 | $recip[PR_RECIPIENT_TYPE] = isset($attendee->attendeetype) ? $attendee->attendeetype : MAPI_TO; |
| 1641 | 1608 | $recip[PR_RECIPIENT_FLAGS] = recipSendable; |
| 1642 | 1609 | $recip[PR_RECIPIENT_TRACKSTATUS] = isset($attendee->attendeestatus) ? $attendee->attendeestatus : olResponseNone; |
| 1643 | - } |
|
| 1644 | - else { |
|
| 1610 | + } else { |
|
| 1645 | 1611 | $recip[PR_DISPLAY_NAME] = u2w($attendee->name); |
| 1646 | 1612 | $recip[PR_SEARCH_KEY] = "SMTP:" . $recip[PR_EMAIL_ADDRESS] . "\0"; |
| 1647 | 1613 | $recip[PR_ADDRTYPE] = "SMTP"; |
@@ -1727,12 +1693,10 @@ discard block |
||
| 1727 | 1693 | if (isset($props[$contactprops["businessaddress"]])) { |
| 1728 | 1694 | $props[$contactprops["mailingaddress"]] = 2; |
| 1729 | 1695 | $this->setMailingAddress($contact->businesscity, $contact->businesscountry, $contact->businesspostalcode, $contact->businessstate, $contact->businessstreet, $props[$contactprops["businessaddress"]], $props, $contactprops); |
| 1730 | - } |
|
| 1731 | - elseif (isset($props[$contactprops["homeaddress"]])) { |
|
| 1696 | + } elseif (isset($props[$contactprops["homeaddress"]])) { |
|
| 1732 | 1697 | $props[$contactprops["mailingaddress"]] = 1; |
| 1733 | 1698 | $this->setMailingAddress($contact->homecity, $contact->homecountry, $contact->homepostalcode, $contact->homestate, $contact->homestreet, $props[$contactprops["homeaddress"]], $props, $contactprops); |
| 1734 | - } |
|
| 1735 | - elseif (isset($props[$contactprops["otheraddress"]])) { |
|
| 1699 | + } elseif (isset($props[$contactprops["otheraddress"]])) { |
|
| 1736 | 1700 | $props[$contactprops["mailingaddress"]] = 3; |
| 1737 | 1701 | $this->setMailingAddress($contact->othercity, $contact->othercountry, $contact->otherpostalcode, $contact->otherstate, $contact->otherstreet, $props[$contactprops["otheraddress"]], $props, $contactprops); |
| 1738 | 1702 | } |
@@ -1793,8 +1757,7 @@ discard block |
||
| 1793 | 1757 | $middlename = (isset($contact->middlename)) ? $contact->middlename : ""; |
| 1794 | 1758 | $company = (isset($contact->companyname)) ? $contact->companyname : ""; |
| 1795 | 1759 | $props[$contactprops["fileas"]] = Utils::BuildFileAs($lastname, $firstname, $middlename, $company); |
| 1796 | - } |
|
| 1797 | - else { |
|
| 1760 | + } else { |
|
| 1798 | 1761 | SLog::Write(LOGLEVEL_DEBUG, "FILEAS_ORDER not defined"); |
| 1799 | 1762 | } |
| 1800 | 1763 | |
@@ -1833,8 +1796,7 @@ discard block |
||
| 1833 | 1796 | $props[$taskprops["completion"]] = 1.0; |
| 1834 | 1797 | $props[$taskprops["status"]] = 2; |
| 1835 | 1798 | $props[$taskprops["reminderset"]] = false; |
| 1836 | - } |
|
| 1837 | - else { |
|
| 1799 | + } else { |
|
| 1838 | 1800 | // Set completion to 0% |
| 1839 | 1801 | // Set status to 'not started' |
| 1840 | 1802 | $props[$taskprops["completion"]] = 0.0; |
@@ -1993,12 +1955,10 @@ discard block |
||
| 1993 | 1955 | if (mapi_prop_type($mapiprop) != PT_BINARY && mapi_prop_type($mapiprop) != PT_MV_BINARY) { |
| 1994 | 1956 | if (is_array($message->{$asprop})) { |
| 1995 | 1957 | $value = array_map("u2wi", $message->{$asprop}); |
| 1996 | - } |
|
| 1997 | - else { |
|
| 1958 | + } else { |
|
| 1998 | 1959 | $value = u2wi($message->{$asprop}); |
| 1999 | 1960 | } |
| 2000 | - } |
|
| 2001 | - else { |
|
| 1961 | + } else { |
|
| 2002 | 1962 | $value = $message->{$asprop}; |
| 2003 | 1963 | } |
| 2004 | 1964 | |
@@ -2033,13 +1993,11 @@ discard block |
||
| 2033 | 1993 | if (is_array($value) && empty($value)) { |
| 2034 | 1994 | $propsToDelete[] = $mapiprop; |
| 2035 | 1995 | SLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->setPropsInMAPI(): Property '%s' to be deleted as it is an empty array", $asprop)); |
| 2036 | - } |
|
| 2037 | - else { |
|
| 1996 | + } else { |
|
| 2038 | 1997 | // all properties will be set at once |
| 2039 | 1998 | $propsToSet[$mapiprop] = $value; |
| 2040 | 1999 | } |
| 2041 | - } |
|
| 2042 | - elseif (in_array($asprop, $unsetVars)) { |
|
| 2000 | + } elseif (in_array($asprop, $unsetVars)) { |
|
| 2043 | 2001 | $propsToDelete[] = $mapiprop; |
| 2044 | 2002 | } |
| 2045 | 2003 | } |
@@ -2099,12 +2057,10 @@ discard block |
||
| 2099 | 2057 | // Force to actual '0' or '1' |
| 2100 | 2058 | if ($messageprops[$mapiprop]) { |
| 2101 | 2059 | $message->{$asprop} = 1; |
| 2102 | - } |
|
| 2103 | - else { |
|
| 2060 | + } else { |
|
| 2104 | 2061 | $message->{$asprop} = 0; |
| 2105 | 2062 | } |
| 2106 | - } |
|
| 2107 | - else { |
|
| 2063 | + } else { |
|
| 2108 | 2064 | // Special handling for PR_MESSAGE_FLAGS |
| 2109 | 2065 | if ($mapiprop == PR_MESSAGE_FLAGS) { |
| 2110 | 2066 | $message->{$asprop} = $messageprops[$mapiprop] & 1; |
@@ -2112,15 +2068,12 @@ discard block |
||
| 2112 | 2068 | elseif ($mapiprop == PR_RTF_COMPRESSED) { |
| 2113 | 2069 | // do not send rtf to the mobile |
| 2114 | 2070 | continue; |
| 2115 | - } |
|
| 2116 | - elseif (is_array($messageprops[$mapiprop])) { |
|
| 2071 | + } elseif (is_array($messageprops[$mapiprop])) { |
|
| 2117 | 2072 | $message->{$asprop} = array_map("w2u", $messageprops[$mapiprop]); |
| 2118 | - } |
|
| 2119 | - else { |
|
| 2073 | + } else { |
|
| 2120 | 2074 | if (mapi_prop_type($mapiprop) != PT_BINARY && mapi_prop_type($mapiprop) != PT_MV_BINARY) { |
| 2121 | 2075 | $message->{$asprop} = w2u($messageprops[$mapiprop]); |
| 2122 | - } |
|
| 2123 | - else { |
|
| 2076 | + } else { |
|
| 2124 | 2077 | $message->{$asprop} = $messageprops[$mapiprop]; |
| 2125 | 2078 | } |
| 2126 | 2079 | } |
@@ -2316,17 +2269,14 @@ discard block |
||
| 2316 | 2269 | // northern hemisphere (july = dst) |
| 2317 | 2270 | if ($localtime >= $start && $localtime < $end) { |
| 2318 | 2271 | $dst = true; |
| 2319 | - } |
|
| 2320 | - else { |
|
| 2272 | + } else { |
|
| 2321 | 2273 | $dst = false; |
| 2322 | 2274 | } |
| 2323 | - } |
|
| 2324 | - else { |
|
| 2275 | + } else { |
|
| 2325 | 2276 | // southern hemisphere (january = dst) |
| 2326 | 2277 | if ($localtime >= $end && $localtime < $start) { |
| 2327 | 2278 | $dst = false; |
| 2328 | - } |
|
| 2329 | - else { |
|
| 2279 | + } else { |
|
| 2330 | 2280 | $dst = true; |
| 2331 | 2281 | } |
| 2332 | 2282 | } |
@@ -2372,8 +2322,7 @@ discard block |
||
| 2372 | 2322 | $monthnow = gmdate("n", $date); // gmdate returns 1-12 |
| 2373 | 2323 | if ($monthnow > $month) { |
| 2374 | 2324 | $date = $date - (24 * 7 * 60 * 60); |
| 2375 | - } |
|
| 2376 | - else { |
|
| 2325 | + } else { |
|
| 2377 | 2326 | break; |
| 2378 | 2327 | } |
| 2379 | 2328 | } |
@@ -2602,8 +2551,7 @@ discard block |
||
| 2602 | 2551 | $recur["type"] = 10; |
| 2603 | 2552 | if (isset($message->recurrence->dayofweek)) { |
| 2604 | 2553 | $recur["subtype"] = 1; |
| 2605 | - } |
|
| 2606 | - else { |
|
| 2554 | + } else { |
|
| 2607 | 2555 | $recur["subtype"] = 0; |
| 2608 | 2556 | } |
| 2609 | 2557 | |
@@ -2660,12 +2608,10 @@ discard block |
||
| 2660 | 2608 | if (isset($message->recurrence->until)) { |
| 2661 | 2609 | $recur["term"] = 0x21; |
| 2662 | 2610 | $recur["end"] = $message->recurrence->until; |
| 2663 | - } |
|
| 2664 | - elseif (isset($message->recurrence->occurrences)) { |
|
| 2611 | + } elseif (isset($message->recurrence->occurrences)) { |
|
| 2665 | 2612 | $recur["term"] = 0x22; |
| 2666 | 2613 | $recur["numoccur"] = $message->recurrence->occurrences; |
| 2667 | - } |
|
| 2668 | - else { |
|
| 2614 | + } else { |
|
| 2669 | 2615 | $recur["term"] = 0x23; |
| 2670 | 2616 | } |
| 2671 | 2617 | |
@@ -2745,8 +2691,7 @@ discard block |
||
| 2745 | 2691 | if ($stream) { |
| 2746 | 2692 | $stat = mapi_stream_stat($stream); |
| 2747 | 2693 | $streamsize = $stat['cb']; |
| 2748 | - } |
|
| 2749 | - else { |
|
| 2694 | + } else { |
|
| 2750 | 2695 | $streamsize = 0; |
| 2751 | 2696 | } |
| 2752 | 2697 | |
@@ -2757,24 +2702,20 @@ discard block |
||
| 2757 | 2702 | if ($bpReturnType == SYNC_BODYPREFERENCE_RTF) { |
| 2758 | 2703 | $body = $this->mapiReadStream($stream, $streamsize); |
| 2759 | 2704 | $message->asbody->data = StringStreamWrapper::Open(base64_encode($body)); |
| 2760 | - } |
|
| 2761 | - elseif (isset($message->internetcpid) && $bpReturnType == SYNC_BODYPREFERENCE_HTML) { |
|
| 2705 | + } elseif (isset($message->internetcpid) && $bpReturnType == SYNC_BODYPREFERENCE_HTML) { |
|
| 2762 | 2706 | // if PR_HTML is UTF-8 we can stream it directly, else we have to convert to UTF-8 & wrap it |
| 2763 | 2707 | if ($message->internetcpid == INTERNET_CPID_UTF8) { |
| 2764 | 2708 | $message->asbody->data = MAPIStreamWrapper::Open($stream, $truncateHtmlSafe); |
| 2765 | - } |
|
| 2766 | - else { |
|
| 2709 | + } else { |
|
| 2767 | 2710 | $body = $this->mapiReadStream($stream, $streamsize); |
| 2768 | 2711 | $message->asbody->data = StringStreamWrapper::Open(Utils::ConvertCodepageStringToUtf8($message->internetcpid, $body), $truncateHtmlSafe); |
| 2769 | 2712 | $message->internetcpid = INTERNET_CPID_UTF8; |
| 2770 | 2713 | } |
| 2771 | - } |
|
| 2772 | - else { |
|
| 2714 | + } else { |
|
| 2773 | 2715 | $message->asbody->data = MAPIStreamWrapper::Open($stream); |
| 2774 | 2716 | } |
| 2775 | 2717 | $message->asbody->estimatedDataSize = $streamsize; |
| 2776 | - } |
|
| 2777 | - else { |
|
| 2718 | + } else { |
|
| 2778 | 2719 | $body = $this->mapiReadStream($stream, $streamsize); |
| 2779 | 2720 | $message->body = str_replace("\n", "\r\n", w2u(str_replace("\r", "", $body))); |
| 2780 | 2721 | $message->bodysize = $streamsize; |
@@ -2814,8 +2755,7 @@ discard block |
||
| 2814 | 2755 | if (isset($mapiEmail[PR_EC_IMAP_EMAIL]) || MAPIUtils::GetError(PR_EC_IMAP_EMAIL, $mapiEmail) == MAPI_E_NOT_ENOUGH_MEMORY) { |
| 2815 | 2756 | $stream = mapi_openproperty($mapimessage, PR_EC_IMAP_EMAIL, IID_IStream, 0, 0); |
| 2816 | 2757 | SLog::Write(LOGLEVEL_DEBUG, "MAPIProvider->imtoinet(): using PR_EC_IMAP_EMAIL as full RFC822 message"); |
| 2817 | - } |
|
| 2818 | - else { |
|
| 2758 | + } else { |
|
| 2819 | 2759 | $addrbook = $this->getAddressbook(); |
| 2820 | 2760 | $stream = mapi_inetmapi_imtoinet($this->session, $addrbook, $mapimessage, ['use_tnef' => -1, 'ignore_missing_attachments' => 1]); |
| 2821 | 2761 | } |
@@ -2830,8 +2770,7 @@ discard block |
||
| 2830 | 2770 | $message->asbody->data = MAPIStreamWrapper::Open($stream); |
| 2831 | 2771 | $message->asbody->estimatedDataSize = $streamsize; |
| 2832 | 2772 | $message->asbody->truncated = 0; |
| 2833 | - } |
|
| 2834 | - else { |
|
| 2773 | + } else { |
|
| 2835 | 2774 | $message->mimedata = MAPIStreamWrapper::Open($stream); |
| 2836 | 2775 | $message->mimesize = $streamsize; |
| 2837 | 2776 | $message->mimetruncated = 0; |
@@ -2902,8 +2841,7 @@ discard block |
||
| 2902 | 2841 | if (Request::GetProtocolVersion() >= 14.0 && $bpo->GetPreview()) { |
| 2903 | 2842 | $message->asbody->preview = Utils::Utf8_truncate(MAPIUtils::readPropStream($mapimessage, PR_BODY), $bpo->GetPreview()); |
| 2904 | 2843 | } |
| 2905 | - } |
|
| 2906 | - else { |
|
| 2844 | + } else { |
|
| 2907 | 2845 | // Override 'body' for truncation |
| 2908 | 2846 | $truncsize = Utils::GetTruncSize($contentparameters->GetTruncation()); |
| 2909 | 2847 | $this->setMessageBodyForType($mapimessage, SYNC_BODYPREFERENCE_PLAIN, $message); |
@@ -2968,8 +2906,7 @@ discard block |
||
| 2968 | 2906 | case SYNC_BODYPREFERENCE_MIME: |
| 2969 | 2907 | break; |
| 2970 | 2908 | } |
| 2971 | - } |
|
| 2972 | - else { |
|
| 2909 | + } else { |
|
| 2973 | 2910 | SLog::Write(LOGLEVEL_DEBUG, "MAPIProvider->setASbody either type or data are not set. Setting to empty body"); |
| 2974 | 2911 | $props[$appointmentprops["body"]] = ""; |
| 2975 | 2912 | } |
@@ -3105,8 +3042,7 @@ discard block |
||
| 3105 | 3042 | $this->specialFoldersData[] = substr($persistData, 8, $unpackedData['elDataSize']); |
| 3106 | 3043 | // Add PersistId and DataElementsSize lengths to the data size as they're not part of it |
| 3107 | 3044 | $persistData = substr($persistData, $unpackedData['dataSize'] + 4); |
| 3108 | - } |
|
| 3109 | - else { |
|
| 3045 | + } else { |
|
| 3110 | 3046 | SLog::Write(LOGLEVEL_INFO, "MAPIProvider->getSpecialFoldersData(): persistent data is not valid"); |
| 3111 | 3047 | break; |
| 3112 | 3048 | } |
@@ -18,468 +18,468 @@ discard block |
||
| 18 | 18 | * the PDA are always e-mail folders. |
| 19 | 19 | */ |
| 20 | 20 | class ImportChangesICS implements IImportChanges { |
| 21 | - private $folderid; |
|
| 22 | - private $folderidHex; |
|
| 23 | - private $store; |
|
| 24 | - private $session; |
|
| 25 | - private $flags; |
|
| 26 | - private $statestream; |
|
| 27 | - private $importer; |
|
| 28 | - private $memChanges; |
|
| 29 | - private $mapiprovider; |
|
| 30 | - private $conflictsLoaded; |
|
| 31 | - private $conflictsContentParameters; |
|
| 32 | - private $conflictsState; |
|
| 33 | - private $cutoffdate; |
|
| 34 | - private $contentClass; |
|
| 35 | - private $prefix; |
|
| 36 | - |
|
| 37 | - /** |
|
| 38 | - * Constructor. |
|
| 39 | - * |
|
| 40 | - * @param mapisession $session |
|
| 41 | - * @param mapistore $store |
|
| 42 | - * @param string $folderid (opt) |
|
| 43 | - * |
|
| 44 | - * @throws StatusException |
|
| 45 | - */ |
|
| 46 | - public function __construct($session, $store, $folderid = false) { |
|
| 47 | - $this->session = $session; |
|
| 48 | - $this->store = $store; |
|
| 49 | - $this->folderid = $folderid; |
|
| 50 | - $this->folderidHex = bin2hex($folderid); |
|
| 51 | - $this->conflictsLoaded = false; |
|
| 52 | - $this->cutoffdate = false; |
|
| 53 | - $this->contentClass = false; |
|
| 54 | - $this->prefix = ''; |
|
| 55 | - |
|
| 56 | - if ($folderid) { |
|
| 57 | - $entryid = mapi_msgstore_entryidfromsourcekey($store, $folderid); |
|
| 58 | - $folderidForBackendId = GSync::GetDeviceManager()->GetFolderIdForBackendId($this->folderidHex); |
|
| 59 | - // Only append backend id if the mapping backendid<->folderid is available. |
|
| 60 | - if ($folderidForBackendId != $this->folderidHex) { |
|
| 61 | - $this->prefix = $folderidForBackendId . ':'; |
|
| 62 | - } |
|
| 63 | - } |
|
| 64 | - else { |
|
| 65 | - $storeprops = mapi_getprops($store, [PR_IPM_SUBTREE_ENTRYID, PR_IPM_PUBLIC_FOLDERS_ENTRYID]); |
|
| 66 | - if (GSync::GetBackend()->GetImpersonatedUser() == 'system') { |
|
| 67 | - $entryid = $storeprops[PR_IPM_PUBLIC_FOLDERS_ENTRYID]; |
|
| 68 | - } |
|
| 69 | - else { |
|
| 70 | - $entryid = $storeprops[PR_IPM_SUBTREE_ENTRYID]; |
|
| 71 | - } |
|
| 72 | - } |
|
| 73 | - |
|
| 74 | - $folder = false; |
|
| 75 | - if ($entryid) { |
|
| 76 | - $folder = mapi_msgstore_openentry($store, $entryid); |
|
| 77 | - } |
|
| 78 | - |
|
| 79 | - if (!$folder) { |
|
| 80 | - $this->importer = false; |
|
| 81 | - |
|
| 82 | - // We throw an general error SYNC_FSSTATUS_CODEUNKNOWN (12) which is also SYNC_STATUS_FOLDERHIERARCHYCHANGED (12) |
|
| 83 | - // if this happened while doing content sync, the mobile will try to resync the folderhierarchy |
|
| 84 | - throw new StatusException(sprintf("ImportChangesICS('%s','%s'): Error, unable to open folder: 0x%X", $session, bin2hex($folderid), mapi_last_hresult()), SYNC_FSSTATUS_CODEUNKNOWN); |
|
| 85 | - } |
|
| 86 | - |
|
| 87 | - $this->mapiprovider = new MAPIProvider($this->session, $this->store); |
|
| 88 | - |
|
| 89 | - if ($folderid) { |
|
| 90 | - $this->importer = mapi_openproperty($folder, PR_COLLECTOR, IID_IExchangeImportContentsChanges, 0, 0); |
|
| 91 | - } |
|
| 92 | - else { |
|
| 93 | - $this->importer = mapi_openproperty($folder, PR_COLLECTOR, IID_IExchangeImportHierarchyChanges, 0, 0); |
|
| 94 | - } |
|
| 95 | - } |
|
| 96 | - |
|
| 97 | - /** |
|
| 98 | - * Initializes the importer. |
|
| 99 | - * |
|
| 100 | - * @param string $state |
|
| 101 | - * @param int $flags |
|
| 102 | - * |
|
| 103 | - * @throws StatusException |
|
| 104 | - * |
|
| 105 | - * @return bool |
|
| 106 | - */ |
|
| 107 | - public function Config($state, $flags = 0) { |
|
| 108 | - $this->flags = $flags; |
|
| 109 | - |
|
| 110 | - // this should never happen |
|
| 111 | - if ($this->importer === false) { |
|
| 112 | - throw new StatusException("ImportChangesICS->Config(): Error, importer not available", SYNC_FSSTATUS_CODEUNKNOWN, null, LOGLEVEL_ERROR); |
|
| 113 | - } |
|
| 114 | - |
|
| 115 | - // Put the state information in a stream that can be used by ICS |
|
| 116 | - $stream = mapi_stream_create(); |
|
| 117 | - if (strlen($state) == 0) { |
|
| 118 | - $state = hex2bin("0000000000000000"); |
|
| 119 | - } |
|
| 120 | - |
|
| 121 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->Config(): initializing importer with state: 0x%s", bin2hex($state))); |
|
| 122 | - |
|
| 123 | - mapi_stream_write($stream, $state); |
|
| 124 | - $this->statestream = $stream; |
|
| 125 | - |
|
| 126 | - if ($this->folderid !== false) { |
|
| 127 | - // possible conflicting messages will be cached here |
|
| 128 | - $this->memChanges = new ChangesMemoryWrapper(); |
|
| 129 | - $stat = mapi_importcontentschanges_config($this->importer, $stream, $flags); |
|
| 130 | - } |
|
| 131 | - else { |
|
| 132 | - $stat = mapi_importhierarchychanges_config($this->importer, $stream, $flags); |
|
| 133 | - } |
|
| 134 | - |
|
| 135 | - if (!$stat) { |
|
| 136 | - throw new StatusException(sprintf("ImportChangesICS->Config(): Error, mapi_import_*_changes_config() failed: 0x%X", mapi_last_hresult()), SYNC_FSSTATUS_CODEUNKNOWN, null, LOGLEVEL_WARN); |
|
| 137 | - } |
|
| 138 | - |
|
| 139 | - return $stat; |
|
| 140 | - } |
|
| 141 | - |
|
| 142 | - /** |
|
| 143 | - * Configures additional parameters for content selection. |
|
| 144 | - * |
|
| 145 | - * @param ContentParameters $contentparameters |
|
| 146 | - * |
|
| 147 | - * @throws StatusException |
|
| 148 | - * |
|
| 149 | - * @return bool |
|
| 150 | - */ |
|
| 151 | - public function ConfigContentParameters($contentparameters) { |
|
| 152 | - $filtertype = $contentparameters->GetFilterType(); |
|
| 153 | - |
|
| 154 | - switch ($contentparameters->GetContentClass()) { |
|
| 155 | - case "Email": |
|
| 156 | - $this->cutoffdate = ($filtertype) ? Utils::GetCutOffDate($filtertype) : false; |
|
| 157 | - break; |
|
| 158 | - |
|
| 159 | - case "Calendar": |
|
| 160 | - $this->cutoffdate = ($filtertype) ? Utils::GetCutOffDate($filtertype) : false; |
|
| 161 | - break; |
|
| 162 | - |
|
| 163 | - default: |
|
| 164 | - case "Contacts": |
|
| 165 | - case "Tasks": |
|
| 166 | - $this->cutoffdate = false; |
|
| 167 | - break; |
|
| 168 | - } |
|
| 169 | - $this->contentClass = $contentparameters->GetContentClass(); |
|
| 170 | - |
|
| 171 | - return true; |
|
| 172 | - } |
|
| 173 | - |
|
| 174 | - /** |
|
| 175 | - * Reads state from the Importer. |
|
| 176 | - * |
|
| 177 | - * @throws StatusException |
|
| 178 | - * |
|
| 179 | - * @return string |
|
| 180 | - */ |
|
| 181 | - public function GetState() { |
|
| 182 | - $error = false; |
|
| 183 | - if (!isset($this->statestream) || $this->importer === false) { |
|
| 184 | - $error = true; |
|
| 185 | - } |
|
| 186 | - |
|
| 187 | - if ($error === false && $this->folderid !== false && function_exists("mapi_importcontentschanges_updatestate")) { |
|
| 188 | - if (mapi_importcontentschanges_updatestate($this->importer, $this->statestream) != true) { |
|
| 189 | - $error = true; |
|
| 190 | - } |
|
| 191 | - } |
|
| 192 | - |
|
| 193 | - if ($error == true) { |
|
| 194 | - throw new StatusException(sprintf("ImportChangesICS->GetState(): Error, state not available or unable to update: 0x%X", mapi_last_hresult()), (($this->folderid) ? SYNC_STATUS_FOLDERHIERARCHYCHANGED : SYNC_FSSTATUS_CODEUNKNOWN), null, LOGLEVEL_WARN); |
|
| 195 | - } |
|
| 196 | - |
|
| 197 | - mapi_stream_seek($this->statestream, 0, STREAM_SEEK_SET); |
|
| 198 | - |
|
| 199 | - $state = ""; |
|
| 200 | - while (true) { |
|
| 201 | - $data = mapi_stream_read($this->statestream, 4096); |
|
| 202 | - if (strlen($data)) { |
|
| 203 | - $state .= $data; |
|
| 204 | - } |
|
| 205 | - else { |
|
| 206 | - break; |
|
| 207 | - } |
|
| 208 | - } |
|
| 209 | - |
|
| 210 | - return $state; |
|
| 211 | - } |
|
| 212 | - |
|
| 213 | - /** |
|
| 214 | - * Checks if a message may be modified. This involves checking: |
|
| 215 | - * - if there is a synchronization interval and if so, if the message is in it (sync window). |
|
| 216 | - * These checks only apply to Emails and Appointments only, Contacts, Tasks and Notes do not have time restrictions. |
|
| 217 | - * - if the message is not marked as private in a shared folder. |
|
| 218 | - * |
|
| 219 | - * @param string $messageid the message id to be checked |
|
| 220 | - * |
|
| 221 | - * @return bool |
|
| 222 | - */ |
|
| 223 | - private function isModificationAllowed($messageid) { |
|
| 224 | - $sharedUser = GSync::GetAdditionalSyncFolderStore(bin2hex($this->folderid)); |
|
| 225 | - // if this is either a user folder or SYSTEM and no restriction is set, we don't need to check |
|
| 226 | - if (($sharedUser == false || $sharedUser == 'SYSTEM') && $this->cutoffdate === false && !GSync::GetBackend()->GetImpersonatedUser()) { |
|
| 227 | - return true; |
|
| 228 | - } |
|
| 229 | - |
|
| 230 | - // open the existing object |
|
| 231 | - $entryid = mapi_msgstore_entryidfromsourcekey($this->store, $this->folderid, hex2bin($messageid)); |
|
| 232 | - if (!$entryid) { |
|
| 233 | - SLog::Write(LOGLEVEL_WARN, sprintf("ImportChangesICS->isModificationAllowed('%s'): Error, unable to resolve message id: 0x%X", $messageid, mapi_last_hresult())); |
|
| 234 | - |
|
| 235 | - return false; |
|
| 236 | - } |
|
| 237 | - |
|
| 238 | - $mapimessage = mapi_msgstore_openentry($this->store, $entryid); |
|
| 239 | - if (!$mapimessage) { |
|
| 240 | - SLog::Write(LOGLEVEL_WARN, sprintf("ImportChangesICS->isModificationAllowed('%s'): Error, unable to open entry id: 0x%X", $messageid, mapi_last_hresult())); |
|
| 241 | - |
|
| 242 | - return false; |
|
| 243 | - } |
|
| 244 | - |
|
| 245 | - // check the sync interval |
|
| 246 | - if ($this->cutoffdate !== false) { |
|
| 247 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->isModificationAllowed('%s'): cut off date is: %s", $messageid, $this->cutoffdate)); |
|
| 248 | - if (($this->contentClass == "Email" && !MAPIUtils::IsInEmailSyncInterval($this->store, $mapimessage, $this->cutoffdate)) || |
|
| 249 | - ($this->contentClass == "Calendar" && !MAPIUtils::IsInCalendarSyncInterval($this->store, $mapimessage, $this->cutoffdate))) { |
|
| 250 | - SLog::Write(LOGLEVEL_WARN, sprintf("ImportChangesICS->isModificationAllowed('%s'): Message in %s is outside the sync interval. Data not saved.", $messageid, $this->contentClass)); |
|
| 251 | - |
|
| 252 | - return false; |
|
| 253 | - } |
|
| 254 | - } |
|
| 255 | - |
|
| 256 | - // check if not private |
|
| 257 | - if (MAPIUtils::IsMessageSharedAndPrivate($this->folderid, $mapimessage)) { |
|
| 258 | - SLog::Write(LOGLEVEL_WARN, sprintf("ImportChangesICS->isModificationAllowed('%s'): Message is shared and marked as private. Data not saved.", $messageid)); |
|
| 259 | - |
|
| 260 | - return false; |
|
| 261 | - } |
|
| 262 | - |
|
| 263 | - // yes, modification allowed |
|
| 264 | - return true; |
|
| 265 | - } |
|
| 266 | - |
|
| 267 | - /*---------------------------------------------------------------------------------------------------------- |
|
| 21 | + private $folderid; |
|
| 22 | + private $folderidHex; |
|
| 23 | + private $store; |
|
| 24 | + private $session; |
|
| 25 | + private $flags; |
|
| 26 | + private $statestream; |
|
| 27 | + private $importer; |
|
| 28 | + private $memChanges; |
|
| 29 | + private $mapiprovider; |
|
| 30 | + private $conflictsLoaded; |
|
| 31 | + private $conflictsContentParameters; |
|
| 32 | + private $conflictsState; |
|
| 33 | + private $cutoffdate; |
|
| 34 | + private $contentClass; |
|
| 35 | + private $prefix; |
|
| 36 | + |
|
| 37 | + /** |
|
| 38 | + * Constructor. |
|
| 39 | + * |
|
| 40 | + * @param mapisession $session |
|
| 41 | + * @param mapistore $store |
|
| 42 | + * @param string $folderid (opt) |
|
| 43 | + * |
|
| 44 | + * @throws StatusException |
|
| 45 | + */ |
|
| 46 | + public function __construct($session, $store, $folderid = false) { |
|
| 47 | + $this->session = $session; |
|
| 48 | + $this->store = $store; |
|
| 49 | + $this->folderid = $folderid; |
|
| 50 | + $this->folderidHex = bin2hex($folderid); |
|
| 51 | + $this->conflictsLoaded = false; |
|
| 52 | + $this->cutoffdate = false; |
|
| 53 | + $this->contentClass = false; |
|
| 54 | + $this->prefix = ''; |
|
| 55 | + |
|
| 56 | + if ($folderid) { |
|
| 57 | + $entryid = mapi_msgstore_entryidfromsourcekey($store, $folderid); |
|
| 58 | + $folderidForBackendId = GSync::GetDeviceManager()->GetFolderIdForBackendId($this->folderidHex); |
|
| 59 | + // Only append backend id if the mapping backendid<->folderid is available. |
|
| 60 | + if ($folderidForBackendId != $this->folderidHex) { |
|
| 61 | + $this->prefix = $folderidForBackendId . ':'; |
|
| 62 | + } |
|
| 63 | + } |
|
| 64 | + else { |
|
| 65 | + $storeprops = mapi_getprops($store, [PR_IPM_SUBTREE_ENTRYID, PR_IPM_PUBLIC_FOLDERS_ENTRYID]); |
|
| 66 | + if (GSync::GetBackend()->GetImpersonatedUser() == 'system') { |
|
| 67 | + $entryid = $storeprops[PR_IPM_PUBLIC_FOLDERS_ENTRYID]; |
|
| 68 | + } |
|
| 69 | + else { |
|
| 70 | + $entryid = $storeprops[PR_IPM_SUBTREE_ENTRYID]; |
|
| 71 | + } |
|
| 72 | + } |
|
| 73 | + |
|
| 74 | + $folder = false; |
|
| 75 | + if ($entryid) { |
|
| 76 | + $folder = mapi_msgstore_openentry($store, $entryid); |
|
| 77 | + } |
|
| 78 | + |
|
| 79 | + if (!$folder) { |
|
| 80 | + $this->importer = false; |
|
| 81 | + |
|
| 82 | + // We throw an general error SYNC_FSSTATUS_CODEUNKNOWN (12) which is also SYNC_STATUS_FOLDERHIERARCHYCHANGED (12) |
|
| 83 | + // if this happened while doing content sync, the mobile will try to resync the folderhierarchy |
|
| 84 | + throw new StatusException(sprintf("ImportChangesICS('%s','%s'): Error, unable to open folder: 0x%X", $session, bin2hex($folderid), mapi_last_hresult()), SYNC_FSSTATUS_CODEUNKNOWN); |
|
| 85 | + } |
|
| 86 | + |
|
| 87 | + $this->mapiprovider = new MAPIProvider($this->session, $this->store); |
|
| 88 | + |
|
| 89 | + if ($folderid) { |
|
| 90 | + $this->importer = mapi_openproperty($folder, PR_COLLECTOR, IID_IExchangeImportContentsChanges, 0, 0); |
|
| 91 | + } |
|
| 92 | + else { |
|
| 93 | + $this->importer = mapi_openproperty($folder, PR_COLLECTOR, IID_IExchangeImportHierarchyChanges, 0, 0); |
|
| 94 | + } |
|
| 95 | + } |
|
| 96 | + |
|
| 97 | + /** |
|
| 98 | + * Initializes the importer. |
|
| 99 | + * |
|
| 100 | + * @param string $state |
|
| 101 | + * @param int $flags |
|
| 102 | + * |
|
| 103 | + * @throws StatusException |
|
| 104 | + * |
|
| 105 | + * @return bool |
|
| 106 | + */ |
|
| 107 | + public function Config($state, $flags = 0) { |
|
| 108 | + $this->flags = $flags; |
|
| 109 | + |
|
| 110 | + // this should never happen |
|
| 111 | + if ($this->importer === false) { |
|
| 112 | + throw new StatusException("ImportChangesICS->Config(): Error, importer not available", SYNC_FSSTATUS_CODEUNKNOWN, null, LOGLEVEL_ERROR); |
|
| 113 | + } |
|
| 114 | + |
|
| 115 | + // Put the state information in a stream that can be used by ICS |
|
| 116 | + $stream = mapi_stream_create(); |
|
| 117 | + if (strlen($state) == 0) { |
|
| 118 | + $state = hex2bin("0000000000000000"); |
|
| 119 | + } |
|
| 120 | + |
|
| 121 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->Config(): initializing importer with state: 0x%s", bin2hex($state))); |
|
| 122 | + |
|
| 123 | + mapi_stream_write($stream, $state); |
|
| 124 | + $this->statestream = $stream; |
|
| 125 | + |
|
| 126 | + if ($this->folderid !== false) { |
|
| 127 | + // possible conflicting messages will be cached here |
|
| 128 | + $this->memChanges = new ChangesMemoryWrapper(); |
|
| 129 | + $stat = mapi_importcontentschanges_config($this->importer, $stream, $flags); |
|
| 130 | + } |
|
| 131 | + else { |
|
| 132 | + $stat = mapi_importhierarchychanges_config($this->importer, $stream, $flags); |
|
| 133 | + } |
|
| 134 | + |
|
| 135 | + if (!$stat) { |
|
| 136 | + throw new StatusException(sprintf("ImportChangesICS->Config(): Error, mapi_import_*_changes_config() failed: 0x%X", mapi_last_hresult()), SYNC_FSSTATUS_CODEUNKNOWN, null, LOGLEVEL_WARN); |
|
| 137 | + } |
|
| 138 | + |
|
| 139 | + return $stat; |
|
| 140 | + } |
|
| 141 | + |
|
| 142 | + /** |
|
| 143 | + * Configures additional parameters for content selection. |
|
| 144 | + * |
|
| 145 | + * @param ContentParameters $contentparameters |
|
| 146 | + * |
|
| 147 | + * @throws StatusException |
|
| 148 | + * |
|
| 149 | + * @return bool |
|
| 150 | + */ |
|
| 151 | + public function ConfigContentParameters($contentparameters) { |
|
| 152 | + $filtertype = $contentparameters->GetFilterType(); |
|
| 153 | + |
|
| 154 | + switch ($contentparameters->GetContentClass()) { |
|
| 155 | + case "Email": |
|
| 156 | + $this->cutoffdate = ($filtertype) ? Utils::GetCutOffDate($filtertype) : false; |
|
| 157 | + break; |
|
| 158 | + |
|
| 159 | + case "Calendar": |
|
| 160 | + $this->cutoffdate = ($filtertype) ? Utils::GetCutOffDate($filtertype) : false; |
|
| 161 | + break; |
|
| 162 | + |
|
| 163 | + default: |
|
| 164 | + case "Contacts": |
|
| 165 | + case "Tasks": |
|
| 166 | + $this->cutoffdate = false; |
|
| 167 | + break; |
|
| 168 | + } |
|
| 169 | + $this->contentClass = $contentparameters->GetContentClass(); |
|
| 170 | + |
|
| 171 | + return true; |
|
| 172 | + } |
|
| 173 | + |
|
| 174 | + /** |
|
| 175 | + * Reads state from the Importer. |
|
| 176 | + * |
|
| 177 | + * @throws StatusException |
|
| 178 | + * |
|
| 179 | + * @return string |
|
| 180 | + */ |
|
| 181 | + public function GetState() { |
|
| 182 | + $error = false; |
|
| 183 | + if (!isset($this->statestream) || $this->importer === false) { |
|
| 184 | + $error = true; |
|
| 185 | + } |
|
| 186 | + |
|
| 187 | + if ($error === false && $this->folderid !== false && function_exists("mapi_importcontentschanges_updatestate")) { |
|
| 188 | + if (mapi_importcontentschanges_updatestate($this->importer, $this->statestream) != true) { |
|
| 189 | + $error = true; |
|
| 190 | + } |
|
| 191 | + } |
|
| 192 | + |
|
| 193 | + if ($error == true) { |
|
| 194 | + throw new StatusException(sprintf("ImportChangesICS->GetState(): Error, state not available or unable to update: 0x%X", mapi_last_hresult()), (($this->folderid) ? SYNC_STATUS_FOLDERHIERARCHYCHANGED : SYNC_FSSTATUS_CODEUNKNOWN), null, LOGLEVEL_WARN); |
|
| 195 | + } |
|
| 196 | + |
|
| 197 | + mapi_stream_seek($this->statestream, 0, STREAM_SEEK_SET); |
|
| 198 | + |
|
| 199 | + $state = ""; |
|
| 200 | + while (true) { |
|
| 201 | + $data = mapi_stream_read($this->statestream, 4096); |
|
| 202 | + if (strlen($data)) { |
|
| 203 | + $state .= $data; |
|
| 204 | + } |
|
| 205 | + else { |
|
| 206 | + break; |
|
| 207 | + } |
|
| 208 | + } |
|
| 209 | + |
|
| 210 | + return $state; |
|
| 211 | + } |
|
| 212 | + |
|
| 213 | + /** |
|
| 214 | + * Checks if a message may be modified. This involves checking: |
|
| 215 | + * - if there is a synchronization interval and if so, if the message is in it (sync window). |
|
| 216 | + * These checks only apply to Emails and Appointments only, Contacts, Tasks and Notes do not have time restrictions. |
|
| 217 | + * - if the message is not marked as private in a shared folder. |
|
| 218 | + * |
|
| 219 | + * @param string $messageid the message id to be checked |
|
| 220 | + * |
|
| 221 | + * @return bool |
|
| 222 | + */ |
|
| 223 | + private function isModificationAllowed($messageid) { |
|
| 224 | + $sharedUser = GSync::GetAdditionalSyncFolderStore(bin2hex($this->folderid)); |
|
| 225 | + // if this is either a user folder or SYSTEM and no restriction is set, we don't need to check |
|
| 226 | + if (($sharedUser == false || $sharedUser == 'SYSTEM') && $this->cutoffdate === false && !GSync::GetBackend()->GetImpersonatedUser()) { |
|
| 227 | + return true; |
|
| 228 | + } |
|
| 229 | + |
|
| 230 | + // open the existing object |
|
| 231 | + $entryid = mapi_msgstore_entryidfromsourcekey($this->store, $this->folderid, hex2bin($messageid)); |
|
| 232 | + if (!$entryid) { |
|
| 233 | + SLog::Write(LOGLEVEL_WARN, sprintf("ImportChangesICS->isModificationAllowed('%s'): Error, unable to resolve message id: 0x%X", $messageid, mapi_last_hresult())); |
|
| 234 | + |
|
| 235 | + return false; |
|
| 236 | + } |
|
| 237 | + |
|
| 238 | + $mapimessage = mapi_msgstore_openentry($this->store, $entryid); |
|
| 239 | + if (!$mapimessage) { |
|
| 240 | + SLog::Write(LOGLEVEL_WARN, sprintf("ImportChangesICS->isModificationAllowed('%s'): Error, unable to open entry id: 0x%X", $messageid, mapi_last_hresult())); |
|
| 241 | + |
|
| 242 | + return false; |
|
| 243 | + } |
|
| 244 | + |
|
| 245 | + // check the sync interval |
|
| 246 | + if ($this->cutoffdate !== false) { |
|
| 247 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->isModificationAllowed('%s'): cut off date is: %s", $messageid, $this->cutoffdate)); |
|
| 248 | + if (($this->contentClass == "Email" && !MAPIUtils::IsInEmailSyncInterval($this->store, $mapimessage, $this->cutoffdate)) || |
|
| 249 | + ($this->contentClass == "Calendar" && !MAPIUtils::IsInCalendarSyncInterval($this->store, $mapimessage, $this->cutoffdate))) { |
|
| 250 | + SLog::Write(LOGLEVEL_WARN, sprintf("ImportChangesICS->isModificationAllowed('%s'): Message in %s is outside the sync interval. Data not saved.", $messageid, $this->contentClass)); |
|
| 251 | + |
|
| 252 | + return false; |
|
| 253 | + } |
|
| 254 | + } |
|
| 255 | + |
|
| 256 | + // check if not private |
|
| 257 | + if (MAPIUtils::IsMessageSharedAndPrivate($this->folderid, $mapimessage)) { |
|
| 258 | + SLog::Write(LOGLEVEL_WARN, sprintf("ImportChangesICS->isModificationAllowed('%s'): Message is shared and marked as private. Data not saved.", $messageid)); |
|
| 259 | + |
|
| 260 | + return false; |
|
| 261 | + } |
|
| 262 | + |
|
| 263 | + // yes, modification allowed |
|
| 264 | + return true; |
|
| 265 | + } |
|
| 266 | + |
|
| 267 | + /*---------------------------------------------------------------------------------------------------------- |
|
| 268 | 268 | * Methods for ContentsExporter |
| 269 | 269 | */ |
| 270 | 270 | |
| 271 | - /** |
|
| 272 | - * Loads objects which are expected to be exported with the state |
|
| 273 | - * Before importing/saving the actual message from the mobile, a conflict detection should be done. |
|
| 274 | - * |
|
| 275 | - * @param ContentParameters $contentparameters class of objects |
|
| 276 | - * @param string $state |
|
| 277 | - * |
|
| 278 | - * @throws StatusException |
|
| 279 | - * |
|
| 280 | - * @return bool |
|
| 281 | - */ |
|
| 282 | - public function LoadConflicts($contentparameters, $state) { |
|
| 283 | - if (!isset($this->session) || !isset($this->store) || !isset($this->folderid)) { |
|
| 284 | - throw new StatusException("ImportChangesICS->LoadConflicts(): Error, can not load changes for conflict detection. Session, store or folder information not available", SYNC_STATUS_SERVERERROR); |
|
| 285 | - } |
|
| 286 | - |
|
| 287 | - // save data to load changes later if necessary |
|
| 288 | - $this->conflictsLoaded = false; |
|
| 289 | - $this->conflictsContentParameters = $contentparameters; |
|
| 290 | - $this->conflictsState = $state; |
|
| 291 | - |
|
| 292 | - SLog::Write(LOGLEVEL_DEBUG, "ImportChangesICS->LoadConflicts(): will be loaded later if necessary"); |
|
| 293 | - |
|
| 294 | - return true; |
|
| 295 | - } |
|
| 296 | - |
|
| 297 | - /** |
|
| 298 | - * Potential conflicts are only loaded when really necessary, |
|
| 299 | - * e.g. on ADD or MODIFY. |
|
| 300 | - * |
|
| 301 | - * @return bool |
|
| 302 | - */ |
|
| 303 | - private function lazyLoadConflicts() { |
|
| 304 | - if (!isset($this->session) || !isset($this->store) || !isset($this->folderid) || |
|
| 305 | - !isset($this->conflictsContentParameters) || $this->conflictsState === false) { |
|
| 306 | - SLog::Write(LOGLEVEL_WARN, "ImportChangesICS->lazyLoadConflicts(): can not load potential conflicting changes in lazymode for conflict detection. Missing information"); |
|
| 307 | - |
|
| 308 | - return false; |
|
| 309 | - } |
|
| 310 | - |
|
| 311 | - if (!$this->conflictsLoaded) { |
|
| 312 | - SLog::Write(LOGLEVEL_DEBUG, "ImportChangesICS->lazyLoadConflicts(): loading.."); |
|
| 313 | - |
|
| 314 | - // configure an exporter so we can detect conflicts |
|
| 315 | - $exporter = new ExportChangesICS($this->session, $this->store, $this->folderid); |
|
| 316 | - $exporter->Config($this->conflictsState); |
|
| 317 | - $exporter->ConfigContentParameters($this->conflictsContentParameters); |
|
| 318 | - $exporter->InitializeExporter($this->memChanges); |
|
| 319 | - |
|
| 320 | - // monitor how long it takes to export potential conflicts |
|
| 321 | - // if this takes "too long" we cancel this operation! |
|
| 322 | - $potConflicts = $exporter->GetChangeCount(); |
|
| 323 | - if ($potConflicts > 100) { |
|
| 324 | - SLog::Write(LOGLEVEL_WARN, sprintf("ImportChangesICS->lazyLoadConflicts(): conflict detection abandoned as there are too many (%d) changes to be exported.", $potConflicts)); |
|
| 325 | - $this->conflictsLoaded = true; |
|
| 326 | - |
|
| 327 | - return false; |
|
| 328 | - } |
|
| 329 | - $started = time(); |
|
| 330 | - $exported = 0; |
|
| 331 | - |
|
| 332 | - try { |
|
| 333 | - while (is_array($exporter->Synchronize())) { |
|
| 334 | - ++$exported; |
|
| 335 | - |
|
| 336 | - // stop if this takes more than 15 seconds and there are more than 5 changes still to be exported |
|
| 337 | - // within 20 seconds this should be finished or it will not be performed |
|
| 338 | - if ((time() - $started) > 15 && ($potConflicts - $exported) > 5) { |
|
| 339 | - SLog::Write(LOGLEVEL_WARN, sprintf("ImportChangesICS->lazyLoadConflicts(): conflict detection cancelled as operation is too slow. In %d seconds only %d from %d changes were processed.", (time() - $started), $exported, $potConflicts)); |
|
| 340 | - $this->conflictsLoaded = true; |
|
| 341 | - |
|
| 342 | - return false; |
|
| 343 | - } |
|
| 344 | - } |
|
| 345 | - } |
|
| 346 | - // something really bad happened while exporting changes |
|
| 347 | - catch (StatusException $stex) { |
|
| 348 | - SLog::Write(LOGLEVEL_WARN, sprintf("ImportChangesICS->lazyLoadConflicts(): got StatusException code %d while exporting changes. Ignore and mark conflicts as loaded.", $stex->getCode())); |
|
| 349 | - } |
|
| 350 | - $this->conflictsLoaded = true; |
|
| 351 | - } |
|
| 352 | - |
|
| 353 | - return true; |
|
| 354 | - } |
|
| 355 | - |
|
| 356 | - /** |
|
| 357 | - * Imports a single message. |
|
| 358 | - * |
|
| 359 | - * @param string $id |
|
| 360 | - * @param SyncObject $message |
|
| 361 | - * |
|
| 362 | - * @throws StatusException |
|
| 363 | - * |
|
| 364 | - * @return boolean/string - failure / id of message |
|
| 365 | - */ |
|
| 366 | - public function ImportMessageChange($id, $message) { |
|
| 367 | - $flags = 0; |
|
| 368 | - $props = []; |
|
| 369 | - $props[PR_PARENT_SOURCE_KEY] = $this->folderid; |
|
| 370 | - |
|
| 371 | - // set the PR_SOURCE_KEY if available or mark it as new message |
|
| 372 | - if ($id) { |
|
| 373 | - list(, $sk) = Utils::SplitMessageId($id); |
|
| 374 | - $props[PR_SOURCE_KEY] = hex2bin($sk); |
|
| 375 | - |
|
| 376 | - // check if message is in the synchronization interval and/or shared+private |
|
| 377 | - if (!$this->isModificationAllowed($sk)) { |
|
| 378 | - throw new StatusException(sprintf("ImportChangesICS->ImportMessageChange('%s','%s'): Message modification is not allowed. Data not saved.", $id, get_class($message)), SYNC_STATUS_SYNCCANNOTBECOMPLETED); |
|
| 379 | - } |
|
| 380 | - |
|
| 381 | - // check for conflicts |
|
| 382 | - $this->lazyLoadConflicts(); |
|
| 383 | - if ($this->memChanges->IsChanged($id)) { |
|
| 384 | - if ($this->flags & SYNC_CONFLICT_OVERWRITE_PIM) { |
|
| 385 | - // in these cases the status SYNC_STATUS_CONFLICTCLIENTSERVEROBJECT should be returned, so the mobile client can inform the end user |
|
| 386 | - throw new StatusException(sprintf("ImportChangesICS->ImportMessageChange('%s','%s'): Conflict detected. Data from PIM will be dropped! Server overwrites PIM. User is informed.", $id, get_class($message)), SYNC_STATUS_CONFLICTCLIENTSERVEROBJECT, null, LOGLEVEL_INFO); |
|
| 387 | - |
|
| 388 | - return false; |
|
| 389 | - } |
|
| 390 | - |
|
| 391 | - SLog::Write(LOGLEVEL_INFO, sprintf("ImportChangesICS->ImportMessageChange('%s','%s'): Conflict detected. Data from Server will be dropped! PIM overwrites server.", $id, get_class($message))); |
|
| 392 | - } |
|
| 393 | - if ($this->memChanges->IsDeleted($id)) { |
|
| 394 | - SLog::Write(LOGLEVEL_INFO, sprintf("ImportChangesICS->ImportMessageChange('%s','%s'): Conflict detected. Data from PIM will be dropped! Object was deleted on server.", $id, get_class($message))); |
|
| 395 | - |
|
| 396 | - return false; |
|
| 397 | - } |
|
| 398 | - } |
|
| 399 | - else { |
|
| 400 | - $flags = SYNC_NEW_MESSAGE; |
|
| 401 | - } |
|
| 402 | - |
|
| 403 | - if (mapi_importcontentschanges_importmessagechange($this->importer, $props, $flags, $mapimessage)) { |
|
| 404 | - $this->mapiprovider->SetMessage($mapimessage, $message); |
|
| 405 | - mapi_savechanges($mapimessage); |
|
| 406 | - |
|
| 407 | - if (mapi_last_hresult()) { |
|
| 408 | - throw new StatusException(sprintf("ImportChangesICS->ImportMessageChange('%s','%s'): Error, mapi_savechanges() failed: 0x%X", $id, get_class($message), mapi_last_hresult()), SYNC_STATUS_SYNCCANNOTBECOMPLETED); |
|
| 409 | - } |
|
| 410 | - |
|
| 411 | - $sourcekeyprops = mapi_getprops($mapimessage, [PR_SOURCE_KEY]); |
|
| 412 | - |
|
| 413 | - return $this->prefix . bin2hex($sourcekeyprops[PR_SOURCE_KEY]); |
|
| 414 | - } |
|
| 415 | - |
|
| 416 | - throw new StatusException(sprintf("ImportChangesICS->ImportMessageChange('%s','%s'): Error updating object: 0x%X", $id, get_class($message), mapi_last_hresult()), SYNC_STATUS_OBJECTNOTFOUND); |
|
| 417 | - } |
|
| 418 | - |
|
| 419 | - /** |
|
| 420 | - * Imports a deletion. This may conflict if the local object has been modified. |
|
| 421 | - * |
|
| 422 | - * @param string $id |
|
| 423 | - * @param bool $asSoftDelete (opt) if true, the deletion is exported as "SoftDelete", else as "Remove" - default: false |
|
| 424 | - * |
|
| 425 | - * @return bool |
|
| 426 | - */ |
|
| 427 | - public function ImportMessageDeletion($id, $asSoftDelete = false) { |
|
| 428 | - list(, $sk) = Utils::SplitMessageId($id); |
|
| 429 | - |
|
| 430 | - // check if message is in the synchronization interval and/or shared+private |
|
| 431 | - if (!$this->isModificationAllowed($sk)) { |
|
| 432 | - throw new StatusException(sprintf("ImportChangesICS->ImportMessageDeletion('%s'): Message deletion is not allowed. Deletion not executed.", $id), SYNC_STATUS_OBJECTNOTFOUND); |
|
| 433 | - } |
|
| 434 | - |
|
| 435 | - // check for conflicts |
|
| 436 | - $this->lazyLoadConflicts(); |
|
| 437 | - if ($this->memChanges->IsChanged($id)) { |
|
| 438 | - SLog::Write(LOGLEVEL_INFO, sprintf("ImportChangesICS->ImportMessageDeletion('%s'): Conflict detected. Data from Server will be dropped! PIM deleted object.", $id)); |
|
| 439 | - } |
|
| 440 | - elseif ($this->memChanges->IsDeleted($id)) { |
|
| 441 | - SLog::Write(LOGLEVEL_INFO, sprintf("ImportChangesICS->ImportMessageDeletion('%s'): Conflict detected. Data is already deleted. Request will be ignored.", $id)); |
|
| 442 | - |
|
| 443 | - return true; |
|
| 444 | - } |
|
| 445 | - |
|
| 446 | - // do a 'soft' delete so people can un-delete if necessary |
|
| 447 | - if (mapi_importcontentschanges_importmessagedeletion($this->importer, 1, [hex2bin($sk)])) { |
|
| 448 | - throw new StatusException(sprintf("ImportChangesICS->ImportMessageDeletion('%s'): Error updating object: 0x%X", $sk, mapi_last_hresult()), SYNC_STATUS_OBJECTNOTFOUND); |
|
| 449 | - } |
|
| 450 | - |
|
| 451 | - return true; |
|
| 452 | - } |
|
| 453 | - |
|
| 454 | - /** |
|
| 455 | - * Imports a change in 'read' flag |
|
| 456 | - * This can never conflict. |
|
| 457 | - * |
|
| 458 | - * @param string $id |
|
| 459 | - * @param int $flags - read/unread |
|
| 460 | - * @param array $categories |
|
| 461 | - * |
|
| 462 | - * @throws StatusException |
|
| 463 | - * |
|
| 464 | - * @return bool |
|
| 465 | - */ |
|
| 466 | - public function ImportMessageReadFlag($id, $flags, $categories = []) { |
|
| 467 | - list($fsk, $sk) = Utils::SplitMessageId($id); |
|
| 468 | - |
|
| 469 | - // if $fsk is set, we convert it into a backend id. |
|
| 470 | - if ($fsk) { |
|
| 471 | - $fsk = GSync::GetDeviceManager()->GetBackendIdForFolderId($fsk); |
|
| 472 | - } |
|
| 473 | - |
|
| 474 | - // read flag change for our current folder |
|
| 475 | - if ($this->folderidHex == $fsk || empty($fsk)) { |
|
| 476 | - // check if it is in the synchronization interval and/or shared+private |
|
| 477 | - if (!$this->isModificationAllowed($sk)) { |
|
| 478 | - throw new StatusException(sprintf("ImportChangesICS->ImportMessageReadFlag('%s','%d'): Flag update is not allowed. Flags not updated.", $id, $flags), SYNC_STATUS_OBJECTNOTFOUND); |
|
| 479 | - } |
|
| 480 | - |
|
| 481 | - // check for conflicts |
|
| 482 | - /* |
|
| 271 | + /** |
|
| 272 | + * Loads objects which are expected to be exported with the state |
|
| 273 | + * Before importing/saving the actual message from the mobile, a conflict detection should be done. |
|
| 274 | + * |
|
| 275 | + * @param ContentParameters $contentparameters class of objects |
|
| 276 | + * @param string $state |
|
| 277 | + * |
|
| 278 | + * @throws StatusException |
|
| 279 | + * |
|
| 280 | + * @return bool |
|
| 281 | + */ |
|
| 282 | + public function LoadConflicts($contentparameters, $state) { |
|
| 283 | + if (!isset($this->session) || !isset($this->store) || !isset($this->folderid)) { |
|
| 284 | + throw new StatusException("ImportChangesICS->LoadConflicts(): Error, can not load changes for conflict detection. Session, store or folder information not available", SYNC_STATUS_SERVERERROR); |
|
| 285 | + } |
|
| 286 | + |
|
| 287 | + // save data to load changes later if necessary |
|
| 288 | + $this->conflictsLoaded = false; |
|
| 289 | + $this->conflictsContentParameters = $contentparameters; |
|
| 290 | + $this->conflictsState = $state; |
|
| 291 | + |
|
| 292 | + SLog::Write(LOGLEVEL_DEBUG, "ImportChangesICS->LoadConflicts(): will be loaded later if necessary"); |
|
| 293 | + |
|
| 294 | + return true; |
|
| 295 | + } |
|
| 296 | + |
|
| 297 | + /** |
|
| 298 | + * Potential conflicts are only loaded when really necessary, |
|
| 299 | + * e.g. on ADD or MODIFY. |
|
| 300 | + * |
|
| 301 | + * @return bool |
|
| 302 | + */ |
|
| 303 | + private function lazyLoadConflicts() { |
|
| 304 | + if (!isset($this->session) || !isset($this->store) || !isset($this->folderid) || |
|
| 305 | + !isset($this->conflictsContentParameters) || $this->conflictsState === false) { |
|
| 306 | + SLog::Write(LOGLEVEL_WARN, "ImportChangesICS->lazyLoadConflicts(): can not load potential conflicting changes in lazymode for conflict detection. Missing information"); |
|
| 307 | + |
|
| 308 | + return false; |
|
| 309 | + } |
|
| 310 | + |
|
| 311 | + if (!$this->conflictsLoaded) { |
|
| 312 | + SLog::Write(LOGLEVEL_DEBUG, "ImportChangesICS->lazyLoadConflicts(): loading.."); |
|
| 313 | + |
|
| 314 | + // configure an exporter so we can detect conflicts |
|
| 315 | + $exporter = new ExportChangesICS($this->session, $this->store, $this->folderid); |
|
| 316 | + $exporter->Config($this->conflictsState); |
|
| 317 | + $exporter->ConfigContentParameters($this->conflictsContentParameters); |
|
| 318 | + $exporter->InitializeExporter($this->memChanges); |
|
| 319 | + |
|
| 320 | + // monitor how long it takes to export potential conflicts |
|
| 321 | + // if this takes "too long" we cancel this operation! |
|
| 322 | + $potConflicts = $exporter->GetChangeCount(); |
|
| 323 | + if ($potConflicts > 100) { |
|
| 324 | + SLog::Write(LOGLEVEL_WARN, sprintf("ImportChangesICS->lazyLoadConflicts(): conflict detection abandoned as there are too many (%d) changes to be exported.", $potConflicts)); |
|
| 325 | + $this->conflictsLoaded = true; |
|
| 326 | + |
|
| 327 | + return false; |
|
| 328 | + } |
|
| 329 | + $started = time(); |
|
| 330 | + $exported = 0; |
|
| 331 | + |
|
| 332 | + try { |
|
| 333 | + while (is_array($exporter->Synchronize())) { |
|
| 334 | + ++$exported; |
|
| 335 | + |
|
| 336 | + // stop if this takes more than 15 seconds and there are more than 5 changes still to be exported |
|
| 337 | + // within 20 seconds this should be finished or it will not be performed |
|
| 338 | + if ((time() - $started) > 15 && ($potConflicts - $exported) > 5) { |
|
| 339 | + SLog::Write(LOGLEVEL_WARN, sprintf("ImportChangesICS->lazyLoadConflicts(): conflict detection cancelled as operation is too slow. In %d seconds only %d from %d changes were processed.", (time() - $started), $exported, $potConflicts)); |
|
| 340 | + $this->conflictsLoaded = true; |
|
| 341 | + |
|
| 342 | + return false; |
|
| 343 | + } |
|
| 344 | + } |
|
| 345 | + } |
|
| 346 | + // something really bad happened while exporting changes |
|
| 347 | + catch (StatusException $stex) { |
|
| 348 | + SLog::Write(LOGLEVEL_WARN, sprintf("ImportChangesICS->lazyLoadConflicts(): got StatusException code %d while exporting changes. Ignore and mark conflicts as loaded.", $stex->getCode())); |
|
| 349 | + } |
|
| 350 | + $this->conflictsLoaded = true; |
|
| 351 | + } |
|
| 352 | + |
|
| 353 | + return true; |
|
| 354 | + } |
|
| 355 | + |
|
| 356 | + /** |
|
| 357 | + * Imports a single message. |
|
| 358 | + * |
|
| 359 | + * @param string $id |
|
| 360 | + * @param SyncObject $message |
|
| 361 | + * |
|
| 362 | + * @throws StatusException |
|
| 363 | + * |
|
| 364 | + * @return boolean/string - failure / id of message |
|
| 365 | + */ |
|
| 366 | + public function ImportMessageChange($id, $message) { |
|
| 367 | + $flags = 0; |
|
| 368 | + $props = []; |
|
| 369 | + $props[PR_PARENT_SOURCE_KEY] = $this->folderid; |
|
| 370 | + |
|
| 371 | + // set the PR_SOURCE_KEY if available or mark it as new message |
|
| 372 | + if ($id) { |
|
| 373 | + list(, $sk) = Utils::SplitMessageId($id); |
|
| 374 | + $props[PR_SOURCE_KEY] = hex2bin($sk); |
|
| 375 | + |
|
| 376 | + // check if message is in the synchronization interval and/or shared+private |
|
| 377 | + if (!$this->isModificationAllowed($sk)) { |
|
| 378 | + throw new StatusException(sprintf("ImportChangesICS->ImportMessageChange('%s','%s'): Message modification is not allowed. Data not saved.", $id, get_class($message)), SYNC_STATUS_SYNCCANNOTBECOMPLETED); |
|
| 379 | + } |
|
| 380 | + |
|
| 381 | + // check for conflicts |
|
| 382 | + $this->lazyLoadConflicts(); |
|
| 383 | + if ($this->memChanges->IsChanged($id)) { |
|
| 384 | + if ($this->flags & SYNC_CONFLICT_OVERWRITE_PIM) { |
|
| 385 | + // in these cases the status SYNC_STATUS_CONFLICTCLIENTSERVEROBJECT should be returned, so the mobile client can inform the end user |
|
| 386 | + throw new StatusException(sprintf("ImportChangesICS->ImportMessageChange('%s','%s'): Conflict detected. Data from PIM will be dropped! Server overwrites PIM. User is informed.", $id, get_class($message)), SYNC_STATUS_CONFLICTCLIENTSERVEROBJECT, null, LOGLEVEL_INFO); |
|
| 387 | + |
|
| 388 | + return false; |
|
| 389 | + } |
|
| 390 | + |
|
| 391 | + SLog::Write(LOGLEVEL_INFO, sprintf("ImportChangesICS->ImportMessageChange('%s','%s'): Conflict detected. Data from Server will be dropped! PIM overwrites server.", $id, get_class($message))); |
|
| 392 | + } |
|
| 393 | + if ($this->memChanges->IsDeleted($id)) { |
|
| 394 | + SLog::Write(LOGLEVEL_INFO, sprintf("ImportChangesICS->ImportMessageChange('%s','%s'): Conflict detected. Data from PIM will be dropped! Object was deleted on server.", $id, get_class($message))); |
|
| 395 | + |
|
| 396 | + return false; |
|
| 397 | + } |
|
| 398 | + } |
|
| 399 | + else { |
|
| 400 | + $flags = SYNC_NEW_MESSAGE; |
|
| 401 | + } |
|
| 402 | + |
|
| 403 | + if (mapi_importcontentschanges_importmessagechange($this->importer, $props, $flags, $mapimessage)) { |
|
| 404 | + $this->mapiprovider->SetMessage($mapimessage, $message); |
|
| 405 | + mapi_savechanges($mapimessage); |
|
| 406 | + |
|
| 407 | + if (mapi_last_hresult()) { |
|
| 408 | + throw new StatusException(sprintf("ImportChangesICS->ImportMessageChange('%s','%s'): Error, mapi_savechanges() failed: 0x%X", $id, get_class($message), mapi_last_hresult()), SYNC_STATUS_SYNCCANNOTBECOMPLETED); |
|
| 409 | + } |
|
| 410 | + |
|
| 411 | + $sourcekeyprops = mapi_getprops($mapimessage, [PR_SOURCE_KEY]); |
|
| 412 | + |
|
| 413 | + return $this->prefix . bin2hex($sourcekeyprops[PR_SOURCE_KEY]); |
|
| 414 | + } |
|
| 415 | + |
|
| 416 | + throw new StatusException(sprintf("ImportChangesICS->ImportMessageChange('%s','%s'): Error updating object: 0x%X", $id, get_class($message), mapi_last_hresult()), SYNC_STATUS_OBJECTNOTFOUND); |
|
| 417 | + } |
|
| 418 | + |
|
| 419 | + /** |
|
| 420 | + * Imports a deletion. This may conflict if the local object has been modified. |
|
| 421 | + * |
|
| 422 | + * @param string $id |
|
| 423 | + * @param bool $asSoftDelete (opt) if true, the deletion is exported as "SoftDelete", else as "Remove" - default: false |
|
| 424 | + * |
|
| 425 | + * @return bool |
|
| 426 | + */ |
|
| 427 | + public function ImportMessageDeletion($id, $asSoftDelete = false) { |
|
| 428 | + list(, $sk) = Utils::SplitMessageId($id); |
|
| 429 | + |
|
| 430 | + // check if message is in the synchronization interval and/or shared+private |
|
| 431 | + if (!$this->isModificationAllowed($sk)) { |
|
| 432 | + throw new StatusException(sprintf("ImportChangesICS->ImportMessageDeletion('%s'): Message deletion is not allowed. Deletion not executed.", $id), SYNC_STATUS_OBJECTNOTFOUND); |
|
| 433 | + } |
|
| 434 | + |
|
| 435 | + // check for conflicts |
|
| 436 | + $this->lazyLoadConflicts(); |
|
| 437 | + if ($this->memChanges->IsChanged($id)) { |
|
| 438 | + SLog::Write(LOGLEVEL_INFO, sprintf("ImportChangesICS->ImportMessageDeletion('%s'): Conflict detected. Data from Server will be dropped! PIM deleted object.", $id)); |
|
| 439 | + } |
|
| 440 | + elseif ($this->memChanges->IsDeleted($id)) { |
|
| 441 | + SLog::Write(LOGLEVEL_INFO, sprintf("ImportChangesICS->ImportMessageDeletion('%s'): Conflict detected. Data is already deleted. Request will be ignored.", $id)); |
|
| 442 | + |
|
| 443 | + return true; |
|
| 444 | + } |
|
| 445 | + |
|
| 446 | + // do a 'soft' delete so people can un-delete if necessary |
|
| 447 | + if (mapi_importcontentschanges_importmessagedeletion($this->importer, 1, [hex2bin($sk)])) { |
|
| 448 | + throw new StatusException(sprintf("ImportChangesICS->ImportMessageDeletion('%s'): Error updating object: 0x%X", $sk, mapi_last_hresult()), SYNC_STATUS_OBJECTNOTFOUND); |
|
| 449 | + } |
|
| 450 | + |
|
| 451 | + return true; |
|
| 452 | + } |
|
| 453 | + |
|
| 454 | + /** |
|
| 455 | + * Imports a change in 'read' flag |
|
| 456 | + * This can never conflict. |
|
| 457 | + * |
|
| 458 | + * @param string $id |
|
| 459 | + * @param int $flags - read/unread |
|
| 460 | + * @param array $categories |
|
| 461 | + * |
|
| 462 | + * @throws StatusException |
|
| 463 | + * |
|
| 464 | + * @return bool |
|
| 465 | + */ |
|
| 466 | + public function ImportMessageReadFlag($id, $flags, $categories = []) { |
|
| 467 | + list($fsk, $sk) = Utils::SplitMessageId($id); |
|
| 468 | + |
|
| 469 | + // if $fsk is set, we convert it into a backend id. |
|
| 470 | + if ($fsk) { |
|
| 471 | + $fsk = GSync::GetDeviceManager()->GetBackendIdForFolderId($fsk); |
|
| 472 | + } |
|
| 473 | + |
|
| 474 | + // read flag change for our current folder |
|
| 475 | + if ($this->folderidHex == $fsk || empty($fsk)) { |
|
| 476 | + // check if it is in the synchronization interval and/or shared+private |
|
| 477 | + if (!$this->isModificationAllowed($sk)) { |
|
| 478 | + throw new StatusException(sprintf("ImportChangesICS->ImportMessageReadFlag('%s','%d'): Flag update is not allowed. Flags not updated.", $id, $flags), SYNC_STATUS_OBJECTNOTFOUND); |
|
| 479 | + } |
|
| 480 | + |
|
| 481 | + // check for conflicts |
|
| 482 | + /* |
|
| 483 | 483 | * Checking for conflicts is correct at this point, but is a very expensive operation. |
| 484 | 484 | * If the message was deleted, only an error will be shown. |
| 485 | 485 | * |
@@ -490,333 +490,333 @@ discard block |
||
| 490 | 490 | } |
| 491 | 491 | */ |
| 492 | 492 | |
| 493 | - $readstate = ["sourcekey" => hex2bin($sk), "flags" => $flags]; |
|
| 494 | - |
|
| 495 | - if (!mapi_importcontentschanges_importperuserreadstatechange($this->importer, [$readstate])) { |
|
| 496 | - throw new StatusException(sprintf("ImportChangesICS->ImportMessageReadFlag('%s','%d'): Error setting read state: 0x%X", $id, $flags, mapi_last_hresult()), SYNC_STATUS_OBJECTNOTFOUND); |
|
| 497 | - } |
|
| 498 | - } |
|
| 499 | - // yeah OL sucks - ZP-779 |
|
| 500 | - else { |
|
| 501 | - if (!$fsk) { |
|
| 502 | - throw new StatusException(sprintf("ImportChangesICS->ImportMessageReadFlag('%s','%d'): Error setting read state. The message is in another folder but id is unknown as no short folder id is available. Please remove your device states to fully resync your device. Operation ignored.", $id, $flags), SYNC_STATUS_OBJECTNOTFOUND); |
|
| 503 | - } |
|
| 504 | - $store = GSync::GetBackend()->GetMAPIStoreForFolderId(GSync::GetAdditionalSyncFolderStore($fsk), $fsk); |
|
| 505 | - $entryid = mapi_msgstore_entryidfromsourcekey($store, hex2bin($fsk), hex2bin($sk)); |
|
| 506 | - $realMessage = mapi_msgstore_openentry($store, $entryid); |
|
| 507 | - $flag = 0; |
|
| 508 | - if ($flags == 0) { |
|
| 509 | - $flag |= CLEAR_READ_FLAG; |
|
| 510 | - } |
|
| 511 | - $p = mapi_message_setreadflag($realMessage, $flag); |
|
| 512 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->ImportMessageReadFlag('%s','%d'): setting readflag on message: 0x%X", $id, $flags, mapi_last_hresult())); |
|
| 513 | - } |
|
| 514 | - |
|
| 515 | - return true; |
|
| 516 | - } |
|
| 517 | - |
|
| 518 | - /** |
|
| 519 | - * Imports a move of a message. This occurs when a user moves an item to another folder. |
|
| 520 | - * |
|
| 521 | - * Normally, we would implement this via the 'offical' importmessagemove() function on the ICS importer, |
|
| 522 | - * but the grommunio importer does not support this. Therefore we currently implement it via a standard mapi |
|
| 523 | - * call. This causes a mirror 'add/delete' to be sent to the PDA at the next sync. |
|
| 524 | - * Manfred, 2010-10-21. For some mobiles import was causing duplicate messages in the destination folder |
|
| 525 | - * (Mantis #202). Therefore we will create a new message in the destination folder, copy properties |
|
| 526 | - * of the source message to the new one and then delete the source message. |
|
| 527 | - * |
|
| 528 | - * @param string $id |
|
| 529 | - * @param string $newfolder destination folder |
|
| 530 | - * |
|
| 531 | - * @throws StatusException |
|
| 532 | - * |
|
| 533 | - * @return boolean/string |
|
| 534 | - */ |
|
| 535 | - public function ImportMessageMove($id, $newfolder) { |
|
| 536 | - list(, $sk) = Utils::SplitMessageId($id); |
|
| 537 | - if (strtolower($newfolder) == strtolower(bin2hex($this->folderid))) { |
|
| 538 | - throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, source and destination are equal", $id, $newfolder), SYNC_MOVEITEMSSTATUS_SAMESOURCEANDDEST); |
|
| 539 | - } |
|
| 540 | - |
|
| 541 | - // Get the entryid of the message we're moving |
|
| 542 | - $entryid = mapi_msgstore_entryidfromsourcekey($this->store, $this->folderid, hex2bin($sk)); |
|
| 543 | - $srcmessage = false; |
|
| 544 | - |
|
| 545 | - if ($entryid) { |
|
| 546 | - // open the source message |
|
| 547 | - $srcmessage = mapi_msgstore_openentry($this->store, $entryid); |
|
| 548 | - } |
|
| 549 | - |
|
| 550 | - if (!$entryid || !$srcmessage) { |
|
| 551 | - $code = SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID; |
|
| 552 | - $mapiLastHresult = mapi_last_hresult(); |
|
| 553 | - // if we move to the trash and the source message is not found, we can also just tell the mobile that we successfully moved to avoid errors (ZP-624) |
|
| 554 | - if ($newfolder == GSync::GetBackend()->GetWasteBasket()) { |
|
| 555 | - $code = SYNC_MOVEITEMSSTATUS_SUCCESS; |
|
| 556 | - } |
|
| 557 | - $errorCase = !$entryid ? "resolve source message id" : "open source message"; |
|
| 558 | - |
|
| 559 | - throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to %s: 0x%X", $sk, $newfolder, $errorCase, $mapiLastHresult), $code); |
|
| 560 | - } |
|
| 561 | - |
|
| 562 | - // check if it is in the synchronization interval and/or shared+private |
|
| 563 | - if (!$this->isModificationAllowed($sk)) { |
|
| 564 | - throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Source message move is not allowed. Move not performed.", $id, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID); |
|
| 565 | - } |
|
| 566 | - |
|
| 567 | - // get correct mapi store for the destination folder |
|
| 568 | - $dststore = GSync::GetBackend()->GetMAPIStoreForFolderId(GSync::GetAdditionalSyncFolderStore($newfolder), $newfolder); |
|
| 569 | - if ($dststore === false) { |
|
| 570 | - throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to open store of destination folder", $sk, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDDESTID); |
|
| 571 | - } |
|
| 572 | - |
|
| 573 | - $dstentryid = mapi_msgstore_entryidfromsourcekey($dststore, hex2bin($newfolder)); |
|
| 574 | - if (!$dstentryid) { |
|
| 575 | - throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to resolve destination folder", $sk, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDDESTID); |
|
| 576 | - } |
|
| 577 | - |
|
| 578 | - $dstfolder = mapi_msgstore_openentry($dststore, $dstentryid); |
|
| 579 | - if (!$dstfolder) { |
|
| 580 | - throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to open destination folder", $sk, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDDESTID); |
|
| 581 | - } |
|
| 582 | - |
|
| 583 | - $newmessage = mapi_folder_createmessage($dstfolder); |
|
| 584 | - if (!$newmessage) { |
|
| 585 | - throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to create message in destination folder: 0x%X", $sk, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_INVALIDDESTID); |
|
| 586 | - } |
|
| 587 | - |
|
| 588 | - // Copy message |
|
| 589 | - mapi_copyto($srcmessage, [], [], $newmessage); |
|
| 590 | - if (mapi_last_hresult()) { |
|
| 591 | - throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, copy to destination message failed: 0x%X", $sk, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_CANNOTMOVE); |
|
| 592 | - } |
|
| 593 | - |
|
| 594 | - $srcfolderentryid = mapi_msgstore_entryidfromsourcekey($this->store, $this->folderid); |
|
| 595 | - if (!$srcfolderentryid) { |
|
| 596 | - throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to resolve source folder", $sk, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID); |
|
| 597 | - } |
|
| 598 | - |
|
| 599 | - $srcfolder = mapi_msgstore_openentry($this->store, $srcfolderentryid); |
|
| 600 | - if (!$srcfolder) { |
|
| 601 | - throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to open source folder: 0x%X", $sk, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID); |
|
| 602 | - } |
|
| 603 | - |
|
| 604 | - // Save changes |
|
| 605 | - mapi_savechanges($newmessage); |
|
| 606 | - if (mapi_last_hresult()) { |
|
| 607 | - throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, mapi_savechanges() failed: 0x%X", $sk, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_CANNOTMOVE); |
|
| 608 | - } |
|
| 609 | - |
|
| 610 | - // Delete the old message |
|
| 611 | - if (!mapi_folder_deletemessages($srcfolder, [$entryid])) { |
|
| 612 | - throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, delete of source message failed: 0x%X. Possible duplicates.", $sk, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_SOURCEORDESTLOCKED); |
|
| 613 | - } |
|
| 614 | - |
|
| 615 | - $sourcekeyprops = mapi_getprops($newmessage, [PR_SOURCE_KEY]); |
|
| 616 | - if (isset($sourcekeyprops[PR_SOURCE_KEY]) && $sourcekeyprops[PR_SOURCE_KEY]) { |
|
| 617 | - $prefix = ""; |
|
| 618 | - // prepend the destination short folderid, if it exists |
|
| 619 | - $destShortId = GSync::GetDeviceManager()->GetFolderIdForBackendId($newfolder); |
|
| 620 | - if ($destShortId !== $newfolder) { |
|
| 621 | - $prefix = $destShortId . ":"; |
|
| 622 | - } |
|
| 623 | - |
|
| 624 | - return $prefix . bin2hex($sourcekeyprops[PR_SOURCE_KEY]); |
|
| 625 | - } |
|
| 626 | - |
|
| 627 | - return false; |
|
| 628 | - } |
|
| 629 | - |
|
| 630 | - /*---------------------------------------------------------------------------------------------------------- |
|
| 493 | + $readstate = ["sourcekey" => hex2bin($sk), "flags" => $flags]; |
|
| 494 | + |
|
| 495 | + if (!mapi_importcontentschanges_importperuserreadstatechange($this->importer, [$readstate])) { |
|
| 496 | + throw new StatusException(sprintf("ImportChangesICS->ImportMessageReadFlag('%s','%d'): Error setting read state: 0x%X", $id, $flags, mapi_last_hresult()), SYNC_STATUS_OBJECTNOTFOUND); |
|
| 497 | + } |
|
| 498 | + } |
|
| 499 | + // yeah OL sucks - ZP-779 |
|
| 500 | + else { |
|
| 501 | + if (!$fsk) { |
|
| 502 | + throw new StatusException(sprintf("ImportChangesICS->ImportMessageReadFlag('%s','%d'): Error setting read state. The message is in another folder but id is unknown as no short folder id is available. Please remove your device states to fully resync your device. Operation ignored.", $id, $flags), SYNC_STATUS_OBJECTNOTFOUND); |
|
| 503 | + } |
|
| 504 | + $store = GSync::GetBackend()->GetMAPIStoreForFolderId(GSync::GetAdditionalSyncFolderStore($fsk), $fsk); |
|
| 505 | + $entryid = mapi_msgstore_entryidfromsourcekey($store, hex2bin($fsk), hex2bin($sk)); |
|
| 506 | + $realMessage = mapi_msgstore_openentry($store, $entryid); |
|
| 507 | + $flag = 0; |
|
| 508 | + if ($flags == 0) { |
|
| 509 | + $flag |= CLEAR_READ_FLAG; |
|
| 510 | + } |
|
| 511 | + $p = mapi_message_setreadflag($realMessage, $flag); |
|
| 512 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->ImportMessageReadFlag('%s','%d'): setting readflag on message: 0x%X", $id, $flags, mapi_last_hresult())); |
|
| 513 | + } |
|
| 514 | + |
|
| 515 | + return true; |
|
| 516 | + } |
|
| 517 | + |
|
| 518 | + /** |
|
| 519 | + * Imports a move of a message. This occurs when a user moves an item to another folder. |
|
| 520 | + * |
|
| 521 | + * Normally, we would implement this via the 'offical' importmessagemove() function on the ICS importer, |
|
| 522 | + * but the grommunio importer does not support this. Therefore we currently implement it via a standard mapi |
|
| 523 | + * call. This causes a mirror 'add/delete' to be sent to the PDA at the next sync. |
|
| 524 | + * Manfred, 2010-10-21. For some mobiles import was causing duplicate messages in the destination folder |
|
| 525 | + * (Mantis #202). Therefore we will create a new message in the destination folder, copy properties |
|
| 526 | + * of the source message to the new one and then delete the source message. |
|
| 527 | + * |
|
| 528 | + * @param string $id |
|
| 529 | + * @param string $newfolder destination folder |
|
| 530 | + * |
|
| 531 | + * @throws StatusException |
|
| 532 | + * |
|
| 533 | + * @return boolean/string |
|
| 534 | + */ |
|
| 535 | + public function ImportMessageMove($id, $newfolder) { |
|
| 536 | + list(, $sk) = Utils::SplitMessageId($id); |
|
| 537 | + if (strtolower($newfolder) == strtolower(bin2hex($this->folderid))) { |
|
| 538 | + throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, source and destination are equal", $id, $newfolder), SYNC_MOVEITEMSSTATUS_SAMESOURCEANDDEST); |
|
| 539 | + } |
|
| 540 | + |
|
| 541 | + // Get the entryid of the message we're moving |
|
| 542 | + $entryid = mapi_msgstore_entryidfromsourcekey($this->store, $this->folderid, hex2bin($sk)); |
|
| 543 | + $srcmessage = false; |
|
| 544 | + |
|
| 545 | + if ($entryid) { |
|
| 546 | + // open the source message |
|
| 547 | + $srcmessage = mapi_msgstore_openentry($this->store, $entryid); |
|
| 548 | + } |
|
| 549 | + |
|
| 550 | + if (!$entryid || !$srcmessage) { |
|
| 551 | + $code = SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID; |
|
| 552 | + $mapiLastHresult = mapi_last_hresult(); |
|
| 553 | + // if we move to the trash and the source message is not found, we can also just tell the mobile that we successfully moved to avoid errors (ZP-624) |
|
| 554 | + if ($newfolder == GSync::GetBackend()->GetWasteBasket()) { |
|
| 555 | + $code = SYNC_MOVEITEMSSTATUS_SUCCESS; |
|
| 556 | + } |
|
| 557 | + $errorCase = !$entryid ? "resolve source message id" : "open source message"; |
|
| 558 | + |
|
| 559 | + throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to %s: 0x%X", $sk, $newfolder, $errorCase, $mapiLastHresult), $code); |
|
| 560 | + } |
|
| 561 | + |
|
| 562 | + // check if it is in the synchronization interval and/or shared+private |
|
| 563 | + if (!$this->isModificationAllowed($sk)) { |
|
| 564 | + throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Source message move is not allowed. Move not performed.", $id, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID); |
|
| 565 | + } |
|
| 566 | + |
|
| 567 | + // get correct mapi store for the destination folder |
|
| 568 | + $dststore = GSync::GetBackend()->GetMAPIStoreForFolderId(GSync::GetAdditionalSyncFolderStore($newfolder), $newfolder); |
|
| 569 | + if ($dststore === false) { |
|
| 570 | + throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to open store of destination folder", $sk, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDDESTID); |
|
| 571 | + } |
|
| 572 | + |
|
| 573 | + $dstentryid = mapi_msgstore_entryidfromsourcekey($dststore, hex2bin($newfolder)); |
|
| 574 | + if (!$dstentryid) { |
|
| 575 | + throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to resolve destination folder", $sk, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDDESTID); |
|
| 576 | + } |
|
| 577 | + |
|
| 578 | + $dstfolder = mapi_msgstore_openentry($dststore, $dstentryid); |
|
| 579 | + if (!$dstfolder) { |
|
| 580 | + throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to open destination folder", $sk, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDDESTID); |
|
| 581 | + } |
|
| 582 | + |
|
| 583 | + $newmessage = mapi_folder_createmessage($dstfolder); |
|
| 584 | + if (!$newmessage) { |
|
| 585 | + throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to create message in destination folder: 0x%X", $sk, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_INVALIDDESTID); |
|
| 586 | + } |
|
| 587 | + |
|
| 588 | + // Copy message |
|
| 589 | + mapi_copyto($srcmessage, [], [], $newmessage); |
|
| 590 | + if (mapi_last_hresult()) { |
|
| 591 | + throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, copy to destination message failed: 0x%X", $sk, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_CANNOTMOVE); |
|
| 592 | + } |
|
| 593 | + |
|
| 594 | + $srcfolderentryid = mapi_msgstore_entryidfromsourcekey($this->store, $this->folderid); |
|
| 595 | + if (!$srcfolderentryid) { |
|
| 596 | + throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to resolve source folder", $sk, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID); |
|
| 597 | + } |
|
| 598 | + |
|
| 599 | + $srcfolder = mapi_msgstore_openentry($this->store, $srcfolderentryid); |
|
| 600 | + if (!$srcfolder) { |
|
| 601 | + throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to open source folder: 0x%X", $sk, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID); |
|
| 602 | + } |
|
| 603 | + |
|
| 604 | + // Save changes |
|
| 605 | + mapi_savechanges($newmessage); |
|
| 606 | + if (mapi_last_hresult()) { |
|
| 607 | + throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, mapi_savechanges() failed: 0x%X", $sk, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_CANNOTMOVE); |
|
| 608 | + } |
|
| 609 | + |
|
| 610 | + // Delete the old message |
|
| 611 | + if (!mapi_folder_deletemessages($srcfolder, [$entryid])) { |
|
| 612 | + throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, delete of source message failed: 0x%X. Possible duplicates.", $sk, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_SOURCEORDESTLOCKED); |
|
| 613 | + } |
|
| 614 | + |
|
| 615 | + $sourcekeyprops = mapi_getprops($newmessage, [PR_SOURCE_KEY]); |
|
| 616 | + if (isset($sourcekeyprops[PR_SOURCE_KEY]) && $sourcekeyprops[PR_SOURCE_KEY]) { |
|
| 617 | + $prefix = ""; |
|
| 618 | + // prepend the destination short folderid, if it exists |
|
| 619 | + $destShortId = GSync::GetDeviceManager()->GetFolderIdForBackendId($newfolder); |
|
| 620 | + if ($destShortId !== $newfolder) { |
|
| 621 | + $prefix = $destShortId . ":"; |
|
| 622 | + } |
|
| 623 | + |
|
| 624 | + return $prefix . bin2hex($sourcekeyprops[PR_SOURCE_KEY]); |
|
| 625 | + } |
|
| 626 | + |
|
| 627 | + return false; |
|
| 628 | + } |
|
| 629 | + |
|
| 630 | + /*---------------------------------------------------------------------------------------------------------- |
|
| 631 | 631 | * Methods for HierarchyExporter |
| 632 | 632 | */ |
| 633 | 633 | |
| 634 | - /** |
|
| 635 | - * Imports a change on a folder. |
|
| 636 | - * |
|
| 637 | - * @param object $folder SyncFolder |
|
| 638 | - * |
|
| 639 | - * @throws StatusException |
|
| 640 | - * |
|
| 641 | - * @return boolean/SyncFolder false on error or a SyncFolder object with serverid and BackendId set (if available) |
|
| 642 | - */ |
|
| 643 | - public function ImportFolderChange($folder) { |
|
| 644 | - $id = isset($folder->BackendId) ? $folder->BackendId : false; |
|
| 645 | - $parent = $folder->parentid; |
|
| 646 | - $parent_org = $folder->parentid; |
|
| 647 | - $displayname = u2wi($folder->displayname); |
|
| 648 | - $type = $folder->type; |
|
| 649 | - |
|
| 650 | - if (Utils::IsSystemFolder($type)) { |
|
| 651 | - throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, system folder can not be created/modified", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname), SYNC_FSSTATUS_SYSTEMFOLDER); |
|
| 652 | - } |
|
| 653 | - |
|
| 654 | - // create a new folder if $id is not set |
|
| 655 | - if (!$id) { |
|
| 656 | - // the root folder is "0" - get IPM_SUBTREE |
|
| 657 | - if ($parent == "0") { |
|
| 658 | - $parentprops = mapi_getprops($this->store, [PR_IPM_SUBTREE_ENTRYID, PR_IPM_PUBLIC_FOLDERS_ENTRYID]); |
|
| 659 | - if (GSync::GetBackend()->GetImpersonatedUser() == 'system' && isset($parentprops[PR_IPM_PUBLIC_FOLDERS_ENTRYID])) { |
|
| 660 | - $parentfentryid = $parentprops[PR_IPM_PUBLIC_FOLDERS_ENTRYID]; |
|
| 661 | - } |
|
| 662 | - elseif (isset($parentprops[PR_IPM_SUBTREE_ENTRYID])) { |
|
| 663 | - $parentfentryid = $parentprops[PR_IPM_SUBTREE_ENTRYID]; |
|
| 664 | - } |
|
| 665 | - } |
|
| 666 | - else { |
|
| 667 | - $parentfentryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($parent)); |
|
| 668 | - } |
|
| 669 | - |
|
| 670 | - if (!$parentfentryid) { |
|
| 671 | - throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, unable to open parent folder (no entry id)", Utils::PrintAsString(false), $folder->parentid, $displayname), SYNC_FSSTATUS_PARENTNOTFOUND); |
|
| 672 | - } |
|
| 673 | - |
|
| 674 | - $parentfolder = mapi_msgstore_openentry($this->store, $parentfentryid); |
|
| 675 | - if (!$parentfolder) { |
|
| 676 | - throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, unable to open parent folder (open entry)", Utils::PrintAsString(false), $folder->parentid, $displayname), SYNC_FSSTATUS_PARENTNOTFOUND); |
|
| 677 | - } |
|
| 678 | - |
|
| 679 | - // mapi_folder_createfolder() fails if a folder with this name already exists -> MAPI_E_COLLISION |
|
| 680 | - $newfolder = mapi_folder_createfolder($parentfolder, $displayname, ""); |
|
| 681 | - if (mapi_last_hresult()) { |
|
| 682 | - throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, mapi_folder_createfolder() failed: 0x%X", Utils::PrintAsString(false), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_FOLDEREXISTS); |
|
| 683 | - } |
|
| 684 | - |
|
| 685 | - mapi_setprops($newfolder, [PR_CONTAINER_CLASS => MAPIUtils::GetContainerClassFromFolderType($type)]); |
|
| 686 | - |
|
| 687 | - $props = mapi_getprops($newfolder, [PR_SOURCE_KEY]); |
|
| 688 | - if (isset($props[PR_SOURCE_KEY])) { |
|
| 689 | - $folder->BackendId = bin2hex($props[PR_SOURCE_KEY]); |
|
| 690 | - $folderOrigin = DeviceManager::FLD_ORIGIN_USER; |
|
| 691 | - if (GSync::GetBackend()->GetImpersonatedUser()) { |
|
| 692 | - $folderOrigin = DeviceManager::FLD_ORIGIN_IMPERSONATED; |
|
| 693 | - } |
|
| 694 | - $folder->serverid = GSync::GetDeviceManager()->GetFolderIdForBackendId($folder->BackendId, true, $folderOrigin, $folder->displayname); |
|
| 695 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->ImportFolderChange(): Created folder '%s' with id: '%s' backendid: '%s'", $displayname, $folder->serverid, $folder->BackendId)); |
|
| 696 | - |
|
| 697 | - return $folder; |
|
| 698 | - } |
|
| 699 | - |
|
| 700 | - throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, folder created but PR_SOURCE_KEY not available: 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_SERVERERROR); |
|
| 701 | - } |
|
| 702 | - |
|
| 703 | - // open folder for update |
|
| 704 | - $entryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($id)); |
|
| 705 | - if (!$entryid) { |
|
| 706 | - throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, unable to open folder (no entry id): 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_PARENTNOTFOUND); |
|
| 707 | - } |
|
| 708 | - |
|
| 709 | - // check if this is a MAPI default folder |
|
| 710 | - if ($this->mapiprovider->IsMAPIDefaultFolder($entryid)) { |
|
| 711 | - throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, MAPI default folder can not be created/modified", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname), SYNC_FSSTATUS_SYSTEMFOLDER); |
|
| 712 | - } |
|
| 713 | - |
|
| 714 | - $mfolder = mapi_msgstore_openentry($this->store, $entryid); |
|
| 715 | - if (!$mfolder) { |
|
| 716 | - throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, unable to open folder (open entry): 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_PARENTNOTFOUND); |
|
| 717 | - } |
|
| 718 | - |
|
| 719 | - $props = mapi_getprops($mfolder, [PR_SOURCE_KEY, PR_PARENT_SOURCE_KEY, PR_DISPLAY_NAME, PR_CONTAINER_CLASS]); |
|
| 720 | - if (!isset($props[PR_SOURCE_KEY]) || !isset($props[PR_PARENT_SOURCE_KEY]) || !isset($props[PR_DISPLAY_NAME]) || !isset($props[PR_CONTAINER_CLASS])) { |
|
| 721 | - throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, folder data not available: 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_SERVERERROR); |
|
| 722 | - } |
|
| 723 | - |
|
| 724 | - // get the real parent source key from mapi |
|
| 725 | - if ($parent == "0") { |
|
| 726 | - $parentprops = mapi_getprops($this->store, [PR_IPM_SUBTREE_ENTRYID, PR_IPM_PUBLIC_FOLDERS_ENTRYID]); |
|
| 727 | - if (GSync::GetBackend()->GetImpersonatedUser() == 'system') { |
|
| 728 | - $parentfentryid = $parentprops[PR_IPM_PUBLIC_FOLDERS_ENTRYID]; |
|
| 729 | - } |
|
| 730 | - else { |
|
| 731 | - $parentfentryid = $parentprops[PR_IPM_SUBTREE_ENTRYID]; |
|
| 732 | - } |
|
| 733 | - $mapifolder = mapi_msgstore_openentry($this->store, $parentfentryid); |
|
| 734 | - |
|
| 735 | - $rootfolderprops = mapi_getprops($mapifolder, [PR_SOURCE_KEY]); |
|
| 736 | - $parent = bin2hex($rootfolderprops[PR_SOURCE_KEY]); |
|
| 737 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->ImportFolderChange(): resolved AS parent '0' to sourcekey '%s'", $parent)); |
|
| 738 | - } |
|
| 739 | - |
|
| 740 | - // a changed parent id means that the folder should be moved |
|
| 741 | - if (bin2hex($props[PR_PARENT_SOURCE_KEY]) !== $parent) { |
|
| 742 | - $sourceparentfentryid = mapi_msgstore_entryidfromsourcekey($this->store, $props[PR_PARENT_SOURCE_KEY]); |
|
| 743 | - if (!$sourceparentfentryid) { |
|
| 744 | - throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, unable to open parent source folder (no entry id): 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_PARENTNOTFOUND); |
|
| 745 | - } |
|
| 746 | - |
|
| 747 | - $sourceparentfolder = mapi_msgstore_openentry($this->store, $sourceparentfentryid); |
|
| 748 | - if (!$sourceparentfolder) { |
|
| 749 | - throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, unable to open parent source folder (open entry): 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_PARENTNOTFOUND); |
|
| 750 | - } |
|
| 751 | - |
|
| 752 | - $destparentfentryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($parent)); |
|
| 753 | - if (!$sourceparentfentryid) { |
|
| 754 | - throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, unable to open destination folder (no entry id): 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_SERVERERROR); |
|
| 755 | - } |
|
| 756 | - |
|
| 757 | - $destfolder = mapi_msgstore_openentry($this->store, $destparentfentryid); |
|
| 758 | - if (!$destfolder) { |
|
| 759 | - throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, unable to open destination folder (open entry): 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_SERVERERROR); |
|
| 760 | - } |
|
| 761 | - |
|
| 762 | - // mapi_folder_copyfolder() fails if a folder with this name already exists -> MAPI_E_COLLISION |
|
| 763 | - if (!mapi_folder_copyfolder($sourceparentfolder, $entryid, $destfolder, $displayname, FOLDER_MOVE)) { |
|
| 764 | - throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, unable to move folder: 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_FOLDEREXISTS); |
|
| 765 | - } |
|
| 766 | - |
|
| 767 | - // the parent changed, but we got a backendID as parent and have to return an AS folderid - the parent-backendId must be mapped at this point already |
|
| 768 | - if ($folder->parentid != 0) { |
|
| 769 | - $folder->parentid = GSync::GetDeviceManager()->GetFolderIdForBackendId($parent); |
|
| 770 | - } |
|
| 771 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->ImportFolderChange(): Moved folder '%s' with id: %s/%s from: %s to: %s/%s", $displayname, $folder->serverid, $folder->BackendId, bin2hex($props[PR_PARENT_SOURCE_KEY]), $folder->parentid, $parent_org)); |
|
| 772 | - |
|
| 773 | - return $folder; |
|
| 774 | - } |
|
| 775 | - |
|
| 776 | - // update the display name |
|
| 777 | - $props = [PR_DISPLAY_NAME => $displayname]; |
|
| 778 | - mapi_setprops($mfolder, $props); |
|
| 779 | - mapi_savechanges($mfolder); |
|
| 780 | - if (mapi_last_hresult()) { |
|
| 781 | - throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, mapi_savechanges() failed: 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_SERVERERROR); |
|
| 782 | - } |
|
| 783 | - |
|
| 784 | - SLog::Write(LOGLEVEL_DEBUG, "Imported changes for folder: {$id}"); |
|
| 785 | - |
|
| 786 | - return true; |
|
| 787 | - } |
|
| 788 | - |
|
| 789 | - /** |
|
| 790 | - * Imports a folder deletion. |
|
| 791 | - * |
|
| 792 | - * @param SyncFolder $folder at least "serverid" needs to be set |
|
| 793 | - * |
|
| 794 | - * @throws StatusException |
|
| 795 | - * |
|
| 796 | - * @return int SYNC_FOLDERHIERARCHY_STATUS |
|
| 797 | - */ |
|
| 798 | - public function ImportFolderDeletion($folder) { |
|
| 799 | - $id = $folder->BackendId; |
|
| 800 | - $parent = isset($folder->parentid) ? $folder->parentid : false; |
|
| 801 | - SLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->ImportFolderDeletion('%s','%s'): importing folder deletetion", $id, $parent)); |
|
| 802 | - |
|
| 803 | - $folderentryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($id)); |
|
| 804 | - if (!$folderentryid) { |
|
| 805 | - throw new StatusException(sprintf("ImportChangesICS->ImportFolderDeletion('%s','%s'): Error, unable to resolve folder", $id, $parent, mapi_last_hresult()), SYNC_FSSTATUS_FOLDERDOESNOTEXIST); |
|
| 806 | - } |
|
| 807 | - |
|
| 808 | - // get the folder type from the MAPIProvider |
|
| 809 | - $type = $this->mapiprovider->GetFolderType($folderentryid); |
|
| 810 | - |
|
| 811 | - if (Utils::IsSystemFolder($type) || $this->mapiprovider->IsMAPIDefaultFolder($folderentryid)) { |
|
| 812 | - throw new StatusException(sprintf("ImportChangesICS->ImportFolderDeletion('%s','%s'): Error deleting system/default folder", $id, $parent), SYNC_FSSTATUS_SYSTEMFOLDER); |
|
| 813 | - } |
|
| 814 | - |
|
| 815 | - $ret = mapi_importhierarchychanges_importfolderdeletion($this->importer, 0, [PR_SOURCE_KEY => hex2bin($id)]); |
|
| 816 | - if (!$ret) { |
|
| 817 | - throw new StatusException(sprintf("ImportChangesICS->ImportFolderDeletion('%s','%s'): Error deleting folder: 0x%X", $id, $parent, mapi_last_hresult()), SYNC_FSSTATUS_SERVERERROR); |
|
| 818 | - } |
|
| 819 | - |
|
| 820 | - return $ret; |
|
| 821 | - } |
|
| 634 | + /** |
|
| 635 | + * Imports a change on a folder. |
|
| 636 | + * |
|
| 637 | + * @param object $folder SyncFolder |
|
| 638 | + * |
|
| 639 | + * @throws StatusException |
|
| 640 | + * |
|
| 641 | + * @return boolean/SyncFolder false on error or a SyncFolder object with serverid and BackendId set (if available) |
|
| 642 | + */ |
|
| 643 | + public function ImportFolderChange($folder) { |
|
| 644 | + $id = isset($folder->BackendId) ? $folder->BackendId : false; |
|
| 645 | + $parent = $folder->parentid; |
|
| 646 | + $parent_org = $folder->parentid; |
|
| 647 | + $displayname = u2wi($folder->displayname); |
|
| 648 | + $type = $folder->type; |
|
| 649 | + |
|
| 650 | + if (Utils::IsSystemFolder($type)) { |
|
| 651 | + throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, system folder can not be created/modified", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname), SYNC_FSSTATUS_SYSTEMFOLDER); |
|
| 652 | + } |
|
| 653 | + |
|
| 654 | + // create a new folder if $id is not set |
|
| 655 | + if (!$id) { |
|
| 656 | + // the root folder is "0" - get IPM_SUBTREE |
|
| 657 | + if ($parent == "0") { |
|
| 658 | + $parentprops = mapi_getprops($this->store, [PR_IPM_SUBTREE_ENTRYID, PR_IPM_PUBLIC_FOLDERS_ENTRYID]); |
|
| 659 | + if (GSync::GetBackend()->GetImpersonatedUser() == 'system' && isset($parentprops[PR_IPM_PUBLIC_FOLDERS_ENTRYID])) { |
|
| 660 | + $parentfentryid = $parentprops[PR_IPM_PUBLIC_FOLDERS_ENTRYID]; |
|
| 661 | + } |
|
| 662 | + elseif (isset($parentprops[PR_IPM_SUBTREE_ENTRYID])) { |
|
| 663 | + $parentfentryid = $parentprops[PR_IPM_SUBTREE_ENTRYID]; |
|
| 664 | + } |
|
| 665 | + } |
|
| 666 | + else { |
|
| 667 | + $parentfentryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($parent)); |
|
| 668 | + } |
|
| 669 | + |
|
| 670 | + if (!$parentfentryid) { |
|
| 671 | + throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, unable to open parent folder (no entry id)", Utils::PrintAsString(false), $folder->parentid, $displayname), SYNC_FSSTATUS_PARENTNOTFOUND); |
|
| 672 | + } |
|
| 673 | + |
|
| 674 | + $parentfolder = mapi_msgstore_openentry($this->store, $parentfentryid); |
|
| 675 | + if (!$parentfolder) { |
|
| 676 | + throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, unable to open parent folder (open entry)", Utils::PrintAsString(false), $folder->parentid, $displayname), SYNC_FSSTATUS_PARENTNOTFOUND); |
|
| 677 | + } |
|
| 678 | + |
|
| 679 | + // mapi_folder_createfolder() fails if a folder with this name already exists -> MAPI_E_COLLISION |
|
| 680 | + $newfolder = mapi_folder_createfolder($parentfolder, $displayname, ""); |
|
| 681 | + if (mapi_last_hresult()) { |
|
| 682 | + throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, mapi_folder_createfolder() failed: 0x%X", Utils::PrintAsString(false), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_FOLDEREXISTS); |
|
| 683 | + } |
|
| 684 | + |
|
| 685 | + mapi_setprops($newfolder, [PR_CONTAINER_CLASS => MAPIUtils::GetContainerClassFromFolderType($type)]); |
|
| 686 | + |
|
| 687 | + $props = mapi_getprops($newfolder, [PR_SOURCE_KEY]); |
|
| 688 | + if (isset($props[PR_SOURCE_KEY])) { |
|
| 689 | + $folder->BackendId = bin2hex($props[PR_SOURCE_KEY]); |
|
| 690 | + $folderOrigin = DeviceManager::FLD_ORIGIN_USER; |
|
| 691 | + if (GSync::GetBackend()->GetImpersonatedUser()) { |
|
| 692 | + $folderOrigin = DeviceManager::FLD_ORIGIN_IMPERSONATED; |
|
| 693 | + } |
|
| 694 | + $folder->serverid = GSync::GetDeviceManager()->GetFolderIdForBackendId($folder->BackendId, true, $folderOrigin, $folder->displayname); |
|
| 695 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->ImportFolderChange(): Created folder '%s' with id: '%s' backendid: '%s'", $displayname, $folder->serverid, $folder->BackendId)); |
|
| 696 | + |
|
| 697 | + return $folder; |
|
| 698 | + } |
|
| 699 | + |
|
| 700 | + throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, folder created but PR_SOURCE_KEY not available: 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_SERVERERROR); |
|
| 701 | + } |
|
| 702 | + |
|
| 703 | + // open folder for update |
|
| 704 | + $entryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($id)); |
|
| 705 | + if (!$entryid) { |
|
| 706 | + throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, unable to open folder (no entry id): 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_PARENTNOTFOUND); |
|
| 707 | + } |
|
| 708 | + |
|
| 709 | + // check if this is a MAPI default folder |
|
| 710 | + if ($this->mapiprovider->IsMAPIDefaultFolder($entryid)) { |
|
| 711 | + throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, MAPI default folder can not be created/modified", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname), SYNC_FSSTATUS_SYSTEMFOLDER); |
|
| 712 | + } |
|
| 713 | + |
|
| 714 | + $mfolder = mapi_msgstore_openentry($this->store, $entryid); |
|
| 715 | + if (!$mfolder) { |
|
| 716 | + throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, unable to open folder (open entry): 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_PARENTNOTFOUND); |
|
| 717 | + } |
|
| 718 | + |
|
| 719 | + $props = mapi_getprops($mfolder, [PR_SOURCE_KEY, PR_PARENT_SOURCE_KEY, PR_DISPLAY_NAME, PR_CONTAINER_CLASS]); |
|
| 720 | + if (!isset($props[PR_SOURCE_KEY]) || !isset($props[PR_PARENT_SOURCE_KEY]) || !isset($props[PR_DISPLAY_NAME]) || !isset($props[PR_CONTAINER_CLASS])) { |
|
| 721 | + throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, folder data not available: 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_SERVERERROR); |
|
| 722 | + } |
|
| 723 | + |
|
| 724 | + // get the real parent source key from mapi |
|
| 725 | + if ($parent == "0") { |
|
| 726 | + $parentprops = mapi_getprops($this->store, [PR_IPM_SUBTREE_ENTRYID, PR_IPM_PUBLIC_FOLDERS_ENTRYID]); |
|
| 727 | + if (GSync::GetBackend()->GetImpersonatedUser() == 'system') { |
|
| 728 | + $parentfentryid = $parentprops[PR_IPM_PUBLIC_FOLDERS_ENTRYID]; |
|
| 729 | + } |
|
| 730 | + else { |
|
| 731 | + $parentfentryid = $parentprops[PR_IPM_SUBTREE_ENTRYID]; |
|
| 732 | + } |
|
| 733 | + $mapifolder = mapi_msgstore_openentry($this->store, $parentfentryid); |
|
| 734 | + |
|
| 735 | + $rootfolderprops = mapi_getprops($mapifolder, [PR_SOURCE_KEY]); |
|
| 736 | + $parent = bin2hex($rootfolderprops[PR_SOURCE_KEY]); |
|
| 737 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->ImportFolderChange(): resolved AS parent '0' to sourcekey '%s'", $parent)); |
|
| 738 | + } |
|
| 739 | + |
|
| 740 | + // a changed parent id means that the folder should be moved |
|
| 741 | + if (bin2hex($props[PR_PARENT_SOURCE_KEY]) !== $parent) { |
|
| 742 | + $sourceparentfentryid = mapi_msgstore_entryidfromsourcekey($this->store, $props[PR_PARENT_SOURCE_KEY]); |
|
| 743 | + if (!$sourceparentfentryid) { |
|
| 744 | + throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, unable to open parent source folder (no entry id): 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_PARENTNOTFOUND); |
|
| 745 | + } |
|
| 746 | + |
|
| 747 | + $sourceparentfolder = mapi_msgstore_openentry($this->store, $sourceparentfentryid); |
|
| 748 | + if (!$sourceparentfolder) { |
|
| 749 | + throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, unable to open parent source folder (open entry): 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_PARENTNOTFOUND); |
|
| 750 | + } |
|
| 751 | + |
|
| 752 | + $destparentfentryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($parent)); |
|
| 753 | + if (!$sourceparentfentryid) { |
|
| 754 | + throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, unable to open destination folder (no entry id): 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_SERVERERROR); |
|
| 755 | + } |
|
| 756 | + |
|
| 757 | + $destfolder = mapi_msgstore_openentry($this->store, $destparentfentryid); |
|
| 758 | + if (!$destfolder) { |
|
| 759 | + throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, unable to open destination folder (open entry): 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_SERVERERROR); |
|
| 760 | + } |
|
| 761 | + |
|
| 762 | + // mapi_folder_copyfolder() fails if a folder with this name already exists -> MAPI_E_COLLISION |
|
| 763 | + if (!mapi_folder_copyfolder($sourceparentfolder, $entryid, $destfolder, $displayname, FOLDER_MOVE)) { |
|
| 764 | + throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, unable to move folder: 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_FOLDEREXISTS); |
|
| 765 | + } |
|
| 766 | + |
|
| 767 | + // the parent changed, but we got a backendID as parent and have to return an AS folderid - the parent-backendId must be mapped at this point already |
|
| 768 | + if ($folder->parentid != 0) { |
|
| 769 | + $folder->parentid = GSync::GetDeviceManager()->GetFolderIdForBackendId($parent); |
|
| 770 | + } |
|
| 771 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->ImportFolderChange(): Moved folder '%s' with id: %s/%s from: %s to: %s/%s", $displayname, $folder->serverid, $folder->BackendId, bin2hex($props[PR_PARENT_SOURCE_KEY]), $folder->parentid, $parent_org)); |
|
| 772 | + |
|
| 773 | + return $folder; |
|
| 774 | + } |
|
| 775 | + |
|
| 776 | + // update the display name |
|
| 777 | + $props = [PR_DISPLAY_NAME => $displayname]; |
|
| 778 | + mapi_setprops($mfolder, $props); |
|
| 779 | + mapi_savechanges($mfolder); |
|
| 780 | + if (mapi_last_hresult()) { |
|
| 781 | + throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, mapi_savechanges() failed: 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_SERVERERROR); |
|
| 782 | + } |
|
| 783 | + |
|
| 784 | + SLog::Write(LOGLEVEL_DEBUG, "Imported changes for folder: {$id}"); |
|
| 785 | + |
|
| 786 | + return true; |
|
| 787 | + } |
|
| 788 | + |
|
| 789 | + /** |
|
| 790 | + * Imports a folder deletion. |
|
| 791 | + * |
|
| 792 | + * @param SyncFolder $folder at least "serverid" needs to be set |
|
| 793 | + * |
|
| 794 | + * @throws StatusException |
|
| 795 | + * |
|
| 796 | + * @return int SYNC_FOLDERHIERARCHY_STATUS |
|
| 797 | + */ |
|
| 798 | + public function ImportFolderDeletion($folder) { |
|
| 799 | + $id = $folder->BackendId; |
|
| 800 | + $parent = isset($folder->parentid) ? $folder->parentid : false; |
|
| 801 | + SLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->ImportFolderDeletion('%s','%s'): importing folder deletetion", $id, $parent)); |
|
| 802 | + |
|
| 803 | + $folderentryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($id)); |
|
| 804 | + if (!$folderentryid) { |
|
| 805 | + throw new StatusException(sprintf("ImportChangesICS->ImportFolderDeletion('%s','%s'): Error, unable to resolve folder", $id, $parent, mapi_last_hresult()), SYNC_FSSTATUS_FOLDERDOESNOTEXIST); |
|
| 806 | + } |
|
| 807 | + |
|
| 808 | + // get the folder type from the MAPIProvider |
|
| 809 | + $type = $this->mapiprovider->GetFolderType($folderentryid); |
|
| 810 | + |
|
| 811 | + if (Utils::IsSystemFolder($type) || $this->mapiprovider->IsMAPIDefaultFolder($folderentryid)) { |
|
| 812 | + throw new StatusException(sprintf("ImportChangesICS->ImportFolderDeletion('%s','%s'): Error deleting system/default folder", $id, $parent), SYNC_FSSTATUS_SYSTEMFOLDER); |
|
| 813 | + } |
|
| 814 | + |
|
| 815 | + $ret = mapi_importhierarchychanges_importfolderdeletion($this->importer, 0, [PR_SOURCE_KEY => hex2bin($id)]); |
|
| 816 | + if (!$ret) { |
|
| 817 | + throw new StatusException(sprintf("ImportChangesICS->ImportFolderDeletion('%s','%s'): Error deleting folder: 0x%X", $id, $parent, mapi_last_hresult()), SYNC_FSSTATUS_SERVERERROR); |
|
| 818 | + } |
|
| 819 | + |
|
| 820 | + return $ret; |
|
| 821 | + } |
|
| 822 | 822 | } |
@@ -58,7 +58,7 @@ discard block |
||
| 58 | 58 | $folderidForBackendId = GSync::GetDeviceManager()->GetFolderIdForBackendId($this->folderidHex); |
| 59 | 59 | // Only append backend id if the mapping backendid<->folderid is available. |
| 60 | 60 | if ($folderidForBackendId != $this->folderidHex) { |
| 61 | - $this->prefix = $folderidForBackendId . ':'; |
|
| 61 | + $this->prefix = $folderidForBackendId.':'; |
|
| 62 | 62 | } |
| 63 | 63 | } |
| 64 | 64 | else { |
@@ -410,7 +410,7 @@ discard block |
||
| 410 | 410 | |
| 411 | 411 | $sourcekeyprops = mapi_getprops($mapimessage, [PR_SOURCE_KEY]); |
| 412 | 412 | |
| 413 | - return $this->prefix . bin2hex($sourcekeyprops[PR_SOURCE_KEY]); |
|
| 413 | + return $this->prefix.bin2hex($sourcekeyprops[PR_SOURCE_KEY]); |
|
| 414 | 414 | } |
| 415 | 415 | |
| 416 | 416 | throw new StatusException(sprintf("ImportChangesICS->ImportMessageChange('%s','%s'): Error updating object: 0x%X", $id, get_class($message), mapi_last_hresult()), SYNC_STATUS_OBJECTNOTFOUND); |
@@ -618,10 +618,10 @@ discard block |
||
| 618 | 618 | // prepend the destination short folderid, if it exists |
| 619 | 619 | $destShortId = GSync::GetDeviceManager()->GetFolderIdForBackendId($newfolder); |
| 620 | 620 | if ($destShortId !== $newfolder) { |
| 621 | - $prefix = $destShortId . ":"; |
|
| 621 | + $prefix = $destShortId.":"; |
|
| 622 | 622 | } |
| 623 | 623 | |
| 624 | - return $prefix . bin2hex($sourcekeyprops[PR_SOURCE_KEY]); |
|
| 624 | + return $prefix.bin2hex($sourcekeyprops[PR_SOURCE_KEY]); |
|
| 625 | 625 | } |
| 626 | 626 | |
| 627 | 627 | return false; |
@@ -60,13 +60,11 @@ discard block |
||
| 60 | 60 | if ($folderidForBackendId != $this->folderidHex) { |
| 61 | 61 | $this->prefix = $folderidForBackendId . ':'; |
| 62 | 62 | } |
| 63 | - } |
|
| 64 | - else { |
|
| 63 | + } else { |
|
| 65 | 64 | $storeprops = mapi_getprops($store, [PR_IPM_SUBTREE_ENTRYID, PR_IPM_PUBLIC_FOLDERS_ENTRYID]); |
| 66 | 65 | if (GSync::GetBackend()->GetImpersonatedUser() == 'system') { |
| 67 | 66 | $entryid = $storeprops[PR_IPM_PUBLIC_FOLDERS_ENTRYID]; |
| 68 | - } |
|
| 69 | - else { |
|
| 67 | + } else { |
|
| 70 | 68 | $entryid = $storeprops[PR_IPM_SUBTREE_ENTRYID]; |
| 71 | 69 | } |
| 72 | 70 | } |
@@ -88,8 +86,7 @@ discard block |
||
| 88 | 86 | |
| 89 | 87 | if ($folderid) { |
| 90 | 88 | $this->importer = mapi_openproperty($folder, PR_COLLECTOR, IID_IExchangeImportContentsChanges, 0, 0); |
| 91 | - } |
|
| 92 | - else { |
|
| 89 | + } else { |
|
| 93 | 90 | $this->importer = mapi_openproperty($folder, PR_COLLECTOR, IID_IExchangeImportHierarchyChanges, 0, 0); |
| 94 | 91 | } |
| 95 | 92 | } |
@@ -127,8 +124,7 @@ discard block |
||
| 127 | 124 | // possible conflicting messages will be cached here |
| 128 | 125 | $this->memChanges = new ChangesMemoryWrapper(); |
| 129 | 126 | $stat = mapi_importcontentschanges_config($this->importer, $stream, $flags); |
| 130 | - } |
|
| 131 | - else { |
|
| 127 | + } else { |
|
| 132 | 128 | $stat = mapi_importhierarchychanges_config($this->importer, $stream, $flags); |
| 133 | 129 | } |
| 134 | 130 | |
@@ -201,8 +197,7 @@ discard block |
||
| 201 | 197 | $data = mapi_stream_read($this->statestream, 4096); |
| 202 | 198 | if (strlen($data)) { |
| 203 | 199 | $state .= $data; |
| 204 | - } |
|
| 205 | - else { |
|
| 200 | + } else { |
|
| 206 | 201 | break; |
| 207 | 202 | } |
| 208 | 203 | } |
@@ -395,8 +390,7 @@ discard block |
||
| 395 | 390 | |
| 396 | 391 | return false; |
| 397 | 392 | } |
| 398 | - } |
|
| 399 | - else { |
|
| 393 | + } else { |
|
| 400 | 394 | $flags = SYNC_NEW_MESSAGE; |
| 401 | 395 | } |
| 402 | 396 | |
@@ -436,8 +430,7 @@ discard block |
||
| 436 | 430 | $this->lazyLoadConflicts(); |
| 437 | 431 | if ($this->memChanges->IsChanged($id)) { |
| 438 | 432 | SLog::Write(LOGLEVEL_INFO, sprintf("ImportChangesICS->ImportMessageDeletion('%s'): Conflict detected. Data from Server will be dropped! PIM deleted object.", $id)); |
| 439 | - } |
|
| 440 | - elseif ($this->memChanges->IsDeleted($id)) { |
|
| 433 | + } elseif ($this->memChanges->IsDeleted($id)) { |
|
| 441 | 434 | SLog::Write(LOGLEVEL_INFO, sprintf("ImportChangesICS->ImportMessageDeletion('%s'): Conflict detected. Data is already deleted. Request will be ignored.", $id)); |
| 442 | 435 | |
| 443 | 436 | return true; |
@@ -658,12 +651,10 @@ discard block |
||
| 658 | 651 | $parentprops = mapi_getprops($this->store, [PR_IPM_SUBTREE_ENTRYID, PR_IPM_PUBLIC_FOLDERS_ENTRYID]); |
| 659 | 652 | if (GSync::GetBackend()->GetImpersonatedUser() == 'system' && isset($parentprops[PR_IPM_PUBLIC_FOLDERS_ENTRYID])) { |
| 660 | 653 | $parentfentryid = $parentprops[PR_IPM_PUBLIC_FOLDERS_ENTRYID]; |
| 661 | - } |
|
| 662 | - elseif (isset($parentprops[PR_IPM_SUBTREE_ENTRYID])) { |
|
| 654 | + } elseif (isset($parentprops[PR_IPM_SUBTREE_ENTRYID])) { |
|
| 663 | 655 | $parentfentryid = $parentprops[PR_IPM_SUBTREE_ENTRYID]; |
| 664 | 656 | } |
| 665 | - } |
|
| 666 | - else { |
|
| 657 | + } else { |
|
| 667 | 658 | $parentfentryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($parent)); |
| 668 | 659 | } |
| 669 | 660 | |
@@ -726,8 +717,7 @@ discard block |
||
| 726 | 717 | $parentprops = mapi_getprops($this->store, [PR_IPM_SUBTREE_ENTRYID, PR_IPM_PUBLIC_FOLDERS_ENTRYID]); |
| 727 | 718 | if (GSync::GetBackend()->GetImpersonatedUser() == 'system') { |
| 728 | 719 | $parentfentryid = $parentprops[PR_IPM_PUBLIC_FOLDERS_ENTRYID]; |
| 729 | - } |
|
| 730 | - else { |
|
| 720 | + } else { |
|
| 731 | 721 | $parentfentryid = $parentprops[PR_IPM_SUBTREE_ENTRYID]; |
| 732 | 722 | } |
| 733 | 723 | $mapifolder = mapi_msgstore_openentry($this->store, $parentfentryid); |
@@ -11,42 +11,42 @@ |
||
| 11 | 11 | */ |
| 12 | 12 | |
| 13 | 13 | interface IChanges { |
| 14 | - /** |
|
| 15 | - * Constructor. |
|
| 16 | - * |
|
| 17 | - * @param mixed $state |
|
| 18 | - * @param mixed $flags |
|
| 19 | - * |
|
| 20 | - * @throws StatusException |
|
| 21 | - */ |
|
| 14 | + /** |
|
| 15 | + * Constructor. |
|
| 16 | + * |
|
| 17 | + * @param mixed $state |
|
| 18 | + * @param mixed $flags |
|
| 19 | + * |
|
| 20 | + * @throws StatusException |
|
| 21 | + */ |
|
| 22 | 22 | |
| 23 | - /** |
|
| 24 | - * Initializes the state and flags. |
|
| 25 | - * |
|
| 26 | - * @param string $state |
|
| 27 | - * @param int $flags |
|
| 28 | - * |
|
| 29 | - * @throws StatusException |
|
| 30 | - * |
|
| 31 | - * @return bool status flag |
|
| 32 | - */ |
|
| 33 | - public function Config($state, $flags = 0); |
|
| 23 | + /** |
|
| 24 | + * Initializes the state and flags. |
|
| 25 | + * |
|
| 26 | + * @param string $state |
|
| 27 | + * @param int $flags |
|
| 28 | + * |
|
| 29 | + * @throws StatusException |
|
| 30 | + * |
|
| 31 | + * @return bool status flag |
|
| 32 | + */ |
|
| 33 | + public function Config($state, $flags = 0); |
|
| 34 | 34 | |
| 35 | - /** |
|
| 36 | - * Configures additional parameters used for content synchronization. |
|
| 37 | - * |
|
| 38 | - * @param ContentParameters $contentparameters |
|
| 39 | - * |
|
| 40 | - * @throws StatusException |
|
| 41 | - * |
|
| 42 | - * @return bool |
|
| 43 | - */ |
|
| 44 | - public function ConfigContentParameters($contentparameters); |
|
| 35 | + /** |
|
| 36 | + * Configures additional parameters used for content synchronization. |
|
| 37 | + * |
|
| 38 | + * @param ContentParameters $contentparameters |
|
| 39 | + * |
|
| 40 | + * @throws StatusException |
|
| 41 | + * |
|
| 42 | + * @return bool |
|
| 43 | + */ |
|
| 44 | + public function ConfigContentParameters($contentparameters); |
|
| 45 | 45 | |
| 46 | - /** |
|
| 47 | - * Reads and returns the current state. |
|
| 48 | - * |
|
| 49 | - * @return string |
|
| 50 | - */ |
|
| 51 | - public function GetState(); |
|
| 46 | + /** |
|
| 47 | + * Reads and returns the current state. |
|
| 48 | + * |
|
| 49 | + * @return string |
|
| 50 | + */ |
|
| 51 | + public function GetState(); |
|
| 52 | 52 | } |
@@ -10,94 +10,94 @@ |
||
| 10 | 10 | */ |
| 11 | 11 | |
| 12 | 12 | interface IImportChanges extends IChanges { |
| 13 | - /*---------------------------------------------------------------------------------------------------------- |
|
| 13 | + /*---------------------------------------------------------------------------------------------------------- |
|
| 14 | 14 | * Methods for to import contents |
| 15 | 15 | */ |
| 16 | 16 | |
| 17 | - /** |
|
| 18 | - * Loads objects which are expected to be exported with the state |
|
| 19 | - * Before importing/saving the actual message from the mobile, a conflict detection should be done. |
|
| 20 | - * |
|
| 21 | - * @param ContentParameters $contentparameters |
|
| 22 | - * @param string $state |
|
| 23 | - * |
|
| 24 | - * @throws StatusException |
|
| 25 | - * |
|
| 26 | - * @return bool |
|
| 27 | - */ |
|
| 28 | - public function LoadConflicts($contentparameters, $state); |
|
| 17 | + /** |
|
| 18 | + * Loads objects which are expected to be exported with the state |
|
| 19 | + * Before importing/saving the actual message from the mobile, a conflict detection should be done. |
|
| 20 | + * |
|
| 21 | + * @param ContentParameters $contentparameters |
|
| 22 | + * @param string $state |
|
| 23 | + * |
|
| 24 | + * @throws StatusException |
|
| 25 | + * |
|
| 26 | + * @return bool |
|
| 27 | + */ |
|
| 28 | + public function LoadConflicts($contentparameters, $state); |
|
| 29 | 29 | |
| 30 | - /** |
|
| 31 | - * Imports a single message. |
|
| 32 | - * |
|
| 33 | - * @param string $id |
|
| 34 | - * @param SyncObject $message |
|
| 35 | - * |
|
| 36 | - * @throws StatusException |
|
| 37 | - * |
|
| 38 | - * @return boolean/string failure / id of message |
|
| 39 | - */ |
|
| 40 | - public function ImportMessageChange($id, $message); |
|
| 30 | + /** |
|
| 31 | + * Imports a single message. |
|
| 32 | + * |
|
| 33 | + * @param string $id |
|
| 34 | + * @param SyncObject $message |
|
| 35 | + * |
|
| 36 | + * @throws StatusException |
|
| 37 | + * |
|
| 38 | + * @return boolean/string failure / id of message |
|
| 39 | + */ |
|
| 40 | + public function ImportMessageChange($id, $message); |
|
| 41 | 41 | |
| 42 | - /** |
|
| 43 | - * Imports a deletion. This may conflict if the local object has been modified. |
|
| 44 | - * |
|
| 45 | - * @param string $id |
|
| 46 | - * @param bool $asSoftDelete (opt) if true, the deletion is exported as "SoftDelete", else as "Remove" - default: false |
|
| 47 | - * |
|
| 48 | - * @return bool |
|
| 49 | - */ |
|
| 50 | - public function ImportMessageDeletion($id, $asSoftDelete = false); |
|
| 42 | + /** |
|
| 43 | + * Imports a deletion. This may conflict if the local object has been modified. |
|
| 44 | + * |
|
| 45 | + * @param string $id |
|
| 46 | + * @param bool $asSoftDelete (opt) if true, the deletion is exported as "SoftDelete", else as "Remove" - default: false |
|
| 47 | + * |
|
| 48 | + * @return bool |
|
| 49 | + */ |
|
| 50 | + public function ImportMessageDeletion($id, $asSoftDelete = false); |
|
| 51 | 51 | |
| 52 | - /** |
|
| 53 | - * Imports a change in 'read' flag |
|
| 54 | - * This can never conflict. |
|
| 55 | - * |
|
| 56 | - * @param string $id |
|
| 57 | - * @param int $flags |
|
| 58 | - * @param array $categories |
|
| 59 | - * |
|
| 60 | - * @throws StatusException |
|
| 61 | - * |
|
| 62 | - * @return bool |
|
| 63 | - */ |
|
| 64 | - public function ImportMessageReadFlag($id, $flags, $categories = []); |
|
| 52 | + /** |
|
| 53 | + * Imports a change in 'read' flag |
|
| 54 | + * This can never conflict. |
|
| 55 | + * |
|
| 56 | + * @param string $id |
|
| 57 | + * @param int $flags |
|
| 58 | + * @param array $categories |
|
| 59 | + * |
|
| 60 | + * @throws StatusException |
|
| 61 | + * |
|
| 62 | + * @return bool |
|
| 63 | + */ |
|
| 64 | + public function ImportMessageReadFlag($id, $flags, $categories = []); |
|
| 65 | 65 | |
| 66 | - /** |
|
| 67 | - * Imports a move of a message. This occurs when a user moves an item to another folder. |
|
| 68 | - * |
|
| 69 | - * @param string $id |
|
| 70 | - * @param string $newfolder destination folder |
|
| 71 | - * |
|
| 72 | - * @throws StatusException |
|
| 73 | - * |
|
| 74 | - * @return bool |
|
| 75 | - */ |
|
| 76 | - public function ImportMessageMove($id, $newfolder); |
|
| 66 | + /** |
|
| 67 | + * Imports a move of a message. This occurs when a user moves an item to another folder. |
|
| 68 | + * |
|
| 69 | + * @param string $id |
|
| 70 | + * @param string $newfolder destination folder |
|
| 71 | + * |
|
| 72 | + * @throws StatusException |
|
| 73 | + * |
|
| 74 | + * @return bool |
|
| 75 | + */ |
|
| 76 | + public function ImportMessageMove($id, $newfolder); |
|
| 77 | 77 | |
| 78 | - /*---------------------------------------------------------------------------------------------------------- |
|
| 78 | + /*---------------------------------------------------------------------------------------------------------- |
|
| 79 | 79 | * Methods to import hierarchy |
| 80 | 80 | */ |
| 81 | 81 | |
| 82 | - /** |
|
| 83 | - * Imports a change on a folder. |
|
| 84 | - * |
|
| 85 | - * @param object $folder SyncFolder |
|
| 86 | - * |
|
| 87 | - * @throws StatusException |
|
| 88 | - * |
|
| 89 | - * @return boolean/SyncObject status/object with the ath least the serverid of the folder set |
|
| 90 | - */ |
|
| 91 | - public function ImportFolderChange($folder); |
|
| 82 | + /** |
|
| 83 | + * Imports a change on a folder. |
|
| 84 | + * |
|
| 85 | + * @param object $folder SyncFolder |
|
| 86 | + * |
|
| 87 | + * @throws StatusException |
|
| 88 | + * |
|
| 89 | + * @return boolean/SyncObject status/object with the ath least the serverid of the folder set |
|
| 90 | + */ |
|
| 91 | + public function ImportFolderChange($folder); |
|
| 92 | 92 | |
| 93 | - /** |
|
| 94 | - * Imports a folder deletion. |
|
| 95 | - * |
|
| 96 | - * @param SyncFolder $folder at least "serverid" needs to be set |
|
| 97 | - * |
|
| 98 | - * @throws StatusException |
|
| 99 | - * |
|
| 100 | - * @return boolean/int success/SYNC_FOLDERHIERARCHY_STATUS |
|
| 101 | - */ |
|
| 102 | - public function ImportFolderDeletion($folder); |
|
| 93 | + /** |
|
| 94 | + * Imports a folder deletion. |
|
| 95 | + * |
|
| 96 | + * @param SyncFolder $folder at least "serverid" needs to be set |
|
| 97 | + * |
|
| 98 | + * @throws StatusException |
|
| 99 | + * |
|
| 100 | + * @return boolean/int success/SYNC_FOLDERHIERARCHY_STATUS |
|
| 101 | + */ |
|
| 102 | + public function ImportFolderDeletion($folder); |
|
| 103 | 103 | } |
@@ -15,127 +15,127 @@ |
||
| 15 | 15 | */ |
| 16 | 16 | |
| 17 | 17 | interface IStateMachine { |
| 18 | - public const DEFTYPE = ""; |
|
| 19 | - public const DEVICEDATA = "devicedata"; |
|
| 20 | - public const FOLDERDATA = "fd"; |
|
| 21 | - public const FAILSAVE = "fs"; |
|
| 22 | - public const HIERARCHY = "hc"; |
|
| 23 | - public const BACKENDSTORAGE = "bs"; |
|
| 18 | + public const DEFTYPE = ""; |
|
| 19 | + public const DEVICEDATA = "devicedata"; |
|
| 20 | + public const FOLDERDATA = "fd"; |
|
| 21 | + public const FAILSAVE = "fs"; |
|
| 22 | + public const HIERARCHY = "hc"; |
|
| 23 | + public const BACKENDSTORAGE = "bs"; |
|
| 24 | 24 | |
| 25 | - public const STATEVERSION_01 = "1"; |
|
| 26 | - public const STATEVERSION_02 = "2"; |
|
| 25 | + public const STATEVERSION_01 = "1"; |
|
| 26 | + public const STATEVERSION_02 = "2"; |
|
| 27 | 27 | |
| 28 | - /** |
|
| 29 | - * Constructor. |
|
| 30 | - * |
|
| 31 | - * @param mixed $devid |
|
| 32 | - * @param mixed $type |
|
| 33 | - * @param mixed $key |
|
| 34 | - * @param mixed $counter |
|
| 35 | - * |
|
| 36 | - * @throws FatalMisconfigurationException |
|
| 37 | - */ |
|
| 28 | + /** |
|
| 29 | + * Constructor. |
|
| 30 | + * |
|
| 31 | + * @param mixed $devid |
|
| 32 | + * @param mixed $type |
|
| 33 | + * @param mixed $key |
|
| 34 | + * @param mixed $counter |
|
| 35 | + * |
|
| 36 | + * @throws FatalMisconfigurationException |
|
| 37 | + */ |
|
| 38 | 38 | |
| 39 | - /** |
|
| 40 | - * Gets a hash value indicating the latest dataset of the named |
|
| 41 | - * state with a specified key and counter. |
|
| 42 | - * If the state is changed between two calls of this method |
|
| 43 | - * the returned hash should be different. |
|
| 44 | - * |
|
| 45 | - * @param string $devid the device id |
|
| 46 | - * @param string $type the state type |
|
| 47 | - * @param string $key (opt) |
|
| 48 | - * @param string $counter (opt) |
|
| 49 | - * |
|
| 50 | - * @throws StateNotFoundException, StateInvalidException, UnavailableException |
|
| 51 | - * |
|
| 52 | - * @return string |
|
| 53 | - */ |
|
| 54 | - public function GetStateHash($devid, $type, $key = false, $counter = false); |
|
| 39 | + /** |
|
| 40 | + * Gets a hash value indicating the latest dataset of the named |
|
| 41 | + * state with a specified key and counter. |
|
| 42 | + * If the state is changed between two calls of this method |
|
| 43 | + * the returned hash should be different. |
|
| 44 | + * |
|
| 45 | + * @param string $devid the device id |
|
| 46 | + * @param string $type the state type |
|
| 47 | + * @param string $key (opt) |
|
| 48 | + * @param string $counter (opt) |
|
| 49 | + * |
|
| 50 | + * @throws StateNotFoundException, StateInvalidException, UnavailableException |
|
| 51 | + * |
|
| 52 | + * @return string |
|
| 53 | + */ |
|
| 54 | + public function GetStateHash($devid, $type, $key = false, $counter = false); |
|
| 55 | 55 | |
| 56 | - /** |
|
| 57 | - * Gets a state for a specified key and counter. |
|
| 58 | - * This method should call IStateMachine->CleanStates() |
|
| 59 | - * to remove older states (same key, previous counters). |
|
| 60 | - * |
|
| 61 | - * @param string $devid the device id |
|
| 62 | - * @param string $type the state type |
|
| 63 | - * @param string $key (opt) |
|
| 64 | - * @param string $counter (opt) |
|
| 65 | - * @param string $cleanstates (opt) |
|
| 66 | - * |
|
| 67 | - * @throws StateNotFoundException, StateInvalidException, UnavailableException |
|
| 68 | - * |
|
| 69 | - * @return mixed |
|
| 70 | - */ |
|
| 71 | - public function GetState($devid, $type, $key = false, $counter = false, $cleanstates = true); |
|
| 56 | + /** |
|
| 57 | + * Gets a state for a specified key and counter. |
|
| 58 | + * This method should call IStateMachine->CleanStates() |
|
| 59 | + * to remove older states (same key, previous counters). |
|
| 60 | + * |
|
| 61 | + * @param string $devid the device id |
|
| 62 | + * @param string $type the state type |
|
| 63 | + * @param string $key (opt) |
|
| 64 | + * @param string $counter (opt) |
|
| 65 | + * @param string $cleanstates (opt) |
|
| 66 | + * |
|
| 67 | + * @throws StateNotFoundException, StateInvalidException, UnavailableException |
|
| 68 | + * |
|
| 69 | + * @return mixed |
|
| 70 | + */ |
|
| 71 | + public function GetState($devid, $type, $key = false, $counter = false, $cleanstates = true); |
|
| 72 | 72 | |
| 73 | - /** |
|
| 74 | - * Writes ta state to for a key and counter. |
|
| 75 | - * |
|
| 76 | - * @param mixed $state |
|
| 77 | - * @param string $devid the device id |
|
| 78 | - * @param string $type the state type |
|
| 79 | - * @param string $key (opt) |
|
| 80 | - * @param int $counter (opt) |
|
| 81 | - * |
|
| 82 | - * @throws StateInvalidException, UnavailableException |
|
| 83 | - * |
|
| 84 | - * @return bool |
|
| 85 | - */ |
|
| 86 | - public function SetState($state, $devid, $type, $key = false, $counter = false); |
|
| 73 | + /** |
|
| 74 | + * Writes ta state to for a key and counter. |
|
| 75 | + * |
|
| 76 | + * @param mixed $state |
|
| 77 | + * @param string $devid the device id |
|
| 78 | + * @param string $type the state type |
|
| 79 | + * @param string $key (opt) |
|
| 80 | + * @param int $counter (opt) |
|
| 81 | + * |
|
| 82 | + * @throws StateInvalidException, UnavailableException |
|
| 83 | + * |
|
| 84 | + * @return bool |
|
| 85 | + */ |
|
| 86 | + public function SetState($state, $devid, $type, $key = false, $counter = false); |
|
| 87 | 87 | |
| 88 | - /** |
|
| 89 | - * Cleans up all older states. |
|
| 90 | - * If called with a $counter, all states previous state counter can be removed. |
|
| 91 | - * If additionally the $thisCounterOnly flag is true, only that specific counter will be removed. |
|
| 92 | - * If called without $counter, all keys (independently from the counter) can be removed. |
|
| 93 | - * |
|
| 94 | - * @param string $devid the device id |
|
| 95 | - * @param string $type the state type |
|
| 96 | - * @param string $key |
|
| 97 | - * @param string $counter (opt) |
|
| 98 | - * @param string $thisCounterOnly (opt) if provided, the exact counter only will be removed |
|
| 99 | - * |
|
| 100 | - * @throws StateInvalidException |
|
| 101 | - * |
|
| 102 | - * @return |
|
| 103 | - */ |
|
| 104 | - public function CleanStates($devid, $type, $key, $counter = false, $thisCounterOnly = false); |
|
| 88 | + /** |
|
| 89 | + * Cleans up all older states. |
|
| 90 | + * If called with a $counter, all states previous state counter can be removed. |
|
| 91 | + * If additionally the $thisCounterOnly flag is true, only that specific counter will be removed. |
|
| 92 | + * If called without $counter, all keys (independently from the counter) can be removed. |
|
| 93 | + * |
|
| 94 | + * @param string $devid the device id |
|
| 95 | + * @param string $type the state type |
|
| 96 | + * @param string $key |
|
| 97 | + * @param string $counter (opt) |
|
| 98 | + * @param string $thisCounterOnly (opt) if provided, the exact counter only will be removed |
|
| 99 | + * |
|
| 100 | + * @throws StateInvalidException |
|
| 101 | + * |
|
| 102 | + * @return |
|
| 103 | + */ |
|
| 104 | + public function CleanStates($devid, $type, $key, $counter = false, $thisCounterOnly = false); |
|
| 105 | 105 | |
| 106 | - /** |
|
| 107 | - * Links a user to a device. |
|
| 108 | - * |
|
| 109 | - * @param string $username |
|
| 110 | - * @param string $devid |
|
| 111 | - * |
|
| 112 | - * @return bool indicating if the user was added or not (existed already) |
|
| 113 | - */ |
|
| 114 | - public function LinkUserDevice($username, $devid); |
|
| 106 | + /** |
|
| 107 | + * Links a user to a device. |
|
| 108 | + * |
|
| 109 | + * @param string $username |
|
| 110 | + * @param string $devid |
|
| 111 | + * |
|
| 112 | + * @return bool indicating if the user was added or not (existed already) |
|
| 113 | + */ |
|
| 114 | + public function LinkUserDevice($username, $devid); |
|
| 115 | 115 | |
| 116 | - /** |
|
| 117 | - * Unlinks a device from a user. |
|
| 118 | - * |
|
| 119 | - * @param string $username |
|
| 120 | - * @param string $devid |
|
| 121 | - * |
|
| 122 | - * @return bool |
|
| 123 | - */ |
|
| 124 | - public function UnLinkUserDevice($username, $devid); |
|
| 116 | + /** |
|
| 117 | + * Unlinks a device from a user. |
|
| 118 | + * |
|
| 119 | + * @param string $username |
|
| 120 | + * @param string $devid |
|
| 121 | + * |
|
| 122 | + * @return bool |
|
| 123 | + */ |
|
| 124 | + public function UnLinkUserDevice($username, $devid); |
|
| 125 | 125 | |
| 126 | - /** |
|
| 127 | - * Returns the current version of the state files. |
|
| 128 | - * |
|
| 129 | - * @return int |
|
| 130 | - */ |
|
| 131 | - public function GetStateVersion(); |
|
| 126 | + /** |
|
| 127 | + * Returns the current version of the state files. |
|
| 128 | + * |
|
| 129 | + * @return int |
|
| 130 | + */ |
|
| 131 | + public function GetStateVersion(); |
|
| 132 | 132 | |
| 133 | - /** |
|
| 134 | - * Sets the current version of the state files. |
|
| 135 | - * |
|
| 136 | - * @param int $version the new supported version |
|
| 137 | - * |
|
| 138 | - * @return bool |
|
| 139 | - */ |
|
| 140 | - public function SetStateVersion($version); |
|
| 133 | + /** |
|
| 134 | + * Sets the current version of the state files. |
|
| 135 | + * |
|
| 136 | + * @param int $version the new supported version |
|
| 137 | + * |
|
| 138 | + * @return bool |
|
| 139 | + */ |
|
| 140 | + public function SetStateVersion($version); |
|
| 141 | 141 | } |
@@ -10,29 +10,29 @@ |
||
| 10 | 10 | */ |
| 11 | 11 | |
| 12 | 12 | interface IExportChanges extends IChanges { |
| 13 | - /** |
|
| 14 | - * Sets the importer where the exporter will sent its changes to |
|
| 15 | - * This exporter should also be ready to accept calls after this. |
|
| 16 | - * |
|
| 17 | - * @param object &$importer Implementation of IImportChanges |
|
| 18 | - * |
|
| 19 | - * @throws StatusException |
|
| 20 | - * |
|
| 21 | - * @return bool |
|
| 22 | - */ |
|
| 23 | - public function InitializeExporter(&$importer); |
|
| 13 | + /** |
|
| 14 | + * Sets the importer where the exporter will sent its changes to |
|
| 15 | + * This exporter should also be ready to accept calls after this. |
|
| 16 | + * |
|
| 17 | + * @param object &$importer Implementation of IImportChanges |
|
| 18 | + * |
|
| 19 | + * @throws StatusException |
|
| 20 | + * |
|
| 21 | + * @return bool |
|
| 22 | + */ |
|
| 23 | + public function InitializeExporter(&$importer); |
|
| 24 | 24 | |
| 25 | - /** |
|
| 26 | - * Returns the amount of changes to be exported. |
|
| 27 | - * |
|
| 28 | - * @return int |
|
| 29 | - */ |
|
| 30 | - public function GetChangeCount(); |
|
| 25 | + /** |
|
| 26 | + * Returns the amount of changes to be exported. |
|
| 27 | + * |
|
| 28 | + * @return int |
|
| 29 | + */ |
|
| 30 | + public function GetChangeCount(); |
|
| 31 | 31 | |
| 32 | - /** |
|
| 33 | - * Synchronizes a change to the configured importer. |
|
| 34 | - * |
|
| 35 | - * @return array with status information |
|
| 36 | - */ |
|
| 37 | - public function Synchronize(); |
|
| 32 | + /** |
|
| 33 | + * Synchronizes a change to the configured importer. |
|
| 34 | + * |
|
| 35 | + * @return array with status information |
|
| 36 | + */ |
|
| 37 | + public function Synchronize(); |
|
| 38 | 38 | } |