mirror of
https://github.com/YunoHost-Apps/z-push_ynh.git
synced 2024-09-03 18:05:58 +02:00
691 lines
35 KiB
PHP
691 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 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;
|
|
}
|
|
}
|
|
?>
|