1<?php 2/*********************************************** 3* File : importer.php 4* Project : Z-Push 5* Descr : This is a generic class that is 6* used by both the proxy importer 7* (for outgoing messages) and our 8* local importer (for incoming 9* messages). Basically all shared 10* conversion data for converting 11* to and from MAPI objects is in here. 12* 13* Created : 14.02.2011 14* 15* Copyright 2007 - 2016 Zarafa Deutschland GmbH 16* 17* This program is free software: you can redistribute it and/or modify 18* it under the terms of the GNU Affero General Public License, version 3, 19* as published by the Free Software Foundation. 20* 21* This program is distributed in the hope that it will be useful, 22* but WITHOUT ANY WARRANTY; without even the implied warranty of 23* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 24* GNU Affero General Public License for more details. 25* 26* You should have received a copy of the GNU Affero General Public License 27* along with this program. If not, see <http://www.gnu.org/licenses/>. 28* 29* Consult LICENSE file for details 30************************************************/ 31 32 33 34/** 35 * This is our local importer. Tt receives data from the PDA, for contents and hierarchy changes. 36 * It must therefore receive the incoming data and convert it into MAPI objects, and then send 37 * them to the ICS importer to do the actual writing of the object. 38 * The creation of folders is fairly trivial, because folders that are created on 39 * the PDA are always e-mail folders. 40 */ 41 42class ImportChangesICS implements IImportChanges { 43 private $folderid; 44 private $folderidHex; 45 private $store; 46 private $session; 47 private $flags; 48 private $statestream; 49 private $importer; 50 private $memChanges; 51 private $mapiprovider; 52 private $conflictsLoaded; 53 private $conflictsContentParameters; 54 private $conflictsState; 55 private $cutoffdate; 56 private $contentClass; 57 private $prefix; 58 private $moveSrcState; 59 private $moveDstState; 60 61 /** 62 * Constructor 63 * 64 * @param mapisession $session 65 * @param mapistore $store 66 * @param string $folderid (opt) 67 * 68 * @access public 69 * @throws StatusException 70 */ 71 public function __construct($session, $store, $folderid = false) { 72 $this->session = $session; 73 $this->store = $store; 74 $this->folderid = $folderid; 75 $this->folderidHex = bin2hex($folderid); 76 $this->conflictsLoaded = false; 77 $this->cutoffdate = false; 78 $this->contentClass = false; 79 $this->prefix = ''; 80 81 if ($folderid) { 82 $entryid = mapi_msgstore_entryidfromsourcekey($store, $folderid); 83 $folderidForBackendId = ZPush::GetDeviceManager()->GetFolderIdForBackendId($this->folderidHex); 84 // Only append backend id if the mapping backendid<->folderid is available. 85 if ($folderidForBackendId != $this->folderidHex) { 86 $this->prefix = $folderidForBackendId . ':'; 87 } 88 } 89 else { 90 $storeprops = mapi_getprops($store, array(PR_IPM_SUBTREE_ENTRYID, PR_IPM_PUBLIC_FOLDERS_ENTRYID)); 91 if (ZPush::GetBackend()->GetImpersonatedUser() == 'system') { 92 $entryid = $storeprops[PR_IPM_PUBLIC_FOLDERS_ENTRYID]; 93 } 94 else { 95 $entryid = $storeprops[PR_IPM_SUBTREE_ENTRYID]; 96 } 97 } 98 99 $folder = false; 100 if ($entryid) 101 $folder = mapi_msgstore_openentry($store, $entryid); 102 103 if(!$folder) { 104 $this->importer = false; 105 106 // We throw an general error SYNC_FSSTATUS_CODEUNKNOWN (12) which is also SYNC_STATUS_FOLDERHIERARCHYCHANGED (12) 107 // if this happened while doing content sync, the mobile will try to resync the folderhierarchy 108 throw new StatusException(sprintf("ImportChangesICS('%s','%s'): Error, unable to open folder: 0x%X", $session, bin2hex($folderid), mapi_last_hresult()), SYNC_FSSTATUS_CODEUNKNOWN); 109 } 110 111 $this->mapiprovider = new MAPIProvider($this->session, $this->store); 112 113 if ($folderid) { 114 $this->importer = mapi_openproperty($folder, PR_COLLECTOR, IID_IExchangeImportContentsChanges, 0 , 0); 115 } 116 else { 117 $this->importer = mapi_openproperty($folder, PR_COLLECTOR, IID_IExchangeImportHierarchyChanges, 0 , 0); 118 } 119 } 120 121 /** 122 * Initializes the importer 123 * 124 * @param string $state 125 * @param int $flags 126 * 127 * @access public 128 * @return boolean 129 * @throws StatusException 130 */ 131 public function Config($state, $flags = 0) { 132 $this->flags = $flags; 133 134 // this should never happen 135 if ($this->importer === false) 136 throw new StatusException("ImportChangesICS->Config(): Error, importer not available", SYNC_FSSTATUS_CODEUNKNOWN, null, LOGLEVEL_ERROR); 137 138 // Put the state information in a stream that can be used by ICS 139 $stream = mapi_stream_create(); 140 if(strlen($state) == 0) 141 $state = hex2bin("0000000000000000"); 142 143 ZLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->Config(): initializing importer with state: 0x%s", bin2hex($state))); 144 145 mapi_stream_write($stream, $state); 146 $this->statestream = $stream; 147 148 if ($this->folderid !== false) { 149 // possible conflicting messages will be cached here 150 $this->memChanges = new ChangesMemoryWrapper(); 151 $stat = mapi_importcontentschanges_config($this->importer, $stream, $flags); 152 } 153 else 154 $stat = mapi_importhierarchychanges_config($this->importer, $stream, $flags); 155 156 if (!$stat) 157 throw new StatusException(sprintf("ImportChangesICS->Config(): Error, mapi_import_*_changes_config() failed: 0x%X", mapi_last_hresult()), SYNC_FSSTATUS_CODEUNKNOWN, null, LOGLEVEL_WARN); 158 return $stat; 159 } 160 161 /** 162 * Configures additional parameters for content selection 163 * 164 * @param ContentParameters $contentparameters 165 * 166 * @access public 167 * @return boolean 168 * @throws StatusException 169 */ 170 public function ConfigContentParameters($contentparameters) { 171 $filtertype = $contentparameters->GetFilterType(); 172 switch($contentparameters->GetContentClass()) { 173 case "Email": 174 $this->cutoffdate = ($filtertype) ? Utils::GetCutOffDate($filtertype) : false; 175 break; 176 case "Calendar": 177 $this->cutoffdate = ($filtertype) ? Utils::GetCutOffDate($filtertype) : false; 178 break; 179 default: 180 case "Contacts": 181 case "Tasks": 182 $this->cutoffdate = false; 183 break; 184 } 185 $this->contentClass = $contentparameters->GetContentClass(); 186 } 187 188 /** 189 * Reads state from the Importer 190 * 191 * @access public 192 * @return string 193 * @throws StatusException 194 */ 195 public function GetState() { 196 $error = false; 197 if(!isset($this->statestream) || $this->importer === false) 198 $error = true; 199 200 if ($error === false && $this->folderid !== false && function_exists("mapi_importcontentschanges_updatestate")) 201 if(mapi_importcontentschanges_updatestate($this->importer, $this->statestream) != true) 202 $error = true; 203 204 if ($error == true) 205 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); 206 207 mapi_stream_seek($this->statestream, 0, STREAM_SEEK_SET); 208 209 $state = ""; 210 while(true) { 211 $data = mapi_stream_read($this->statestream, 4096); 212 if(strlen($data)) 213 $state .= $data; 214 else 215 break; 216 } 217 218 return $state; 219 } 220 221 /** 222 * Sets the states from move operations. 223 * When src and dst state are set, a MOVE operation is being executed. 224 * 225 * @param mixed $srcState 226 * @param mixed (opt) $dstState, default: null 227 * 228 * @access public 229 * @return boolean 230 */ 231 public function SetMoveStates($srcState, $dstState = null) { 232 $this->moveSrcState = $srcState; 233 $this->moveDstState = $dstState; 234 return true; 235 } 236 237 /** 238 * Gets the states of special move operations. 239 * 240 * @access public 241 * @return array(0 => $srcState, 1 => $dstState) 242 */ 243 public function GetMoveStates() { 244 return array($this->moveSrcState, $this->moveDstState); 245 } 246 247 /** 248 * Checks if a message may be modified. This involves checking: 249 * - if there is a synchronization interval and if so, if the message is in it (sync window). 250 * These checks only apply to Emails and Appointments only, Contacts, Tasks and Notes do not have time restrictions. 251 * - if the message is not marked as private in a shared folder. 252 * 253 * @param string $messageid the message id to be checked 254 * 255 * @access private 256 * @return boolean 257 */ 258 private function isModificationAllowed($messageid) { 259 260 $sharedUser = ZPush::GetAdditionalSyncFolderStore(bin2hex($this->folderid)); 261 // if this is either a user folder or SYSTEM and no restriction is set, we don't need to check 262 if (($sharedUser == false || $sharedUser == 'SYSTEM') && $this->cutoffdate === false && !ZPush::GetBackend()->GetImpersonatedUser()) { 263 return true; 264 } 265 266 // open the existing object 267 $entryid = mapi_msgstore_entryidfromsourcekey($this->store, $this->folderid, hex2bin($messageid)); 268 if(!$entryid) { 269 ZLog::Write(LOGLEVEL_WARN, sprintf("ImportChangesICS->isModificationAllowed('%s'): Error, unable to resolve message id: 0x%X", $messageid, mapi_last_hresult())); 270 return false; 271 } 272 273 $mapimessage = mapi_msgstore_openentry($this->store, $entryid); 274 if(!$mapimessage) { 275 ZLog::Write(LOGLEVEL_WARN, sprintf("ImportChangesICS->isModificationAllowed('%s'): Error, unable to open entry id: 0x%X", $messageid, mapi_last_hresult())); 276 return false; 277 } 278 279 // check the sync interval 280 if ($this->cutoffdate !== false) { 281 ZLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->isModificationAllowed('%s'): cut off date is: %s", $messageid, $this->cutoffdate)); 282 if ( ($this->contentClass == "Email" && !MAPIUtils::IsInEmailSyncInterval($this->store, $mapimessage, $this->cutoffdate)) || 283 ($this->contentClass == "Calendar" && !MAPIUtils::IsInCalendarSyncInterval($this->store, $mapimessage, $this->cutoffdate)) ) { 284 285 ZLog::Write(LOGLEVEL_WARN, sprintf("ImportChangesICS->isModificationAllowed('%s'): Message in %s is outside the sync interval. Data not saved.", $messageid, $this->contentClass)); 286 return false; 287 } 288 } 289 290 // check if not private 291 if (MAPIUtils::IsMessageSharedAndPrivate($this->folderid, $mapimessage)) { 292 ZLog::Write(LOGLEVEL_WARN, sprintf("ImportChangesICS->isModificationAllowed('%s'): Message is shared and marked as private. Data not saved.", $messageid)); 293 return false; 294 } 295 296 // yes, modification allowed 297 return true; 298 } 299 300 /**---------------------------------------------------------------------------------------------------------- 301 * Methods for ContentsExporter 302 */ 303 304 /** 305 * Loads objects which are expected to be exported with the state 306 * Before importing/saving the actual message from the mobile, a conflict detection should be done 307 * 308 * @param ContentParameters $contentparameters class of objects 309 * @param string $state 310 * 311 * @access public 312 * @return boolean 313 * @throws StatusException 314 */ 315 public function LoadConflicts($contentparameters, $state) { 316 if (!isset($this->session) || !isset($this->store) || !isset($this->folderid)) 317 throw new StatusException("ImportChangesICS->LoadConflicts(): Error, can not load changes for conflict detection. Session, store or folder information not available", SYNC_STATUS_SERVERERROR); 318 319 // save data to load changes later if necessary 320 $this->conflictsLoaded = false; 321 $this->conflictsContentParameters = $contentparameters; 322 $this->conflictsState = $state; 323 324 ZLog::Write(LOGLEVEL_DEBUG, "ImportChangesICS->LoadConflicts(): will be loaded later if necessary"); 325 return true; 326 } 327 328 /** 329 * Potential conflicts are only loaded when really necessary, 330 * e.g. on ADD or MODIFY 331 * 332 * @access private 333 * @return 334 */ 335 private function lazyLoadConflicts() { 336 if (!isset($this->session) || !isset($this->store) || !isset($this->folderid) || 337 !isset($this->conflictsContentParameters) || $this->conflictsState === false) { 338 ZLog::Write(LOGLEVEL_WARN, "ImportChangesICS->lazyLoadConflicts(): can not load potential conflicting changes in lazymode for conflict detection. Missing information"); 339 return false; 340 } 341 342 if (!$this->conflictsLoaded) { 343 ZLog::Write(LOGLEVEL_DEBUG, "ImportChangesICS->lazyLoadConflicts(): loading.."); 344 345 // configure an exporter so we can detect conflicts 346 $exporter = new ExportChangesICS($this->session, $this->store, $this->folderid); 347 $exporter->Config($this->conflictsState); 348 $exporter->ConfigContentParameters($this->conflictsContentParameters); 349 $exporter->InitializeExporter($this->memChanges); 350 351 // monitor how long it takes to export potential conflicts 352 // if this takes "too long" we cancel this operation! 353 $potConflicts = $exporter->GetChangeCount(); 354 if ($potConflicts > 100) { 355 ZLog::Write(LOGLEVEL_WARN, sprintf("ImportChangesICS->lazyLoadConflicts(): conflict detection abandoned as there are too many (%d) changes to be exported.", $potConflicts)); 356 $this->conflictsLoaded = true; 357 return; 358 } 359 $started = time(); 360 $exported = 0; 361 try { 362 while(is_array($exporter->Synchronize())) { 363 $exported++; 364 365 // stop if this takes more than 15 seconds and there are more than 5 changes still to be exported 366 // within 20 seconds this should be finished or it will not be performed 367 if ((time() - $started) > 15 && ($potConflicts - $exported) > 5 ) { 368 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)); 369 $this->conflictsLoaded = true; 370 return; 371 } 372 } 373 } 374 // something really bad happened while exporting changes 375 catch (StatusException $stex) { 376 ZLog::Write(LOGLEVEL_WARN, sprintf("ImportChangesICS->lazyLoadConflicts(): got StatusException code %d while exporting changes. Ignore and mark conflicts as loaded.",$stex->getCode())); 377 } 378 $this->conflictsLoaded = true; 379 } 380 } 381 382 /** 383 * Imports a single message 384 * 385 * @param string $id 386 * @param SyncObject $message 387 * 388 * @access public 389 * @return boolean/string - failure / id of message 390 * @throws StatusException 391 */ 392 public function ImportMessageChange($id, $message) { 393 $flags = 0; 394 $props = array(); 395 $props[PR_PARENT_SOURCE_KEY] = $this->folderid; 396 397 // set the PR_SOURCE_KEY if available or mark it as new message 398 if($id) { 399 list(, $sk) = Utils::SplitMessageId($id); 400 $props[PR_SOURCE_KEY] = hex2bin($sk); 401 402 // check if message is in the synchronization interval and/or shared+private 403 if (!$this->isModificationAllowed($sk)) 404 throw new StatusException(sprintf("ImportChangesICS->ImportMessageChange('%s','%s'): Message modification is not allowed. Data not saved.", $id, get_class($message)), SYNC_STATUS_SYNCCANNOTBECOMPLETED); 405 406 // check for conflicts 407 $this->lazyLoadConflicts(); 408 if($this->memChanges->IsChanged($id)) { 409 if ($this->flags & SYNC_CONFLICT_OVERWRITE_PIM) { 410 // in these cases the status SYNC_STATUS_CONFLICTCLIENTSERVEROBJECT should be returned, so the mobile client can inform the end user 411 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); 412 return false; 413 } 414 else 415 ZLog::Write(LOGLEVEL_INFO, sprintf("ImportChangesICS->ImportMessageChange('%s','%s'): Conflict detected. Data from Server will be dropped! PIM overwrites server.", $id, get_class($message))); 416 } 417 if($this->memChanges->IsDeleted($id)) { 418 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))); 419 return false; 420 } 421 422 // KOE ZP-990: OL updates the deleted category which causes a race condition if more than one KOE is connected to that user 423 if(ZPush::GetDeviceManager()->IsKoe() && KOE_CAPABILITY_RECEIVEFLAGS && $message instanceof SyncMail && !isset($message->flag) && isset($message->categories)) { 424 // check if the categories changed 425 $mapiCategories = $this->mapiprovider->GetMessageCategories($props[PR_PARENT_SOURCE_KEY], $props[PR_SOURCE_KEY]); 426 if( (empty($message->categories) && empty($mapiCategories)) || 427 (is_array($mapiCategories) && count(array_diff($mapiCategories, $message->categories)) == 0 && count(array_diff($message->categories, $mapiCategories)) == 0)) { 428 ZLog::Write(LOGLEVEL_DEBUG, "ImportChangesICS->ImportMessageChange(): KOE update of flag categories. Ignoring incoming update."); 429 return $id; 430 } 431 } 432 433 } 434 else 435 $flags = SYNC_NEW_MESSAGE; 436 437 if(mapi_importcontentschanges_importmessagechange($this->importer, $props, $flags, $mapimessage)) { 438 $this->mapiprovider->SetMessage($mapimessage, $message); 439 mapi_savechanges($mapimessage); 440 441 if (mapi_last_hresult()) 442 throw new StatusException(sprintf("ImportChangesICS->ImportMessageChange('%s','%s'): Error, mapi_savechanges() failed: 0x%X", $id, get_class($message), mapi_last_hresult()), SYNC_STATUS_SYNCCANNOTBECOMPLETED); 443 444 $sourcekeyprops = mapi_getprops($mapimessage, array (PR_SOURCE_KEY)); 445 446 return $this->prefix . bin2hex($sourcekeyprops[PR_SOURCE_KEY]); 447 } 448 else 449 throw new StatusException(sprintf("ImportChangesICS->ImportMessageChange('%s','%s'): Error updating object: 0x%X", $id, get_class($message), mapi_last_hresult()), SYNC_STATUS_OBJECTNOTFOUND); 450 } 451 452 /** 453 * Imports a deletion. This may conflict if the local object has been modified. 454 * 455 * @param string $id 456 * @param boolean $asSoftDelete (opt) if true, the deletion is exported as "SoftDelete", else as "Remove" - default: false 457 * 458 * @access public 459 * @return boolean 460 */ 461 public function ImportMessageDeletion($id, $asSoftDelete = false) { 462 list(,$sk) = Utils::SplitMessageId($id); 463 464 // check if message is in the synchronization interval and/or shared+private 465 if (!$this->isModificationAllowed($sk)) 466 throw new StatusException(sprintf("ImportChangesICS->ImportMessageDeletion('%s'): Message deletion is not allowed. Deletion not executed.", $id), SYNC_STATUS_OBJECTNOTFOUND); 467 468 // check for conflicts 469 $this->lazyLoadConflicts(); 470 if($this->memChanges->IsChanged($id)) { 471 ZLog::Write(LOGLEVEL_INFO, sprintf("ImportChangesICS->ImportMessageDeletion('%s'): Conflict detected. Data from Server will be dropped! PIM deleted object.", $id)); 472 } 473 elseif($this->memChanges->IsDeleted($id)) { 474 ZLog::Write(LOGLEVEL_INFO, sprintf("ImportChangesICS->ImportMessageDeletion('%s'): Conflict detected. Data is already deleted. Request will be ignored.", $id)); 475 return true; 476 } 477 478 // do a 'soft' delete so people can un-delete if necessary 479 if(mapi_importcontentschanges_importmessagedeletion($this->importer, 1, array(hex2bin($sk)))) 480 throw new StatusException(sprintf("ImportChangesICS->ImportMessageDeletion('%s'): Error updating object: 0x%X", $sk, mapi_last_hresult()), SYNC_STATUS_OBJECTNOTFOUND); 481 482 return true; 483 } 484 485 /** 486 * Imports a change in 'read' flag 487 * This can never conflict 488 * 489 * @param string $id 490 * @param int $flags - read/unread 491 * @param array $categories 492 * 493 * @access public 494 * @return boolean 495 * @throws StatusException 496 */ 497 public function ImportMessageReadFlag($id, $flags, $categories = array()) { 498 list($fsk,$sk) = Utils::SplitMessageId($id); 499 500 // if $fsk is set, we convert it into a backend id. 501 if ($fsk) { 502 $fsk = ZPush::GetDeviceManager()->GetBackendIdForFolderId($fsk); 503 } 504 505 // read flag change for our current folder 506 if ($this->folderidHex == $fsk || empty($fsk)) { 507 508 // check if it is in the synchronization interval and/or shared+private 509 if (!$this->isModificationAllowed($sk)) 510 throw new StatusException(sprintf("ImportChangesICS->ImportMessageReadFlag('%s','%d'): Flag update is not allowed. Flags not updated.", $id, $flags), SYNC_STATUS_OBJECTNOTFOUND); 511 512 // check for conflicts 513 /* 514 * Checking for conflicts is correct at this point, but is a very expensive operation. 515 * If the message was deleted, only an error will be shown. 516 * 517 $this->lazyLoadConflicts(); 518 if($this->memChanges->IsDeleted($id)) { 519 ZLog::Write(LOGLEVEL_INFO, sprintf("ImportChangesICS->ImportMessageReadFlag('%s'): Conflict detected. Data is already deleted. Request will be ignored.", $id)); 520 return true; 521 } 522 */ 523 524 $readstate = array ( "sourcekey" => hex2bin($sk), "flags" => $flags); 525 526 if(!mapi_importcontentschanges_importperuserreadstatechange($this->importer, array($readstate) )) 527 throw new StatusException(sprintf("ImportChangesICS->ImportMessageReadFlag('%s','%d'): Error setting read state: 0x%X", $id, $flags, mapi_last_hresult()), SYNC_STATUS_OBJECTNOTFOUND); 528 } 529 // yeah OL sucks - ZP-779 530 else { 531 if (!$fsk) { 532 throw new StatusException(sprintf("ImportChangesICS->ImportMessageReadFlag('%s','%d'): Error setting read state. The message is in another folder but id is unknown as no short folder id is available. Please remove your device states to fully resync your device. Operation ignored.", $id, $flags), SYNC_STATUS_OBJECTNOTFOUND); 533 } 534 $store = ZPush::GetBackend()->GetMAPIStoreForFolderId(ZPush::GetAdditionalSyncFolderStore($fsk), $fsk); 535 $entryid = mapi_msgstore_entryidfromsourcekey($store, hex2bin($fsk), hex2bin($sk)); 536 $realMessage = mapi_msgstore_openentry($store, $entryid); 537 $flag = 0; 538 if ($flags == 0) 539 $flag |= CLEAR_READ_FLAG; 540 $p = mapi_message_setreadflag($realMessage, $flag); 541 ZLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->ImportMessageReadFlag('%s','%d'): setting readflag on message: 0x%X", $id, $flags, mapi_last_hresult())); 542 } 543 return true; 544 } 545 546 /** 547 * Imports a move of a message. This occurs when a user moves an item to another folder 548 * 549 * Normally, we would implement this via the 'offical' importmessagemove() function on the ICS importer, 550 * but the Zarafa/Kopano importer does not support this. Therefore we currently implement it via a standard mapi 551 * call. This causes a mirror 'add/delete' to be sent to the PDA at the next sync. 552 * Manfred, 2010-10-21. For some mobiles import was causing duplicate messages in the destination folder 553 * (Mantis #202). Therefore we will create a new message in the destination folder, copy properties 554 * of the source message to the new one and then delete the source message. 555 * 556 * @param string $id 557 * @param string $newfolder destination folder 558 * 559 * @access public 560 * @return boolean 561 * @throws StatusException 562 */ 563 public function ImportMessageMove($id, $newfolder) { 564 list(,$sk) = Utils::SplitMessageId($id); 565 if (strtolower($newfolder) == strtolower(bin2hex($this->folderid)) ) 566 throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, source and destination are equal", $id, $newfolder), SYNC_MOVEITEMSSTATUS_SAMESOURCEANDDEST); 567 568 // Get the entryid of the message we're moving 569 $entryid = mapi_msgstore_entryidfromsourcekey($this->store, $this->folderid, hex2bin($sk)); 570 $srcmessage = false; 571 572 if ($entryid) { 573 //open the source message 574 $srcmessage = mapi_msgstore_openentry($this->store, $entryid); 575 } 576 577 if(!$entryid || !$srcmessage) { 578 $code = SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID; 579 $mapiLastHresult = mapi_last_hresult(); 580 // if we move to the trash and the source message is not found, we can also just tell the mobile that we successfully moved to avoid errors (ZP-624) 581 if ($newfolder == ZPush::GetBackend()->GetWasteBasket()) { 582 $code = SYNC_MOVEITEMSSTATUS_SUCCESS; 583 } 584 $errorCase = !$entryid ? "resolve source message id" : "open source message"; 585 throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to %s: 0x%X", $sk, $newfolder, $errorCase, $mapiLastHresult), $code); 586 } 587 588 // check if it is in the synchronization interval and/or shared+private 589 if (!$this->isModificationAllowed($sk)) 590 throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Source message move is not allowed. Move not performed.", $id, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID); 591 592 // get correct mapi store for the destination folder 593 $dststore = ZPush::GetBackend()->GetMAPIStoreForFolderId(ZPush::GetAdditionalSyncFolderStore($newfolder), $newfolder); 594 if ($dststore === false) 595 throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to open store of destination folder", $sk, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDDESTID); 596 597 $dstentryid = mapi_msgstore_entryidfromsourcekey($dststore, hex2bin($newfolder)); 598 if(!$dstentryid) 599 throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to resolve destination folder", $sk, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDDESTID); 600 601 $dstfolder = mapi_msgstore_openentry($dststore, $dstentryid); 602 if(!$dstfolder) 603 throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to open destination folder", $sk, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDDESTID); 604 605 $newmessage = mapi_folder_createmessage($dstfolder); 606 if (!$newmessage) 607 throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to create message in destination folder: 0x%X", $sk, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_INVALIDDESTID); 608 609 // Copy message 610 mapi_copyto($srcmessage, array(), array(), $newmessage); 611 if (mapi_last_hresult()) 612 throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, copy to destination message failed: 0x%X", $sk, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_CANNOTMOVE); 613 614 $srcfolderentryid = mapi_msgstore_entryidfromsourcekey($this->store, $this->folderid); 615 if(!$srcfolderentryid) 616 throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to resolve source folder", $sk, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID); 617 618 $srcfolder = mapi_msgstore_openentry($this->store, $srcfolderentryid); 619 if (!$srcfolder) 620 throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to open source folder: 0x%X", $sk, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID); 621 622 // Save changes 623 mapi_savechanges($newmessage); 624 if (mapi_last_hresult()) 625 throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, mapi_savechanges() failed: 0x%X", $sk, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_CANNOTMOVE); 626 627 // Delete the old message 628 if (!mapi_folder_deletemessages($srcfolder, array($entryid), DELETE_HARD_DELETE)) 629 throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, delete of source message failed: 0x%X. Possible duplicates.", $sk, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_SOURCEORDESTLOCKED); 630 631 $sourcekeyprops = mapi_getprops($newmessage, array (PR_SOURCE_KEY)); 632 if (isset($sourcekeyprops[PR_SOURCE_KEY]) && $sourcekeyprops[PR_SOURCE_KEY]) { 633 $prefix = ""; 634 // prepend the destination short folderid, if it exists 635 $destShortId = ZPush::GetDeviceManager()->GetFolderIdForBackendId($newfolder); 636 if ($destShortId !== $newfolder) { 637 $prefix = $destShortId .":"; 638 } 639 return $prefix . bin2hex($sourcekeyprops[PR_SOURCE_KEY]); 640 } 641 642 return false; 643 } 644 645 646 /**---------------------------------------------------------------------------------------------------------- 647 * Methods for HierarchyExporter 648 */ 649 650 /** 651 * Imports a change on a folder 652 * 653 * @param object $folder SyncFolder 654 * 655 * @access public 656 * @return boolean|SyncFolder false on error or a SyncFolder object with serverid and BackendId set (if available) 657 * @throws StatusException 658 */ 659 public function ImportFolderChange($folder) { 660 $id = isset($folder->BackendId)?$folder->BackendId : false; 661 $parent = $folder->parentid; 662 $parent_org = $folder->parentid; 663 $displayname = u2wi($folder->displayname); 664 $type = $folder->type; 665 666 if (Utils::IsSystemFolder($type)) 667 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); 668 669 // create a new folder if $id is not set 670 if (!$id) { 671 // the root folder is "0" - get IPM_SUBTREE 672 if ($parent == "0") { 673 $parentprops = mapi_getprops($this->store, array(PR_IPM_SUBTREE_ENTRYID, PR_IPM_PUBLIC_FOLDERS_ENTRYID)); 674 if (ZPush::GetBackend()->GetImpersonatedUser() == 'system' && isset($parentprops[PR_IPM_PUBLIC_FOLDERS_ENTRYID])) { 675 $parentfentryid = $parentprops[PR_IPM_PUBLIC_FOLDERS_ENTRYID]; 676 } 677 elseif (isset($parentprops[PR_IPM_SUBTREE_ENTRYID])) { 678 $parentfentryid = $parentprops[PR_IPM_SUBTREE_ENTRYID]; 679 } 680 } 681 else 682 $parentfentryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($parent)); 683 684 if (!$parentfentryid) 685 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); 686 687 $parentfolder = mapi_msgstore_openentry($this->store, $parentfentryid); 688 if (!$parentfolder) 689 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); 690 691 // mapi_folder_createfolder() fails if a folder with this name already exists -> MAPI_E_COLLISION 692 $newfolder = mapi_folder_createfolder($parentfolder, $displayname, ""); 693 if (mapi_last_hresult()) 694 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); 695 696 mapi_setprops($newfolder, array(PR_CONTAINER_CLASS => MAPIUtils::GetContainerClassFromFolderType($type))); 697 698 $props = mapi_getprops($newfolder, array(PR_SOURCE_KEY)); 699 if (isset($props[PR_SOURCE_KEY])) { 700 $folder->BackendId = bin2hex($props[PR_SOURCE_KEY]); 701 $folderOrigin = DeviceManager::FLD_ORIGIN_USER; 702 if (ZPush::GetBackend()->GetImpersonatedUser()) { 703 $folderOrigin = DeviceManager::FLD_ORIGIN_IMPERSONATED; 704 } 705 $folder->serverid = ZPush::GetDeviceManager()->GetFolderIdForBackendId($folder->BackendId, true, $folderOrigin, $folder->displayname); 706 ZLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->ImportFolderChange(): Created folder '%s' with id: '%s' backendid: '%s'", $displayname, $folder->serverid, $folder->BackendId)); 707 return $folder; 708 } 709 else 710 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); 711 } 712 713 // open folder for update 714 $entryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($id)); 715 if (!$entryid) 716 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); 717 718 // check if this is a MAPI default folder 719 if ($this->mapiprovider->IsMAPIDefaultFolder($entryid)) 720 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); 721 722 $mfolder = mapi_msgstore_openentry($this->store, $entryid); 723 if (!$mfolder) 724 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); 725 726 $props = mapi_getprops($mfolder, array(PR_SOURCE_KEY, PR_PARENT_SOURCE_KEY, PR_DISPLAY_NAME, PR_CONTAINER_CLASS)); 727 if (!isset($props[PR_SOURCE_KEY]) || !isset($props[PR_PARENT_SOURCE_KEY]) || !isset($props[PR_DISPLAY_NAME]) || !isset($props[PR_CONTAINER_CLASS])) 728 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); 729 730 // get the real parent source key from mapi 731 if ($parent == "0") { 732 $parentprops = mapi_getprops($this->store, array(PR_IPM_SUBTREE_ENTRYID, PR_IPM_PUBLIC_FOLDERS_ENTRYID)); 733 if (ZPush::GetBackend()->GetImpersonatedUser() == 'system') { 734 $parentfentryid = $parentprops[PR_IPM_PUBLIC_FOLDERS_ENTRYID]; 735 } 736 else { 737 $parentfentryid = $parentprops[PR_IPM_SUBTREE_ENTRYID]; 738 } 739 $mapifolder = mapi_msgstore_openentry($this->store, $parentfentryid); 740 741 $rootfolderprops = mapi_getprops($mapifolder, array(PR_SOURCE_KEY)); 742 $parent = bin2hex($rootfolderprops[PR_SOURCE_KEY]); 743 ZLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->ImportFolderChange(): resolved AS parent '0' to sourcekey '%s'", $parent)); 744 } 745 746 // a changed parent id means that the folder should be moved 747 if (bin2hex($props[PR_PARENT_SOURCE_KEY]) !== $parent) { 748 $sourceparentfentryid = mapi_msgstore_entryidfromsourcekey($this->store, $props[PR_PARENT_SOURCE_KEY]); 749 if(!$sourceparentfentryid) 750 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); 751 752 $sourceparentfolder = mapi_msgstore_openentry($this->store, $sourceparentfentryid); 753 if(!$sourceparentfolder) 754 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); 755 756 $destparentfentryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($parent)); 757 if(!$sourceparentfentryid) 758 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); 759 760 $destfolder = mapi_msgstore_openentry($this->store, $destparentfentryid); 761 if(!$destfolder) 762 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); 763 764 // mapi_folder_copyfolder() fails if a folder with this name already exists -> MAPI_E_COLLISION 765 if(! mapi_folder_copyfolder($sourceparentfolder, $entryid, $destfolder, $displayname, FOLDER_MOVE)) 766 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); 767 768 // the parent changed, but we got a backendID as parent and have to return an AS folderid - the parent-backendId must be mapped at this point already 769 if ($folder->parentid != 0) { 770 $folder->parentid = ZPush::GetDeviceManager()->GetFolderIdForBackendId($parent); 771 } 772 ZLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->ImportFolderChange(): Moved folder '%s' with id: %s/%s from: %s to: %s/%s", $displayname, $folder->serverid, $folder->BackendId, bin2hex($props[PR_PARENT_SOURCE_KEY]), $folder->parentid, $parent_org)); 773 774 return $folder; 775 } 776 777 // update the display name 778 $props = array(PR_DISPLAY_NAME => $displayname); 779 mapi_setprops($mfolder, $props); 780 mapi_savechanges($mfolder); 781 if (mapi_last_hresult()) 782 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); 783 784 ZLog::Write(LOGLEVEL_DEBUG, "Imported changes for folder: $id"); 785 return true; 786 } 787 788 /** 789 * Imports a folder deletion 790 * 791 * @param SyncFolder $folder at least "serverid" needs to be set 792 * 793 * @access public 794 * @return int SYNC_FOLDERHIERARCHY_STATUS 795 * @throws StatusException 796 */ 797 public function ImportFolderDeletion($folder) { 798 $id = $folder->BackendId; 799 $parent = isset($folder->parentid) ? $folder->parentid : false; 800 ZLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->ImportFolderDeletion('%s','%s'): importing folder deletetion", $id, $parent)); 801 802 $folderentryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($id)); 803 if(!$folderentryid) 804 throw new StatusException(sprintf("ImportChangesICS->ImportFolderDeletion('%s','%s'): Error, unable to resolve folder", $id, $parent, mapi_last_hresult()), SYNC_FSSTATUS_FOLDERDOESNOTEXIST); 805 806 // get the folder type from the MAPIProvider 807 $type = $this->mapiprovider->GetFolderType($folderentryid); 808 809 if (Utils::IsSystemFolder($type) || $this->mapiprovider->IsMAPIDefaultFolder($folderentryid)) 810 throw new StatusException(sprintf("ImportChangesICS->ImportFolderDeletion('%s','%s'): Error deleting system/default folder", $id, $parent), SYNC_FSSTATUS_SYSTEMFOLDER); 811 812 $ret = mapi_importhierarchychanges_importfolderdeletion ($this->importer, 0, array(PR_SOURCE_KEY => hex2bin($id))); 813 if (!$ret) 814 throw new StatusException(sprintf("ImportChangesICS->ImportFolderDeletion('%s','%s'): Error deleting folder: 0x%X", $id, $parent, mapi_last_hresult()), SYNC_FSSTATUS_SERVERERROR); 815 816 return $ret; 817 } 818} 819