. * * Consult LICENSE file for details ************************************************/ /** * This is our local importer. Tt receives data from the PDA, for contents and hierarchy changes. * It must therefore receive the incoming data and convert it into MAPI objects, and then send * them to the ICS importer to do the actual writing of the object. * The creation of folders is fairly trivial, because folders that are created on * the PDA are always e-mail folders. */ class ImportChangesICS implements IImportChanges { private $folderid; private $store; private $session; private $flags; private $statestream; private $importer; private $memChanges; private $mapiprovider; private $conflictsLoaded; private $conflictsContentParameters; private $conflictsState; private $cutoffdate; private $contentClass; /** * Constructor * * @param mapisession $session * @param mapistore $store * @param string $folderid (opt) * * @access public * @throws StatusException */ public function ImportChangesICS($session, $store, $folderid = false) { $this->session = $session; $this->store = $store; $this->folderid = $folderid; $this->conflictsLoaded = false; $this->cutoffdate = false; $this->contentClass = false; if ($folderid) { $entryid = mapi_msgstore_entryidfromsourcekey($store, $folderid); } else { $storeprops = mapi_getprops($store, array(PR_IPM_SUBTREE_ENTRYID)); $entryid = $storeprops[PR_IPM_SUBTREE_ENTRYID]; } $folder = false; if ($entryid) $folder = mapi_msgstore_openentry($store, $entryid); if(!$folder) { $this->importer = false; // We throw an general error SYNC_FSSTATUS_CODEUNKNOWN (12) which is also SYNC_STATUS_FOLDERHIERARCHYCHANGED (12) // if this happened while doing content sync, the mobile will try to resync the folderhierarchy throw new StatusException(sprintf("ImportChangesICS('%s','%s','%s'): Error, unable to open folder: 0x%X", $session, $store, Utils::PrintAsString($folderid), mapi_last_hresult()), SYNC_FSSTATUS_CODEUNKNOWN); } $this->mapiprovider = new MAPIProvider($this->session, $this->store); if ($folderid) $this->importer = mapi_openproperty($folder, PR_COLLECTOR, IID_IExchangeImportContentsChanges, 0 , 0); else $this->importer = mapi_openproperty($folder, PR_COLLECTOR, IID_IExchangeImportHierarchyChanges, 0 , 0); } /** * Initializes the importer * * @param string $state * @param int $flags * * @access public * @return boolean * @throws StatusException */ public function Config($state, $flags = 0) { $this->flags = $flags; // this should never happen if ($this->importer === false) throw new StatusException("ImportChangesICS->Config(): Error, importer not available", SYNC_FSSTATUS_CODEUNKNOWN, null, LOGLEVEL_ERROR); // Put the state information in a stream that can be used by ICS $stream = mapi_stream_create(); if(strlen($state) == 0) $state = hex2bin("0000000000000000"); ZLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->Config(): initializing importer with state: 0x%s", bin2hex($state))); mapi_stream_write($stream, $state); $this->statestream = $stream; if ($this->folderid !== false) { // possible conflicting messages will be cached here $this->memChanges = new ChangesMemoryWrapper(); $stat = mapi_importcontentschanges_config($this->importer, $stream, $flags); } else $stat = mapi_importhierarchychanges_config($this->importer, $stream, $flags); if (!$stat) throw new StatusException(sprintf("ImportChangesICS->Config(): Error, mapi_import_*_changes_config() failed: 0x%X", mapi_last_hresult()), SYNC_FSSTATUS_CODEUNKNOWN, null, LOGLEVEL_WARN); return $stat; } /** * Configures additional parameters for content selection * * @param ContentParameters $contentparameters * * @access public * @return boolean * @throws StatusException */ public function ConfigContentParameters($contentparameters) { $filtertype = $contentparameters->GetFilterType(); switch($contentparameters->GetContentClass()) { case "Email": $this->cutoffdate = ($filtertype) ? Utils::GetCutOffDate($filtertype) : false; break; case "Calendar": $this->cutoffdate = ($filtertype) ? Utils::GetCutOffDate($filtertype) : false; break; default: case "Contacts": case "Tasks": $this->cutoffdate = false; break; } $this->contentClass = $contentparameters->GetContentClass(); } /** * Reads state from the Importer * * @access public * @return string * @throws StatusException */ public function GetState() { $error = false; if(!isset($this->statestream) || $this->importer === false) $error = true; if ($error === false && $this->folderid !== false && function_exists("mapi_importcontentschanges_updatestate")) if(mapi_importcontentschanges_updatestate($this->importer, $this->statestream) != true) $error = true; if ($error == true) 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); mapi_stream_seek($this->statestream, 0, STREAM_SEEK_SET); $state = ""; while(true) { $data = mapi_stream_read($this->statestream, 4096); if(strlen($data)) $state .= $data; else break; } return $state; } /** * Checks if a message is in the synchronization interval (window) * if a filter (e.g. Sync items two weeks back) or limits this synchronization. * These checks only apply to Emails and Appointments only, Contacts, Tasks and Notes do not have time restrictions. * * @param string $messageid the message id to be checked * * @access private * @return boolean */ private function isMessageInSyncInterval($messageid) { // if there is no restriciton we do not need to check if ($this->cutoffdate === false) return true; ZLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->isMessageInSyncInterval('%s'): cut off date is: %s", $messageid, $this->cutoffdate)); $entryid = mapi_msgstore_entryidfromsourcekey($this->store, $this->folderid, hex2bin($messageid)); if(!$entryid) { ZLog::Write(LOGLEVEL_WARN, sprintf("ImportChangesICS->isMessageInSyncInterval('%s'): Error, unable to resolve message id: 0x%X", $messageid, mapi_last_hresult())); return false; } $mapimessage = mapi_msgstore_openentry($this->store, $entryid); if(!$mapimessage) { ZLog::Write(LOGLEVEL_WARN, sprintf("ImportChangesICS->isMessageInSyncInterval('%s'): Error, unable to open entry id: 0x%X", $messageid, mapi_last_hresult())); return false; } if ($this->contentClass == "Email") return MAPIUtils::IsInEmailSyncInterval($this->store, $mapimessage, $this->cutoffdate); elseif ($this->contentClass == "Calendar") return MAPIUtils::IsInCalendarSyncInterval($this->store, $mapimessage, $this->cutoffdate); return true; } /**---------------------------------------------------------------------------------------------------------- * Methods for ContentsExporter */ /** * Loads objects which are expected to be exported with the state * Before importing/saving the actual message from the mobile, a conflict detection should be done * * @param ContentParameters $contentparameters class of objects * @param string $state * * @access public * @return boolean * @throws StatusException */ public function LoadConflicts($contentparameters, $state) { if (!isset($this->session) || !isset($this->store) || !isset($this->folderid)) throw new StatusException("ImportChangesICS->LoadConflicts(): Error, can not load changes for conflict detection. Session, store or folder information not available", SYNC_STATUS_SERVERERROR); // save data to load changes later if necessary $this->conflictsLoaded = false; $this->conflictsContentParameters = $contentparameters; $this->conflictsState = $state; ZLog::Write(LOGLEVEL_DEBUG, "ImportChangesICS->LoadConflicts(): will be loaded later if necessary"); return true; } /** * Potential conflicts are only loaded when really necessary, * e.g. on ADD or MODIFY * * @access private * @return */ private function lazyLoadConflicts() { if (!isset($this->session) || !isset($this->store) || !isset($this->folderid) || !isset($this->conflictsContentParameters) || $this->conflictsState === false) { ZLog::Write(LOGLEVEL_WARN, "ImportChangesICS->lazyLoadConflicts(): can not load potential conflicting changes in lazymode for conflict detection. Missing information"); return false; } if (!$this->conflictsLoaded) { ZLog::Write(LOGLEVEL_DEBUG, "ImportChangesICS->lazyLoadConflicts(): loading.."); // configure an exporter so we can detect conflicts $exporter = new ExportChangesICS($this->session, $this->store, $this->folderid); $exporter->Config($this->conflictsState); $exporter->ConfigContentParameters($this->conflictsContentParameters); $exporter->InitializeExporter($this->memChanges); // monitor how long it takes to export potential conflicts // if this takes "too long" we cancel this operation! $potConflicts = $exporter->GetChangeCount(); $started = time(); $exported = 0; while(is_array($exporter->Synchronize())) { $exported++; // stop if this takes more than 15 seconds and there are more than 5 changes still to be exported // within 20 seconds this should be finished or it will not be performed if ((time() - $started) > 15 && ($potConflicts - $exported) > 5 ) { ZLog::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)); $this->conflictsLoaded = true; return; } } $this->conflictsLoaded = true; } } /** * Imports a single message * * @param string $id * @param SyncObject $message * * @access public * @return boolean/string - failure / id of message * @throws StatusException */ public function ImportMessageChange($id, $message) { $parentsourcekey = $this->folderid; if($id) $sourcekey = hex2bin($id); $flags = 0; $props = array(); $props[PR_PARENT_SOURCE_KEY] = $parentsourcekey; // set the PR_SOURCE_KEY if available or mark it as new message if($id) { $props[PR_SOURCE_KEY] = $sourcekey; // on editing an existing message, check if it is in the synchronization interval if (!$this->isMessageInSyncInterval($id)) throw new StatusException(sprintf("ImportChangesICS->ImportMessageChange('%s','%s'): Message is outside the sync interval. Data not saved.", $id, get_class($message)), SYNC_STATUS_SYNCCANNOTBECOMPLETED); // check for conflicts $this->lazyLoadConflicts(); if($this->memChanges->IsChanged($id)) { if ($this->flags & SYNC_CONFLICT_OVERWRITE_PIM) { // in these cases the status SYNC_STATUS_CONFLICTCLIENTSERVEROBJECT should be returned, so the mobile client can inform the end user 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); return false; } else ZLog::Write(LOGLEVEL_INFO, sprintf("ImportChangesICS->ImportMessageChange('%s','%s'): Conflict detected. Data from Server will be dropped! PIM overwrites server.", $id, get_class($message))); } if($this->memChanges->IsDeleted($id)) { ZLog::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))); return false; } } else $flags = SYNC_NEW_MESSAGE; if(mapi_importcontentschanges_importmessagechange($this->importer, $props, $flags, $mapimessage)) { $this->mapiprovider->SetMessage($mapimessage, $message); mapi_message_savechanges($mapimessage); if (mapi_last_hresult()) throw new StatusException(sprintf("ImportChangesICS->ImportMessageChange('%s','%s'): Error, mapi_message_savechanges() failed: 0x%X", $id, get_class($message), mapi_last_hresult()), SYNC_STATUS_SYNCCANNOTBECOMPLETED); $sourcekeyprops = mapi_getprops($mapimessage, array (PR_SOURCE_KEY)); return bin2hex($sourcekeyprops[PR_SOURCE_KEY]); } else throw new StatusException(sprintf("ImportChangesICS->ImportMessageChange('%s','%s'): Error updating object: 0x%X", $id, get_class($message), mapi_last_hresult()), SYNC_STATUS_OBJECTNOTFOUND); } /** * Imports a deletion. This may conflict if the local object has been modified * * @param string $id * * @access public * @return boolean * @throws StatusException */ public function ImportMessageDeletion($id) { // check if the message is in the current syncinterval if (!$this->isMessageInSyncInterval($id)) throw new StatusException(sprintf("ImportChangesICS->ImportMessageDeletion('%s'): Message is outside the sync interval and so far not deleted.", $id), SYNC_STATUS_OBJECTNOTFOUND); // check for conflicts $this->lazyLoadConflicts(); if($this->memChanges->IsChanged($id)) { ZLog::Write(LOGLEVEL_INFO, sprintf("ImportChangesICS->ImportMessageDeletion('%s'): Conflict detected. Data from Server will be dropped! PIM deleted object.", $id)); } elseif($this->memChanges->IsDeleted($id)) { ZLog::Write(LOGLEVEL_INFO, sprintf("ImportChangesICS->ImportMessageDeletion('%s'): Conflict detected. Data is already deleted. Request will be ignored.", $id)); return true; } // do a 'soft' delete so people can un-delete if necessary if(mapi_importcontentschanges_importmessagedeletion($this->importer, 1, array(hex2bin($id)))) throw new StatusException(sprintf("ImportChangesICS->ImportMessageDeletion('%s'): Error updating object: 0x%X", $id, mapi_last_hresult()), SYNC_STATUS_OBJECTNOTFOUND); return true; } /** * Imports a change in 'read' flag * This can never conflict * * @param string $id * @param int $flags - read/unread * * @access public * @return boolean * @throws StatusException */ public function ImportMessageReadFlag($id, $flags) { // check if the message is in the current syncinterval if (!$this->isMessageInSyncInterval($id)) throw new StatusException(sprintf("ImportChangesICS->ImportMessageReadFlag('%s','%d'): Message is outside the sync interval. Flags not updated.", $id, $flags), SYNC_STATUS_OBJECTNOTFOUND); // check for conflicts /* * Checking for conflicts is correct at this point, but is a very expensive operation. * If the message was deleted, only an error will be shown. * $this->lazyLoadConflicts(); if($this->memChanges->IsDeleted($id)) { ZLog::Write(LOGLEVEL_INFO, sprintf("ImportChangesICS->ImportMessageReadFlag('%s'): Conflict detected. Data is already deleted. Request will be ignored.", $id)); return true; } */ $readstate = array ( "sourcekey" => hex2bin($id), "flags" => $flags); if(!mapi_importcontentschanges_importperuserreadstatechange($this->importer, array($readstate) )) throw new StatusException(sprintf("ImportChangesICS->ImportMessageReadFlag('%s','%d'): Error setting read state: 0x%X", $id, $flags, mapi_last_hresult()), SYNC_STATUS_OBJECTNOTFOUND); return true; } /** * Imports a move of a message. This occurs when a user moves an item to another folder * * Normally, we would implement this via the 'offical' importmessagemove() function on the ICS importer, * but the Zarafa importer does not support this. Therefore we currently implement it via a standard mapi * call. This causes a mirror 'add/delete' to be sent to the PDA at the next sync. * Manfred, 2010-10-21. For some mobiles import was causing duplicate messages in the destination folder * (Mantis #202). Therefore we will create a new message in the destination folder, copy properties * of the source message to the new one and then delete the source message. * * @param string $id * @param string $newfolder destination folder * * @access public * @return boolean * @throws StatusException */ public function ImportMessageMove($id, $newfolder) { if (strtolower($newfolder) == strtolower(bin2hex($this->folderid)) ) throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, source and destination are equal", $id, $newfolder), SYNC_MOVEITEMSSTATUS_SAMESOURCEANDDEST); // check if the source message is in the current syncinterval if (!$this->isMessageInSyncInterval($id)) throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Source message is outside the sync interval. Move not performed.", $id, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID); // Get the entryid of the message we're moving $entryid = mapi_msgstore_entryidfromsourcekey($this->store, $this->folderid, hex2bin($id)); if(!$entryid) throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to resolve source message id", $id, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID); //open the source message $srcmessage = mapi_msgstore_openentry($this->store, $entryid); if (!$srcmessage) throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to open source message: 0x%X", $id, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID); // get correct mapi store for the destination folder $dststore = ZPush::GetBackend()->GetMAPIStoreForFolderId(ZPush::GetAdditionalSyncFolderStore($newfolder), $newfolder); if ($dststore === false) throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to open store of destination folder", $id, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDDESTID); $dstentryid = mapi_msgstore_entryidfromsourcekey($dststore, hex2bin($newfolder)); if(!$dstentryid) throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to resolve destination folder", $id, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDDESTID); $dstfolder = mapi_msgstore_openentry($dststore, $dstentryid); if(!$dstfolder) throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to open destination folder", $id, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDDESTID); $newmessage = mapi_folder_createmessage($dstfolder); if (!$newmessage) throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to create message in destination folder: 0x%X", $id, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_INVALIDDESTID); // Copy message mapi_copyto($srcmessage, array(), array(), $newmessage); if (mapi_last_hresult()) throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, copy to destination message failed: 0x%X", $id, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_CANNOTMOVE); $srcfolderentryid = mapi_msgstore_entryidfromsourcekey($this->store, $this->folderid); if(!$srcfolderentryid) throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to resolve source folder", $id, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID); $srcfolder = mapi_msgstore_openentry($this->store, $srcfolderentryid); if (!$srcfolder) throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to open source folder: 0x%X", $id, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID); // Save changes mapi_savechanges($newmessage); if (mapi_last_hresult()) throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, mapi_savechanges() failed: 0x%X", $id, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_CANNOTMOVE); // Delete the old message if (!mapi_folder_deletemessages($srcfolder, array($entryid))) throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, delete of source message failed: 0x%X. Possible duplicates.", $id, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_SOURCEORDESTLOCKED); $sourcekeyprops = mapi_getprops($newmessage, array (PR_SOURCE_KEY)); if (isset($sourcekeyprops[PR_SOURCE_KEY]) && $sourcekeyprops[PR_SOURCE_KEY]) return bin2hex($sourcekeyprops[PR_SOURCE_KEY]); return false; } /**---------------------------------------------------------------------------------------------------------- * Methods for HierarchyExporter */ /** * Imports a change on a folder * * @param object $folder SyncFolder * * @access public * @return string id of the folder * @throws StatusException */ public function ImportFolderChange($folder) { $id = isset($folder->serverid)?$folder->serverid:false; $parent = $folder->parentid; $displayname = u2wi($folder->displayname); $type = $folder->type; if (Utils::IsSystemFolder($type)) 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); // create a new folder if $id is not set if (!$id) { // the root folder is "0" - get IPM_SUBTREE if ($parent == "0") { $parentprops = mapi_getprops($this->store, array(PR_IPM_SUBTREE_ENTRYID)); if (isset($parentprops[PR_IPM_SUBTREE_ENTRYID])) $parentfentryid = $parentprops[PR_IPM_SUBTREE_ENTRYID]; } else $parentfentryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($parent)); if (!$parentfentryid) 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); $parentfolder = mapi_msgstore_openentry($this->store, $parentfentryid); if (!$parentfolder) 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); // mapi_folder_createfolder() fails if a folder with this name already exists -> MAPI_E_COLLISION $newfolder = mapi_folder_createfolder($parentfolder, $displayname, ""); if (mapi_last_hresult()) 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); mapi_setprops($newfolder, array(PR_CONTAINER_CLASS => MAPIUtils::GetContainerClassFromFolderType($type))); $props = mapi_getprops($newfolder, array(PR_SOURCE_KEY)); if (isset($props[PR_SOURCE_KEY])) { $sourcekey = bin2hex($props[PR_SOURCE_KEY]); ZLog::Write(LOGLEVEL_DEBUG, sprintf("Created folder '%s' with id: '%s'", $displayname, $sourcekey)); return $sourcekey; } else 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); return false; } // open folder for update $entryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($id)); if (!$entryid) 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); // check if this is a MAPI default folder if ($this->mapiprovider->IsMAPIDefaultFolder($entryid)) 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); $mfolder = mapi_msgstore_openentry($this->store, $entryid); if (!$mfolder) 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); $props = mapi_getprops($mfolder, array(PR_SOURCE_KEY, PR_PARENT_SOURCE_KEY, PR_DISPLAY_NAME, PR_CONTAINER_CLASS)); if (!isset($props[PR_SOURCE_KEY]) || !isset($props[PR_PARENT_SOURCE_KEY]) || !isset($props[PR_DISPLAY_NAME]) || !isset($props[PR_CONTAINER_CLASS])) 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); // get the real parent source key from mapi if ($parent == "0") { $parentprops = mapi_getprops($this->store, array(PR_IPM_SUBTREE_ENTRYID)); $parentfentryid = $parentprops[PR_IPM_SUBTREE_ENTRYID]; $mapifolder = mapi_msgstore_openentry($this->store, $parentfentryid); $rootfolderprops = mapi_getprops($mapifolder, array(PR_SOURCE_KEY)); $parent = bin2hex($rootfolderprops[PR_SOURCE_KEY]); ZLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->ImportFolderChange(): resolved AS parent '0' to sourcekey '%s'", $parent)); } // a changed parent id means that the folder should be moved if (bin2hex($props[PR_PARENT_SOURCE_KEY]) !== $parent) { $sourceparentfentryid = mapi_msgstore_entryidfromsourcekey($this->store, $props[PR_PARENT_SOURCE_KEY]); if(!$sourceparentfentryid) 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); $sourceparentfolder = mapi_msgstore_openentry($this->store, $sourceparentfentryid); if(!$sourceparentfolder) 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); $destparentfentryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($parent)); if(!$sourceparentfentryid) 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); $destfolder = mapi_msgstore_openentry($this->store, $destparentfentryid); if(!$destfolder) 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); // mapi_folder_copyfolder() fails if a folder with this name already exists -> MAPI_E_COLLISION if(! mapi_folder_copyfolder($sourceparentfolder, $entryid, $destfolder, $displayname, FOLDER_MOVE)) 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); $folderProps = mapi_getprops($mfolder, array(PR_SOURCE_KEY)); return $folderProps[PR_SOURCE_KEY]; } // update the display name $props = array(PR_DISPLAY_NAME => $displayname); mapi_setprops($mfolder, $props); mapi_savechanges($mfolder); if (mapi_last_hresult()) 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); ZLog::Write(LOGLEVEL_DEBUG, "Imported changes for folder: $id"); return $id; } /** * Imports a folder deletion * * @param string $id * @param string $parent id is ignored in ICS * * @access public * @return int SYNC_FOLDERHIERARCHY_STATUS * @throws StatusException */ public function ImportFolderDeletion($id, $parent = false) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->ImportFolderDeletion('%s','%s'): importing folder deletetion", $id, $parent)); $folderentryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($id)); if(!$folderentryid) throw new StatusException(sprintf("ImportChangesICS->ImportFolderDeletion('%s','%s'): Error, unable to resolve folder", $id, $parent, mapi_last_hresult()), SYNC_FSSTATUS_FOLDERDOESNOTEXIST); // get the folder type from the MAPIProvider $type = $this->mapiprovider->GetFolderType($folderentryid); if (Utils::IsSystemFolder($type) || $this->mapiprovider->IsMAPIDefaultFolder($folderentryid)) throw new StatusException(sprintf("ImportChangesICS->ImportFolderDeletion('%s','%s'): Error deleting system/default folder", $id, $parent), SYNC_FSSTATUS_SYSTEMFOLDER); $ret = mapi_importhierarchychanges_importfolderdeletion ($this->importer, 0, array(PR_SOURCE_KEY => hex2bin($id))); if (!$ret) throw new StatusException(sprintf("ImportChangesICS->ImportFolderDeletion('%s','%s'): Error deleting folder: 0x%X", $id, $parent, mapi_last_hresult()), SYNC_FSSTATUS_SERVERERROR); return $ret; } } ?>