1
0
Fork 0
mirror of https://github.com/YunoHost-Apps/z-push_ynh.git synced 2024-09-03 18:05:58 +02:00
z-push_ynh/sources/backend/zarafa/importer.php
2014-12-18 18:08:26 +01:00

706 lines
35 KiB
PHP

<?php
/***********************************************
* File : importer.php
* Project : Z-Push
* Descr : This is a generic class that is
* used by both the proxy importer
* (for outgoing messages) and our
* local importer (for incoming
* messages). Basically all shared
* conversion data for converting
* to and from MAPI objects is in here.
*
* Created : 14.02.2011
*
* Copyright 2007 - 2013 Zarafa Deutschland GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation with the following additional
* term according to sec. 7:
*
* According to sec. 7 of the GNU Affero General Public License, version 3,
* the terms of the AGPL are supplemented with the following terms:
*
* "Zarafa" is a registered trademark of Zarafa B.V.
* "Z-Push" is a registered trademark of Zarafa Deutschland GmbH
* The licensing of the Program under the AGPL does not imply a trademark license.
* Therefore any rights, title and interest in our trademarks remain entirely with us.
*
* However, if you propagate an unmodified version of the Program you are
* allowed to use the term "Z-Push" to indicate that you distribute the Program.
* Furthermore you may use our trademarks where it is necessary to indicate
* the intended purpose of a product or service provided you use it in accordance
* with honest practices in industrial or commercial matters.
* If you want to propagate modified versions of the Program under the name "Z-Push",
* you may only do so if you have a written permission by Zarafa Deutschland GmbH
* (to acquire a permission please contact Zarafa at trademark@zarafa.com).
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* 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 change in 'star' flag
* This can never conflict
*
* @param string $id
* @param int $flags
*
* @access public
* @return boolean
* @throws StatusException
*/
public function ImportMessageStarFlag($id, $flags) {
return false;
}
/**
* 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;
}
}
?>