source: trunk/zpush/backend/zarafa/importer.php @ 7589

Revision 7589, 34.9 KB checked in by douglas, 11 years ago (diff)

Ticket #3209 - Integrar módulo de sincronização Z-push ao Expresso

Line 
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 - 2012 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 with the following additional
20* term according to sec. 7:
21*
22* According to sec. 7 of the GNU Affero General Public License, version 3,
23* the terms of the AGPL are supplemented with the following terms:
24*
25* "Zarafa" is a registered trademark of Zarafa B.V.
26* "Z-Push" is a registered trademark of Zarafa Deutschland GmbH
27* The licensing of the Program under the AGPL does not imply a trademark license.
28* Therefore any rights, title and interest in our trademarks remain entirely with us.
29*
30* However, if you propagate an unmodified version of the Program you are
31* allowed to use the term "Z-Push" to indicate that you distribute the Program.
32* Furthermore you may use our trademarks where it is necessary to indicate
33* the intended purpose of a product or service provided you use it in accordance
34* with honest practices in industrial or commercial matters.
35* If you want to propagate modified versions of the Program under the name "Z-Push",
36* you may only do so if you have a written permission by Zarafa Deutschland GmbH
37* (to acquire a permission please contact Zarafa at trademark@zarafa.com).
38*
39* This program is distributed in the hope that it will be useful,
40* but WITHOUT ANY WARRANTY; without even the implied warranty of
41* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
42* GNU Affero General Public License for more details.
43*
44* You should have received a copy of the GNU Affero General Public License
45* along with this program.  If not, see <http://www.gnu.org/licenses/>.
46*
47* Consult LICENSE file for details
48************************************************/
49
50
51
52/**
53 * This is our local importer. Tt receives data from the PDA, for contents and hierarchy changes.
54 * It must therefore receive the incoming data and convert it into MAPI objects, and then send
55 * them to the ICS importer to do the actual writing of the object.
56 * The creation of folders is fairly trivial, because folders that are created on
57 * the PDA are always e-mail folders.
58 */
59
60class ImportChangesICS implements IImportChanges {
61    private $folderid;
62    private $store;
63    private $session;
64    private $flags;
65    private $statestream;
66    private $importer;
67    private $memChanges;
68    private $mapiprovider;
69    private $conflictsLoaded;
70    private $conflictsContentParameters;
71    private $conflictsState;
72    private $cutoffdate;
73    private $contentClass;
74
75    /**
76     * Constructor
77     *
78     * @param mapisession       $session
79     * @param mapistore         $store
80     * @param string            $folderid (opt)
81     *
82     * @access public
83     * @throws StatusException
84     */
85    public function ImportChangesICS($session, $store, $folderid = false) {
86        $this->session = $session;
87        $this->store = $store;
88        $this->folderid = $folderid;
89        $this->conflictsLoaded = false;
90        $this->cutoffdate = false;
91        $this->contentClass = false;
92
93        if ($folderid) {
94            $entryid = mapi_msgstore_entryidfromsourcekey($store, $folderid);
95        }
96        else {
97            $storeprops = mapi_getprops($store, array(PR_IPM_SUBTREE_ENTRYID));
98            $entryid = $storeprops[PR_IPM_SUBTREE_ENTRYID];
99        }
100
101        $folder = false;
102        if ($entryid)
103            $folder = mapi_msgstore_openentry($store, $entryid);
104
105        if(!$folder) {
106            $this->importer = false;
107
108            // We throw an general error SYNC_FSSTATUS_CODEUNKNOWN (12) which is also SYNC_STATUS_FOLDERHIERARCHYCHANGED (12)
109            // if this happened while doing content sync, the mobile will try to resync the folderhierarchy
110            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);
111        }
112
113        $this->mapiprovider = new MAPIProvider($this->session, $this->store);
114
115        if ($folderid)
116            $this->importer = mapi_openproperty($folder, PR_COLLECTOR, IID_IExchangeImportContentsChanges, 0 , 0);
117        else
118            $this->importer = mapi_openproperty($folder, PR_COLLECTOR, IID_IExchangeImportHierarchyChanges, 0 , 0);
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     * Checks if a message is in the synchronization interval (window)
223     * if a filter (e.g. Sync items two weeks back) or limits this synchronization.
224     * These checks only apply to Emails and Appointments only, Contacts, Tasks and Notes do not have time restrictions.
225     *
226     * @param string     $messageid        the message id to be checked
227     *
228     * @access private
229     * @return boolean
230     */
231    private function isMessageInSyncInterval($messageid) {
232        // if there is no restriciton we do not need to check
233        if ($this->cutoffdate === false)
234            return true;
235
236        ZLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->isMessageInSyncInterval('%s'): cut off date is: %s", $messageid, $this->cutoffdate));
237
238        $entryid = mapi_msgstore_entryidfromsourcekey($this->store, $this->folderid, hex2bin($messageid));
239        if(!$entryid) {
240            ZLog::Write(LOGLEVEL_WARN, sprintf("ImportChangesICS->isMessageInSyncInterval('%s'): Error, unable to resolve message id: 0x%X", $messageid, mapi_last_hresult()));
241            return false;
242        }
243
244        $mapimessage = mapi_msgstore_openentry($this->store, $entryid);
245        if(!$mapimessage) {
246            ZLog::Write(LOGLEVEL_WARN, sprintf("ImportChangesICS->isMessageInSyncInterval('%s'): Error, unable to open entry id: 0x%X", $messageid, mapi_last_hresult()));
247            return false;
248        }
249
250        if ($this->contentClass == "Email")
251            return MAPIUtils::IsInEmailSyncInterval($this->store, $mapimessage, $this->cutoffdate);
252        elseif ($this->contentClass == "Calendar")
253            return MAPIUtils::IsInCalendarSyncInterval($this->store, $mapimessage, $this->cutoffdate);
254
255        return true;
256    }
257
258    /**----------------------------------------------------------------------------------------------------------
259     * Methods for ContentsExporter
260     */
261
262    /**
263     * Loads objects which are expected to be exported with the state
264     * Before importing/saving the actual message from the mobile, a conflict detection should be done
265     *
266     * @param ContentParameters         $contentparameters         class of objects
267     * @param string                    $state
268     *
269     * @access public
270     * @return boolean
271     * @throws StatusException
272     */
273    public function LoadConflicts($contentparameters, $state) {
274        if (!isset($this->session) || !isset($this->store) || !isset($this->folderid))
275            throw new StatusException("ImportChangesICS->LoadConflicts(): Error, can not load changes for conflict detection. Session, store or folder information not available", SYNC_STATUS_SERVERERROR);
276
277        // save data to load changes later if necessary
278        $this->conflictsLoaded = false;
279        $this->conflictsContentParameters = $contentparameters;
280        $this->conflictsState = $state;
281
282        ZLog::Write(LOGLEVEL_DEBUG, "ImportChangesICS->LoadConflicts(): will be loaded later if necessary");
283        return true;
284    }
285
286    /**
287     * Potential conflicts are only loaded when really necessary,
288     * e.g. on ADD or MODIFY
289     *
290     * @access private
291     * @return
292     */
293    private function lazyLoadConflicts() {
294        if (!isset($this->session) || !isset($this->store) || !isset($this->folderid) ||
295            !isset($this->conflictsContentParameters) || $this->conflictsState === false) {
296            ZLog::Write(LOGLEVEL_WARN, "ImportChangesICS->lazyLoadConflicts(): can not load potential conflicting changes in lazymode for conflict detection. Missing information");
297            return false;
298        }
299
300        if (!$this->conflictsLoaded) {
301            ZLog::Write(LOGLEVEL_DEBUG, "ImportChangesICS->lazyLoadConflicts(): loading..");
302
303            // configure an exporter so we can detect conflicts
304            $exporter = new ExportChangesICS($this->session, $this->store, $this->folderid);
305            $exporter->Config($this->conflictsState);
306            $exporter->ConfigContentParameters($this->conflictsContentParameters);
307            $exporter->InitializeExporter($this->memChanges);
308
309            // monitor how long it takes to export potential conflicts
310            // if this takes "too long" we cancel this operation!
311            $potConflicts = $exporter->GetChangeCount();
312            $started = time();
313            $exported = 0;
314            while(is_array($exporter->Synchronize())) {
315                $exported++;
316
317                // stop if this takes more than 15 seconds and there are more than 5 changes still to be exported
318                // within 20 seconds this should be finished or it will not be performed
319                if ((time() - $started) > 15 && ($potConflicts - $exported) > 5 ) {
320                    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));
321                    $this->conflictsLoaded = true;
322                    return;
323                }
324            }
325            $this->conflictsLoaded = true;
326        }
327    }
328
329    /**
330     * Imports a single message
331     *
332     * @param string        $id
333     * @param SyncObject    $message
334     *
335     * @access public
336     * @return boolean/string - failure / id of message
337     * @throws StatusException
338     */
339    public function ImportMessageChange($id, $message) {
340        $parentsourcekey = $this->folderid;
341        if($id)
342            $sourcekey = hex2bin($id);
343
344        $flags = 0;
345        $props = array();
346        $props[PR_PARENT_SOURCE_KEY] = $parentsourcekey;
347
348        // set the PR_SOURCE_KEY if available or mark it as new message
349        if($id) {
350            $props[PR_SOURCE_KEY] = $sourcekey;
351
352            // on editing an existing message, check if it is in the synchronization interval
353            if (!$this->isMessageInSyncInterval($id))
354                throw new StatusException(sprintf("ImportChangesICS->ImportMessageChange('%s','%s'): Message is outside the sync interval. Data not saved.", $id, get_class($message)), SYNC_STATUS_SYNCCANNOTBECOMPLETED);
355
356            // check for conflicts
357            $this->lazyLoadConflicts();
358            if($this->memChanges->IsChanged($id)) {
359                if ($this->flags & SYNC_CONFLICT_OVERWRITE_PIM) {
360                    // in these cases the status SYNC_STATUS_CONFLICTCLIENTSERVEROBJECT should be returned, so the mobile client can inform the end user
361                    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);
362                    return false;
363                }
364                else
365                    ZLog::Write(LOGLEVEL_INFO, sprintf("ImportChangesICS->ImportMessageChange('%s','%s'): Conflict detected. Data from Server will be dropped! PIM overwrites server.", $id, get_class($message)));
366            }
367            if($this->memChanges->IsDeleted($id)) {
368                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)));
369                return false;
370            }
371        }
372        else
373            $flags = SYNC_NEW_MESSAGE;
374
375        if(mapi_importcontentschanges_importmessagechange($this->importer, $props, $flags, $mapimessage)) {
376            $this->mapiprovider->SetMessage($mapimessage, $message);
377            mapi_message_savechanges($mapimessage);
378
379            if (mapi_last_hresult())
380                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);
381
382            $sourcekeyprops = mapi_getprops($mapimessage, array (PR_SOURCE_KEY));
383            return bin2hex($sourcekeyprops[PR_SOURCE_KEY]);
384        }
385        else
386            throw new StatusException(sprintf("ImportChangesICS->ImportMessageChange('%s','%s'): Error updating object: 0x%X", $id, get_class($message), mapi_last_hresult()), SYNC_STATUS_OBJECTNOTFOUND);
387    }
388
389    /**
390     * Imports a deletion. This may conflict if the local object has been modified
391     *
392     * @param string        $id
393     *
394     * @access public
395     * @return boolean
396     * @throws StatusException
397     */
398    public function ImportMessageDeletion($id) {
399        // check if the message is in the current syncinterval
400        if (!$this->isMessageInSyncInterval($id))
401            throw new StatusException(sprintf("ImportChangesICS->ImportMessageDeletion('%s'): Message is outside the sync interval and so far not deleted.", $id), SYNC_STATUS_OBJECTNOTFOUND);
402
403        // check for conflicts
404        $this->lazyLoadConflicts();
405        if($this->memChanges->IsChanged($id)) {
406            ZLog::Write(LOGLEVEL_INFO, sprintf("ImportChangesICS->ImportMessageDeletion('%s'): Conflict detected. Data from Server will be dropped! PIM deleted object.", $id));
407        }
408        elseif($this->memChanges->IsDeleted($id)) {
409            ZLog::Write(LOGLEVEL_INFO, sprintf("ImportChangesICS->ImportMessageDeletion('%s'): Conflict detected. Data is already deleted. Request will be ignored.", $id));
410            return true;
411        }
412
413        // do a 'soft' delete so people can un-delete if necessary
414        if(mapi_importcontentschanges_importmessagedeletion($this->importer, 1, array(hex2bin($id))))
415            throw new StatusException(sprintf("ImportChangesICS->ImportMessageDeletion('%s'): Error updating object: 0x%X", $id, mapi_last_hresult()), SYNC_STATUS_OBJECTNOTFOUND);
416
417        return true;
418    }
419
420    /**
421     * Imports a change in 'read' flag
422     * This can never conflict
423     *
424     * @param string        $id
425     * @param int           $flags - read/unread
426     *
427     * @access public
428     * @return boolean
429     * @throws StatusException
430     */
431    public function ImportMessageReadFlag($id, $flags) {
432        // check if the message is in the current syncinterval
433        if (!$this->isMessageInSyncInterval($id))
434            throw new StatusException(sprintf("ImportChangesICS->ImportMessageReadFlag('%s','%d'): Message is outside the sync interval. Flags not updated.", $id, $flags), SYNC_STATUS_OBJECTNOTFOUND);
435
436        // check for conflicts
437        /*
438         * Checking for conflicts is correct at this point, but is a very expensive operation.
439         * If the message was deleted, only an error will be shown.
440         *
441        $this->lazyLoadConflicts();
442        if($this->memChanges->IsDeleted($id)) {
443            ZLog::Write(LOGLEVEL_INFO, sprintf("ImportChangesICS->ImportMessageReadFlag('%s'): Conflict detected. Data is already deleted. Request will be ignored.", $id));
444            return true;
445        }
446         */
447
448        $readstate = array ( "sourcekey" => hex2bin($id), "flags" => $flags);
449
450        if(!mapi_importcontentschanges_importperuserreadstatechange($this->importer, array($readstate) ))
451            throw new StatusException(sprintf("ImportChangesICS->ImportMessageReadFlag('%s','%d'): Error setting read state: 0x%X", $id, $flags, mapi_last_hresult()), SYNC_STATUS_OBJECTNOTFOUND);
452
453        return true;
454    }
455
456    /**
457     * Imports a move of a message. This occurs when a user moves an item to another folder
458     *
459     * Normally, we would implement this via the 'offical' importmessagemove() function on the ICS importer,
460     * but the Zarafa importer does not support this. Therefore we currently implement it via a standard mapi
461     * call. This causes a mirror 'add/delete' to be sent to the PDA at the next sync.
462     * Manfred, 2010-10-21. For some mobiles import was causing duplicate messages in the destination folder
463     * (Mantis #202). Therefore we will create a new message in the destination folder, copy properties
464     * of the source message to the new one and then delete the source message.
465     *
466     * @param string        $id
467     * @param string        $newfolder      destination folder
468     *
469     * @access public
470     * @return boolean
471     * @throws StatusException
472     */
473    public function ImportMessageMove($id, $newfolder) {
474        if (strtolower($newfolder) == strtolower(bin2hex($this->folderid)) )
475            throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, source and destination are equal", $id, $newfolder), SYNC_MOVEITEMSSTATUS_SAMESOURCEANDDEST);
476
477        // check if the source message is in the current syncinterval
478        if (!$this->isMessageInSyncInterval($id))
479            throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Source message is outside the sync interval. Move not performed.", $id, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID);
480
481        // Get the entryid of the message we're moving
482        $entryid = mapi_msgstore_entryidfromsourcekey($this->store, $this->folderid, hex2bin($id));
483        if(!$entryid)
484            throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to resolve source message id", $id, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID);
485
486        //open the source message
487        $srcmessage = mapi_msgstore_openentry($this->store, $entryid);
488        if (!$srcmessage)
489            throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to open source message: 0x%X", $id, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID);
490
491        // get correct mapi store for the destination folder
492        $dststore = ZPush::GetBackend()->GetMAPIStoreForFolderId(ZPush::GetAdditionalSyncFolderStore($newfolder), $newfolder);
493        if ($dststore === false)
494            throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to open store of destination folder", $id, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDDESTID);
495
496        $dstentryid = mapi_msgstore_entryidfromsourcekey($dststore, hex2bin($newfolder));
497        if(!$dstentryid)
498            throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to resolve destination folder", $id, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDDESTID);
499
500        $dstfolder = mapi_msgstore_openentry($dststore, $dstentryid);
501        if(!$dstfolder)
502            throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to open destination folder", $id, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDDESTID);
503
504        $newmessage = mapi_folder_createmessage($dstfolder);
505        if (!$newmessage)
506            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);
507
508        // Copy message
509        mapi_copyto($srcmessage, array(), array(), $newmessage);
510        if (mapi_last_hresult())
511            throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, copy to destination message failed: 0x%X", $id, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_CANNOTMOVE);
512
513        $srcfolderentryid = mapi_msgstore_entryidfromsourcekey($this->store, $this->folderid);
514        if(!$srcfolderentryid)
515            throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to resolve source folder", $id, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID);
516
517        $srcfolder = mapi_msgstore_openentry($this->store, $srcfolderentryid);
518        if (!$srcfolder)
519            throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to open source folder: 0x%X", $id, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID);
520
521        // Save changes
522        mapi_savechanges($newmessage);
523        if (mapi_last_hresult())
524            throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, mapi_savechanges() failed: 0x%X", $id, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_CANNOTMOVE);
525
526        // Delete the old message
527        if (!mapi_folder_deletemessages($srcfolder, array($entryid)))
528            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);
529
530        $sourcekeyprops = mapi_getprops($newmessage, array (PR_SOURCE_KEY));
531        if (isset($sourcekeyprops[PR_SOURCE_KEY]) && $sourcekeyprops[PR_SOURCE_KEY])
532            return  bin2hex($sourcekeyprops[PR_SOURCE_KEY]);
533
534        return false;
535    }
536
537
538    /**----------------------------------------------------------------------------------------------------------
539     * Methods for HierarchyExporter
540     */
541
542    /**
543     * Imports a change on a folder
544     *
545     * @param object        $folder     SyncFolder
546     *
547     * @access public
548     * @return string       id of the folder
549     * @throws StatusException
550     */
551    public function ImportFolderChange($folder) {
552        $id = isset($folder->serverid)?$folder->serverid:false;
553        $parent = $folder->parentid;
554        $displayname = u2wi($folder->displayname);
555        $type = $folder->type;
556
557        if (Utils::IsSystemFolder($type))
558            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);
559
560        // create a new folder if $id is not set
561        if (!$id) {
562            // the root folder is "0" - get IPM_SUBTREE
563            if ($parent == "0") {
564                $parentprops = mapi_getprops($this->store, array(PR_IPM_SUBTREE_ENTRYID));
565                if (isset($parentprops[PR_IPM_SUBTREE_ENTRYID]))
566                    $parentfentryid = $parentprops[PR_IPM_SUBTREE_ENTRYID];
567            }
568            else
569                $parentfentryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($parent));
570
571            if (!$parentfentryid)
572                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);
573
574            $parentfolder = mapi_msgstore_openentry($this->store, $parentfentryid);
575            if (!$parentfolder)
576                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);
577
578            //  mapi_folder_createfolder() fails if a folder with this name already exists -> MAPI_E_COLLISION
579            $newfolder = mapi_folder_createfolder($parentfolder, $displayname, "");
580            if (mapi_last_hresult())
581                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);
582
583            mapi_setprops($newfolder, array(PR_CONTAINER_CLASS => MAPIUtils::GetContainerClassFromFolderType($type)));
584
585            $props =  mapi_getprops($newfolder, array(PR_SOURCE_KEY));
586            if (isset($props[PR_SOURCE_KEY])) {
587                $sourcekey = bin2hex($props[PR_SOURCE_KEY]);
588                ZLog::Write(LOGLEVEL_DEBUG, sprintf("Created folder '%s' with id: '%s'", $displayname, $sourcekey));
589                return $sourcekey;
590            }
591            else
592                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);
593            return false;
594        }
595
596        // open folder for update
597        $entryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($id));
598        if (!$entryid)
599            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);
600
601        // check if this is a MAPI default folder
602        if ($this->mapiprovider->IsMAPIDefaultFolder($entryid))
603            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);
604
605        $mfolder = mapi_msgstore_openentry($this->store, $entryid);
606        if (!$mfolder)
607            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);
608
609        $props =  mapi_getprops($mfolder, array(PR_SOURCE_KEY, PR_PARENT_SOURCE_KEY, PR_DISPLAY_NAME, PR_CONTAINER_CLASS));
610        if (!isset($props[PR_SOURCE_KEY]) || !isset($props[PR_PARENT_SOURCE_KEY]) || !isset($props[PR_DISPLAY_NAME]) || !isset($props[PR_CONTAINER_CLASS]))
611            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);
612
613        // get the real parent source key from mapi
614        if ($parent == "0") {
615            $parentprops = mapi_getprops($this->store, array(PR_IPM_SUBTREE_ENTRYID));
616            $parentfentryid = $parentprops[PR_IPM_SUBTREE_ENTRYID];
617            $mapifolder = mapi_msgstore_openentry($this->store, $parentfentryid);
618
619            $rootfolderprops = mapi_getprops($mapifolder, array(PR_SOURCE_KEY));
620            $parent = bin2hex($rootfolderprops[PR_SOURCE_KEY]);
621            ZLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->ImportFolderChange(): resolved AS parent '0' to sourcekey '%s'", $parent));
622        }
623
624        // a changed parent id means that the folder should be moved
625        if (bin2hex($props[PR_PARENT_SOURCE_KEY]) !== $parent) {
626            $sourceparentfentryid = mapi_msgstore_entryidfromsourcekey($this->store, $props[PR_PARENT_SOURCE_KEY]);
627            if(!$sourceparentfentryid)
628                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);
629
630            $sourceparentfolder = mapi_msgstore_openentry($this->store, $sourceparentfentryid);
631            if(!$sourceparentfolder)
632                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);
633
634            $destparentfentryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($parent));
635            if(!$sourceparentfentryid)
636                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);
637
638            $destfolder = mapi_msgstore_openentry($this->store, $destparentfentryid);
639            if(!$destfolder)
640                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);
641
642            // mapi_folder_copyfolder() fails if a folder with this name already exists -> MAPI_E_COLLISION
643            if(! mapi_folder_copyfolder($sourceparentfolder, $entryid, $destfolder, $displayname, FOLDER_MOVE))
644                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);
645
646            $folderProps = mapi_getprops($mfolder, array(PR_SOURCE_KEY));
647            return $folderProps[PR_SOURCE_KEY];
648        }
649
650        // update the display name
651        $props = array(PR_DISPLAY_NAME => $displayname);
652        mapi_setprops($mfolder, $props);
653        mapi_savechanges($mfolder);
654        if (mapi_last_hresult())
655            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);
656
657        ZLog::Write(LOGLEVEL_DEBUG, "Imported changes for folder: $id");
658        return $id;
659    }
660
661    /**
662     * Imports a folder deletion
663     *
664     * @param string        $id
665     * @param string        $parent id is ignored in ICS
666     *
667     * @access public
668     * @return int          SYNC_FOLDERHIERARCHY_STATUS
669     * @throws StatusException
670     */
671    public function ImportFolderDeletion($id, $parent = false) {
672        ZLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->ImportFolderDeletion('%s','%s'): importing folder deletetion", $id, $parent));
673
674        $folderentryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($id));
675        if(!$folderentryid)
676            throw new StatusException(sprintf("ImportChangesICS->ImportFolderDeletion('%s','%s'): Error, unable to resolve folder", $id, $parent, mapi_last_hresult()), SYNC_FSSTATUS_FOLDERDOESNOTEXIST);
677
678        // get the folder type from the MAPIProvider
679        $type = $this->mapiprovider->GetFolderType($folderentryid);
680
681        if (Utils::IsSystemFolder($type) || $this->mapiprovider->IsMAPIDefaultFolder($folderentryid))
682            throw new StatusException(sprintf("ImportChangesICS->ImportFolderDeletion('%s','%s'): Error deleting system/default folder", $id, $parent), SYNC_FSSTATUS_SYSTEMFOLDER);
683
684        $ret = mapi_importhierarchychanges_importfolderdeletion ($this->importer, 0, array(PR_SOURCE_KEY => hex2bin($id)));
685        if (!$ret)
686            throw new StatusException(sprintf("ImportChangesICS->ImportFolderDeletion('%s','%s'): Error deleting folder: 0x%X", $id, $parent, mapi_last_hresult()), SYNC_FSSTATUS_SERVERERROR);
687
688        return $ret;
689    }
690}
691?>
Note: See TracBrowser for help on using the repository browser.