Back to index

d-push  2.0
zarafa.php
Go to the documentation of this file.
00001 <?php
00002 /***********************************************
00003 * File      :   zarafa.php
00004 * Project   :   Z-Push
00005 * Descr     :   This is backend for the
00006 *               Zarafa Collaboration Platform (ZCP).
00007 *               It is an implementation of IBackend
00008 *               and also implements the ISearchProvider
00009 *               to search in the Zarafa system.
00010 *
00011 * Created   :   01.10.2011
00012 *
00013 * Copyright 2007 - 2011 Zarafa Deutschland GmbH
00014 *
00015 * This program is free software: you can redistribute it and/or modify
00016 * it under the terms of the GNU Affero General Public License, version 3,
00017 * as published by the Free Software Foundation with the following additional
00018 * term according to sec. 7:
00019 *
00020 * According to sec. 7 of the GNU Affero General Public License, version 3,
00021 * the terms of the AGPL are supplemented with the following terms:
00022 *
00023 * "Zarafa" is a registered trademark of Zarafa B.V.
00024 * "Z-Push" is a registered trademark of Zarafa Deutschland GmbH
00025 * The licensing of the Program under the AGPL does not imply a trademark license.
00026 * Therefore any rights, title and interest in our trademarks remain entirely with us.
00027 *
00028 * However, if you propagate an unmodified version of the Program you are
00029 * allowed to use the term "Z-Push" to indicate that you distribute the Program.
00030 * Furthermore you may use our trademarks where it is necessary to indicate
00031 * the intended purpose of a product or service provided you use it in accordance
00032 * with honest practices in industrial or commercial matters.
00033 * If you want to propagate modified versions of the Program under the name "Z-Push",
00034 * you may only do so if you have a written permission by Zarafa Deutschland GmbH
00035 * (to acquire a permission please contact Zarafa at trademark@zarafa.com).
00036 *
00037 * This program is distributed in the hope that it will be useful,
00038 * but WITHOUT ANY WARRANTY; without even the implied warranty of
00039 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
00040 * GNU Affero General Public License for more details.
00041 *
00042 * You should have received a copy of the GNU Affero General Public License
00043 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
00044 *
00045 * Consult LICENSE file for details
00046 *************************************************/
00047 
00048 // include PHP-MAPI classes
00049 include_once('backend/zarafa/mapi/mapi.util.php');
00050 include_once('backend/zarafa/mapi/mapidefs.php');
00051 include_once('backend/zarafa/mapi/mapitags.php');
00052 include_once('backend/zarafa/mapi/mapicode.php');
00053 include_once('backend/zarafa/mapi/mapiguid.php');
00054 include_once('backend/zarafa/mapi/class.baseexception.php');
00055 include_once('backend/zarafa/mapi/class.mapiexception.php');
00056 include_once('backend/zarafa/mapi/class.baserecurrence.php');
00057 include_once('backend/zarafa/mapi/class.taskrecurrence.php');
00058 include_once('backend/zarafa/mapi/class.recurrence.php');
00059 include_once('backend/zarafa/mapi/class.meetingrequest.php');
00060 include_once('backend/zarafa/mapi/class.freebusypublish.php');
00061 
00062 // processing of RFC822 messages
00063 include_once('include/mimeDecode.php');
00064 require_once('include/z_RFC822.php');
00065 
00066 // components of Zarafa backend
00067 include_once('backend/zarafa/mapiutils.php');
00068 include_once('backend/zarafa/mapimapping.php');
00069 include_once('backend/zarafa/mapiprovider.php');
00070 include_once('backend/zarafa/mapiphpwrapper.php');
00071 include_once('backend/zarafa/mapistreamwrapper.php');
00072 include_once('backend/zarafa/importer.php');
00073 include_once('backend/zarafa/exporter.php');
00074 
00075 
00076 class BackendZarafa implements IBackend, ISearchProvider {
00077     private $mainUser;
00078     private $session;
00079     private $defaultstore;
00080     private $store;
00081     private $storeName;
00082     private $storeCache;
00083     private $importedFolders;
00084     private $notifications;
00085     private $changesSink;
00086     private $changesSinkFolders;
00087     private $changesSinkStores;
00088     private $wastebasket;
00089 
00095     public function BackendZarafa() {
00096         $this->session = false;
00097         $this->store = false;
00098         $this->storeName = false;
00099         $this->storeCache = array();
00100         $this->importedFolders = array();
00101         $this->notifications = false;
00102         $this->changesSink = false;
00103         $this->changesSinkFolders = array();
00104         $this->changesSinkStores = array();
00105         $this->wastebasket = false;
00106 
00107         ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendZarafa using PHP-MAPI version: %s", phpversion("mapi")));
00108     }
00109 
00116     public function GetStateMachine() {
00117         return false;
00118     }
00119 
00127     public function GetSearchProvider() {
00128         return $this;
00129     }
00130 
00137     public function GetSupportedASVersion() {
00138         return ZPush::ASV_14;
00139     }
00140 
00152     public function Logon($user, $domain, $pass) {
00153         ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZarafaBackend->Logon(): Trying to authenticate user '%s'..", $user));
00154         $this->mainUser = $user;
00155 
00156         try {
00157             // check if notifications are available in php-mapi
00158             if(function_exists('mapi_feature') && mapi_feature('LOGONFLAGS')) {
00159                 $this->session = @mapi_logon_zarafa($user, $pass, MAPI_SERVER, null, null, 0);
00160                 $this->notifications = true;
00161             }
00162             // old fashioned session
00163             else {
00164                 $this->session = @mapi_logon_zarafa($user, $pass, MAPI_SERVER);
00165                 $this->notifications = false;
00166             }
00167 
00168             if (mapi_last_hresult())
00169                 ZLog::Write(LOGLEVEL_ERROR, sprintf("ZarafaBackend->Logon(): login failed with error code: 0x%X", mapi_last_hresult()));
00170 
00171         }
00172         catch (MAPIException $ex) {
00173             throw new AuthenticationRequiredException($ex->getDisplayMessage());
00174         }
00175 
00176         if(!$this->session) {
00177             ZLog::Write(LOGLEVEL_WARN, sprintf("ZarafaBackend->Logon(): logon failed for user '%s'", $user));
00178             $this->defaultstore = false;
00179             return false;
00180         }
00181 
00182         // Get/open default store
00183         $this->defaultstore = $this->openMessageStore($user);
00184 
00185         if($this->defaultstore === false)
00186             throw new AuthenticationRequiredException(sprintf("ZarafaBackend->Logon(): User '%s' has no default store", $user));
00187 
00188         $this->store = $this->defaultstore;
00189         $this->storeName = $user;
00190 
00191         ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZarafaBackend->Logon(): User '%s' is authenticated",$user));
00192 
00193         // check if this is a Zarafa 7 store with unicode support
00194         MAPIUtils::IsUnicodeStore($this->store);
00195         return true;
00196     }
00197 
00217     public function Setup($store, $checkACLonly = false, $folderid = false) {
00218         list($user, $domain) = Utils::SplitDomainUser($store);
00219 
00220         if (!isset($this->mainUser))
00221             return false;
00222 
00223         if ($user === false)
00224             $user = $this->mainUser;
00225 
00226         // This is a special case. A user will get it's entire folder structure by the foldersync by default.
00227         // The ACL check is executed when an additional folder is going to be sent to the mobile.
00228         // Configured that way the user could receive the same folderid twice, with two different names.
00229         if ($this->mainUser == $user && $checkACLonly && $folderid) {
00230             ZLog::Write(LOGLEVEL_DEBUG, "ZarafaBackend->Setup(): Checking ACLs for folder of the users defaultstore. Fail is forced to avoid folder duplications on mobile.");
00231             return false;
00232         }
00233 
00234         // get the users store
00235         $userstore = $this->openMessageStore($user);
00236 
00237         // only proceed if a store was found, else return false
00238         if ($userstore) {
00239             // only check permissions
00240             if ($checkACLonly == true) {
00241                 // check for admin rights
00242                 if (!$folderid) {
00243                     if ($user != $this->mainUser) {
00244                         $zarafauserinfo = @mapi_zarafa_getuser_by_name($this->defaultstore, $this->mainUser);
00245                         $admin = (isset($zarafauserinfo['admin']) && $zarafauserinfo['admin'])?true:false;
00246                     }
00247                     // the user has always full access to his own store
00248                     else
00249                         $admin = true;
00250 
00251                     ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZarafaBackend->Setup(): Checking for admin ACLs on store '%s': '%s'", $user, Utils::PrintAsString($admin)));
00252                     return $admin;
00253                 }
00254                 // check 'secretary' permissions on this folder
00255                 else {
00256                     $rights = $this->hasSecretaryACLs($userstore, $folderid);
00257                     ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZarafaBackend->Setup(): Checking for secretary ACLs on '%s' of store '%s': '%s'", $folderid, $user, Utils::PrintAsString($rights)));
00258                     return $rights;
00259                 }
00260             }
00261 
00262             // switch operations store
00263             // this should also be done if called with user = mainuser or user = false
00264             // which means to switch back to the default store
00265             else {
00266                 // switch active store
00267                 $this->store = $userstore;
00268                 $this->storeName = $user;
00269                 return true;
00270             }
00271         }
00272         return false;
00273     }
00274 
00283     public function Logoff() {
00284         // update if the calendar which received incoming changes
00285         foreach($this->importedFolders as $folderid => $store) {
00286             // open the root of the store
00287             $storeprops = mapi_getprops($store, array(PR_USER_ENTRYID));
00288             $root = mapi_msgstore_openentry($store);
00289             if (!$root)
00290                 continue;
00291 
00292             // get the entryid of the main calendar of the store and the calendar to be published
00293             $rootprops = mapi_getprops($root, array(PR_IPM_APPOINTMENT_ENTRYID));
00294             $entryid = mapi_msgstore_entryidfromsourcekey($store, hex2bin($folderid));
00295 
00296             // only publish free busy for the main calendar
00297             if(isset($rootprops[PR_IPM_APPOINTMENT_ENTRYID]) && $rootprops[PR_IPM_APPOINTMENT_ENTRYID] == $entryid) {
00298                 ZLog::Write(LOGLEVEL_INFO, sprintf("ZarafaBackend->Logoff(): Updating freebusy information on folder id '%s'", $folderid));
00299                 $calendar = mapi_msgstore_openentry($store, $entryid);
00300 
00301                 $pub = new FreeBusyPublish($this->session, $store, $calendar, $storeprops[PR_USER_ENTRYID]);
00302                 $pub->publishFB(time() - (7 * 24 * 60 * 60), 6 * 30 * 24 * 60 * 60); // publish from one week ago, 6 months ahead
00303             }
00304         }
00305 
00306         return true;
00307     }
00308 
00318     public function GetHierarchy() {
00319         $folders = array();
00320         $importer = false;
00321         $mapiprovider = new MAPIProvider($this->session, $this->store);
00322 
00323         $rootfolder = mapi_msgstore_openentry($this->store);
00324         $rootfolderprops = mapi_getprops($rootfolder, array(PR_SOURCE_KEY));
00325         $rootfoldersourcekey = bin2hex($rootfolderprops[PR_SOURCE_KEY]);
00326 
00327         $hierarchy =  mapi_folder_gethierarchytable($rootfolder, CONVENIENT_DEPTH);
00328         $rows = mapi_table_queryallrows($hierarchy, array(PR_ENTRYID));
00329 
00330         foreach ($rows as $row) {
00331             $mapifolder = mapi_msgstore_openentry($this->store, $row[PR_ENTRYID]);
00332             $folder = $mapiprovider->GetFolder($mapifolder);
00333 
00334             if (isset($folder->parentid) && $folder->parentid != $rootfoldersourcekey)
00335                 $folders[] = $folder;
00336         }
00337 
00338         return $folders;
00339     }
00340 
00350     public function GetImporter($folderid = false) {
00351         ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendZarafa->GetImporter() folderid: '%s'", Utils::PrintAsString($folderid)));
00352         if($folderid !== false) {
00353             // check if the user of the current store has permissions to import to this folderid
00354             if ($this->storeName != $this->mainUser && !$this->hasSecretaryACLs($this->store, $folderid)) {
00355                 ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendZarafa->GetImporter(): missing permissions on folderid: '%s'.", Utils::PrintAsString($folderid)));
00356                 return false;
00357             }
00358             $this->importedFolders[$folderid] = $this->store;
00359             return new ImportChangesICS($this->session, $this->store, hex2bin($folderid));
00360         }
00361         else
00362             return new ImportChangesICS($this->session, $this->store);
00363     }
00364 
00375     public function GetExporter($folderid = false) {
00376         if($folderid !== false) {
00377             // check if the user of the current store has permissions to export from this folderid
00378             if ($this->storeName != $this->mainUser && !$this->hasSecretaryACLs($this->store, $folderid)) {
00379                 ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendZarafa->GetExporter(): missing permissions on folderid: '%s'.", Utils::PrintAsString($folderid)));
00380                 return false;
00381             }
00382             return new ExportChangesICS($this->session, $this->store, hex2bin($folderid));
00383         }
00384         else
00385             return new ExportChangesICS($this->session, $this->store);
00386     }
00387 
00398     public function SendMail($sm) {
00399         ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZarafaBackend->SendMail(): RFC822: %d bytes  forward-id: '%s' reply-id: '%s' parent-id: '%s' SaveInSent: '%s' ReplaceMIME: '%s'",
00400                                             strlen($sm->mime), Utils::PrintAsString($sm->forwardflag), Utils::PrintAsString($sm->replyflag),
00401                                             Utils::PrintAsString((isset($sm->source->folderid) ? $sm->source->folderid : false)),
00402                                             Utils::PrintAsString(($sm->saveinsent)), Utils::PrintAsString(isset($sm->replacemime)) ));
00403 
00404         // by splitting the message in several lines we can easily grep later
00405         foreach(preg_split("/((\r)?\n)/", $sm->mime) as $rfc822line)
00406             ZLog::Write(LOGLEVEL_WBXML, "RFC822: ". $rfc822line);
00407 
00408         $mimeParams = array('decode_headers' => true,
00409                             'decode_bodies' => true,
00410                             'include_bodies' => true,
00411                             'charset' => 'utf-8');
00412 
00413         $mimeObject = new Mail_mimeDecode($sm->mime);
00414         $message = $mimeObject->decode($mimeParams);
00415 
00416         $sendMailProps = MAPIMapping::GetSendMailProperties();
00417         $sendMailProps = getPropIdsFromStrings($this->store, $sendMailProps);
00418 
00419         // Open the outbox and create the message there
00420         $storeprops = mapi_getprops($this->store, array($sendMailProps["outboxentryid"], $sendMailProps["ipmsentmailentryid"]));
00421         if(isset($storeprops[$sendMailProps["outboxentryid"]]))
00422             $outbox = mapi_msgstore_openentry($this->store, $storeprops[$sendMailProps["outboxentryid"]]);
00423 
00424         if(!$outbox)
00425             throw new StatusException(sprintf("ZarafaBackend->SendMail(): No Outbox found or unable to create message: 0x%X", mapi_last_hresult()), SYNC_COMMONSTATUS_SERVERERROR);
00426 
00427         $mapimessage = mapi_folder_createmessage($outbox);
00428 
00429         //message properties to be set
00430         $mapiprops = array();
00431         // only save the outgoing in sent items folder if the mobile requests it
00432         $mapiprops[$sendMailProps["sentmailentryid"]] = $storeprops[$sendMailProps["ipmsentmailentryid"]];
00433 
00434         // Check if imtomapi function is available and use it to send the mime message.
00435         // It is available since ZCP 7.0.6
00436         // @see http://jira.zarafa.com/browse/ZCP-9508
00437         if(function_exists('mapi_feature') && mapi_feature('INETMAPI_IMTOMAPI')) {
00438             ZLog::Write(LOGLEVEL_DEBUG, "Use the mapi_inetmapi_imtomapi function");
00439             $ab = mapi_openaddressbook($this->session);
00440             mapi_inetmapi_imtomapi($this->session, $this->store, $ab, $mapimessage, $sm->mime, array());
00441 
00442             // Delete the PR_SENT_REPRESENTING_* properties because some android devices
00443             // do not send neither From nor Sender header causing empty PR_SENT_REPRESENTING_NAME and
00444             // PR_SENT_REPRESENTING_EMAIL_ADDRESS properties and "broken" PR_SENT_REPRESENTING_ENTRYID
00445             // which results in spooler not being able to send the message.
00446             // @see http://jira.zarafa.com/browse/ZP-85
00447             mapi_deleteprops($mapimessage,
00448                 array(  $sendMailProps["sentrepresentingname"], $sendMailProps["sentrepresentingemail"], $sendMailProps["representingentryid"],
00449                         $sendMailProps["sentrepresentingaddt"], $sendMailProps["sentrepresentinsrchk"]));
00450 
00451             if(isset($sm->source->itemid) && $sm->source->itemid) {
00452                 $entryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($sm->source->folderid), hex2bin($sm->source->itemid));
00453                 if ($entryid)
00454                     $fwmessage = mapi_msgstore_openentry($this->store, $entryid);
00455 
00456                 if(!isset($fwmessage) || !$fwmessage)
00457                     throw new StatusException(sprintf("ZarafaBackend->SendMail(): Could not open message id '%s' in folder id '%s' to be replied/forwarded: 0x%X", $sm->source->itemid, $sm->source->folderid, mapi_last_hresult()), SYNC_COMMONSTATUS_ITEMNOTFOUND);
00458 
00459                 //update icon when forwarding or replying message
00460                 if ($sm->forwardflag) mapi_setprops($fwmessage, array(PR_ICON_INDEX=>262));
00461                 elseif ($sm->replyflag) mapi_setprops($fwmessage, array(PR_ICON_INDEX=>261));
00462                 mapi_savechanges($fwmessage);
00463 
00464                 // only attach the original message if the mobile does not send it itself
00465                 if (!isset($sm->replacemime)) {
00466                     // get message's body in order to append forward or reply text
00467                     $body = MAPIUtils::readPropStream($mapimessage, PR_BODY);
00468                     $bodyHtml = MAPIUtils::readPropStream($mapimessage, PR_HTML);
00469                     if($sm->forwardflag) {
00470                         // attach the original attachments to the outgoing message
00471                         $this->copyAttachments($mapimessage, $fwmessage);
00472                     }
00473 
00474                     if (strlen($body) > 0) {
00475                         $fwbody = MAPIUtils::readPropStream($fwmessage, PR_BODY);
00476                         $mapiprops[$sendMailProps["body"]] = $body."\r\n\r\n".$fwbody;
00477                     }
00478 
00479                     if (strlen($bodyHtml) > 0) {
00480                         $fwbodyHtml = MAPIUtils::readPropStream($fwmessage, PR_HTML);
00481                         $mapiprops[$sendMailProps["html"]] = $bodyHtml."<br><br>".$fwbodyHtml;
00482                     }
00483                 }
00484             }
00485 
00486             mapi_setprops($mapimessage, $mapiprops);
00487             mapi_message_savechanges($mapimessage);
00488             mapi_message_submitmessage($mapimessage);
00489             $hr = mapi_last_hresult();
00490 
00491             if ($hr)
00492                 throw new StatusException(sprintf("ZarafaBackend->SendMail(): Error saving/submitting the message to the Outbox: 0x%X", mapi_last_hresult()), SYNC_COMMONSTATUS_MAILSUBMISSIONFAILED);
00493 
00494             ZLog::Write(LOGLEVEL_DEBUG, "ZarafaBackend->SendMail(): email submitted");
00495             return true;
00496         }
00497 
00498         $mapiprops[$sendMailProps["subject"]] = u2wi(isset($message->headers["subject"])?$message->headers["subject"]:"");
00499         $mapiprops[$sendMailProps["messageclass"]] = "IPM.Note";
00500         $mapiprops[$sendMailProps["deliverytime"]] = time();
00501 
00502         if(isset($message->headers["x-priority"])) {
00503             $this->getImportanceAndPriority($message->headers["x-priority"], $mapiprops, $sendMailProps);
00504         }
00505 
00506         $this->addRecipients($message->headers, $mapimessage);
00507 
00508         // Loop through message subparts.
00509         $body = "";
00510         $body_html = "";
00511         if($message->ctype_primary == "multipart" && ($message->ctype_secondary == "mixed" || $message->ctype_secondary == "alternative")) {
00512             $mparts = $message->parts;
00513             for($i=0; $i<count($mparts); $i++) {
00514                 $part = $mparts[$i];
00515 
00516                 // palm pre & iPhone send forwarded messages in another subpart which are also parsed
00517                 if($part->ctype_primary == "multipart" && ($part->ctype_secondary == "mixed" || $part->ctype_secondary == "alternative"  || $part->ctype_secondary == "related")) {
00518                     foreach($part->parts as $spart)
00519                         $mparts[] = $spart;
00520                     continue;
00521                 }
00522 
00523                 // standard body
00524                 if($part->ctype_primary == "text" && $part->ctype_secondary == "plain" && isset($part->body) && (!isset($part->disposition) || $part->disposition != "attachment")) {
00525                     $body .= u2wi($part->body); // assume only one text body
00526                 }
00527                 // html body
00528                 elseif($part->ctype_primary == "text" && $part->ctype_secondary == "html") {
00529                     $body_html .= u2wi($part->body);
00530                 }
00531                 // TNEF
00532                 elseif($part->ctype_primary == "ms-tnef" || $part->ctype_secondary == "ms-tnef") {
00533                     if (!isset($tnefAndIcalProps)) {
00534                         $tnefAndIcalProps = MAPIMapping::GetTnefAndIcalProperties();
00535                         $tnefAndIcalProps = getPropIdsFromStrings($this->store, $tnefAndIcalProps);
00536                     }
00537 
00538                     require_once('tnefparser.php');
00539                     $zptnef = new TNEFParser($this->store, $tnefAndIcalProps);
00540 
00541                     $zptnef->ExtractProps($part->body, $mapiprops);
00542                     if (is_array($mapiprops) && !empty($mapiprops)) {
00543                         //check if it is a recurring item
00544                         if (isset($mapiprops[$tnefAndIcalProps["tnefrecurr"]])) {
00545                             MAPIUtils::handleRecurringItem($mapiprops, $tnefAndIcalProps);
00546                         }
00547                     }
00548                     else ZLog::Write(LOGLEVEL_WARN, "ZarafaBackend->Sendmail(): TNEFParser: Mapi property array was empty");
00549                 }
00550                 // iCalendar
00551                 elseif($part->ctype_primary == "text" && $part->ctype_secondary == "calendar") {
00552                     if (!isset($tnefAndIcalProps)) {
00553                         $tnefAndIcalProps = MAPIMapping::GetTnefAndIcalProperties();
00554                         $tnefAndIcalProps = getPropIdsFromStrings($this->store, $tnefAndIcalProps);
00555                     }
00556 
00557                     require_once('icalparser.php');
00558                     $zpical = new ICalParser($this->store, $tnefAndIcalProps);
00559                     $zpical->ExtractProps($part->body, $mapiprops);
00560 
00561                     // iPhone sends a second ICS which we ignore if we can
00562                     if (!isset($mapiprops[PR_MESSAGE_CLASS]) && strlen(trim($body)) == 0) {
00563                         ZLog::Write(LOGLEVEL_WARN, "ZarafaBackend->Sendmail(): Secondary iPhone response is being ignored!! Mail dropped!");
00564                         return true;
00565                     }
00566 
00567                     if (! Utils::CheckMapiExtVersion("6.30") && is_array($mapiprops) && !empty($mapiprops)) {
00568                         mapi_setprops($mapimessage, $mapiprops);
00569                     }
00570                     else {
00571                         // store ics as attachment
00572                         //see Utils::IcalTimezoneFix() in utils.php for more information
00573                         $part->body = Utils::IcalTimezoneFix($part->body);
00574                         MAPIUtils::StoreAttachment($mapimessage, $part);
00575                         ZLog::Write(LOGLEVEL_INFO, "ZarafaBackend->Sendmail(): Sending ICS file as attachment");
00576                     }
00577                 }
00578                 // any other type, store as attachment
00579                 else
00580                     MAPIUtils::StoreAttachment($mapimessage, $part);
00581             }
00582         }
00583         // html main body
00584         else if($message->ctype_primary == "text" && $message->ctype_secondary == "html") {
00585             $body_html .= u2wi($message->body);
00586         }
00587         // standard body
00588         else {
00589             $body = u2wi($message->body);
00590         }
00591 
00592         // some devices only transmit a html body
00593         if (strlen($body) == 0 && strlen($body_html) > 0) {
00594             ZLog::Write(LOGLEVEL_WARN, "ZarafaBackend->SendMail(): only html body sent, transformed into plain text");
00595             $body = strip_tags($body_html);
00596         }
00597 
00598         if(isset($sm->source->itemid) && $sm->source->itemid) {
00599             // Append the original text body for reply/forward
00600 
00601             $entryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($sm->source->folderid), hex2bin($sm->source->itemid));
00602             if ($entryid)
00603                 $fwmessage = mapi_msgstore_openentry($this->store, $entryid);
00604 
00605             if(!isset($fwmessage) || !$fwmessage)
00606                 throw new StatusException(sprintf("ZarafaBackend->SendMail(): Could not open message id '%s' in folder id '%s' to be replied/forwarded: 0x%X", $sm->source->itemid, $sm->source->folderid, mapi_last_hresult()), SYNC_COMMONSTATUS_ITEMNOTFOUND);
00607 
00608             //update icon when forwarding or replying message
00609             if ($sm->forwardflag) mapi_setprops($fwmessage, array(PR_ICON_INDEX=>262));
00610             elseif ($sm->replyflag) mapi_setprops($fwmessage, array(PR_ICON_INDEX=>261));
00611             mapi_savechanges($fwmessage);
00612 
00613             // only attach the original message if the mobile does not send it itself
00614             if (!isset($sm->replacemime)) {
00615                 $fwbody = MAPIUtils::readPropStream($fwmessage, PR_BODY);
00616                 $fwbodyHtml = MAPIUtils::readPropStream($fwmessage, PR_HTML);
00617 
00618                 if($sm->forwardflag) {
00619                     // During a forward, we have to add the forward header ourselves. This is because
00620                     // normally the forwarded message is added as an attachment. However, we don't want this
00621                     // because it would be rather complicated to copy over the entire original message due
00622                     // to the lack of IMessage::CopyTo ..
00623                     $fwheader = $this->getForwardHeaders($fwmessage);
00624 
00625                     // add fwheader to body and body_html
00626                     $body .= $fwheader;
00627                     if (strlen($body_html) > 0)
00628                         $body_html .= str_ireplace("\r\n", "<br>", $fwheader);
00629 
00630                     // attach the original attachments to the outgoing message
00631                     $this->copyAttachments($mapimessage, $fwmessage);
00632                 }
00633 
00634                 if(strlen($body) > 0)
00635                     $body .= $fwbody;
00636 
00637                 if (strlen($body_html) > 0)
00638                     $body_html .= $fwbodyHtml;
00639             }
00640         }
00641 
00642         //set PR_INTERNET_CPID to 65001 (utf-8) if store supports it and to 1252 otherwise
00643         $internetcpid = INTERNET_CPID_WINDOWS1252;
00644         if (defined('STORE_SUPPORTS_UNICODE') && STORE_SUPPORTS_UNICODE == true) {
00645             $internetcpid = INTERNET_CPID_UTF8;
00646         }
00647 
00648         $mapiprops[$sendMailProps["body"]] = $body;
00649         $mapiprops[$sendMailProps["internetcpid"]] = $internetcpid;
00650 
00651 
00652         if(strlen($body_html) > 0){
00653             $mapiprops[$sendMailProps["html"]] = $body_html;
00654         }
00655 
00656         //TODO if setting all properties fails, try setting them infividually like in mapiprovider
00657         mapi_setprops($mapimessage, $mapiprops);
00658 
00659         mapi_savechanges($mapimessage);
00660         mapi_message_submitmessage($mapimessage);
00661 
00662         if(mapi_last_hresult())
00663             throw new StatusException(sprintf("ZarafaBackend->SendMail(): Error saving/submitting the message to the Outbox: 0x%X", mapi_last_hresult()), SYNC_COMMONSTATUS_MAILSUBMISSIONFAILED);
00664 
00665         ZLog::Write(LOGLEVEL_DEBUG, "ZarafaBackend->SendMail(): email submitted");
00666         return true;
00667     }
00668 
00680     public function Fetch($folderid, $id, $contentparameters) {
00681         // get the entry id of the message
00682         $entryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($folderid), hex2bin($id));
00683         if(!$entryid)
00684             throw new StatusException(sprintf("BackendZarafa->Fetch('%s','%s'): Error getting entryid: 0x%X", $folderid, $id, mapi_last_hresult()), SYNC_STATUS_OBJECTNOTFOUND);
00685 
00686         // open the message
00687         $message = mapi_msgstore_openentry($this->store, $entryid);
00688         if(!$message)
00689             throw new StatusException(sprintf("BackendZarafa->Fetch('%s','%s'): Error, unable to open message: 0x%X", $folderid, $id, mapi_last_hresult()), SYNC_STATUS_OBJECTNOTFOUND);
00690 
00691         // convert the mapi message into a SyncObject and return it
00692         $mapiprovider = new MAPIProvider($this->session, $this->store);
00693 
00694         // override truncation
00695         $contentparameters->SetTruncation(SYNC_TRUNCATION_ALL);
00696         // TODO check for body preferences
00697         return $mapiprovider->GetMessage($message, $contentparameters);
00698     }
00699 
00706     public function GetWasteBasket() {
00707         if ($this->wastebasket) {
00708             return $this->wastebasket;
00709         }
00710 
00711         $storeprops = mapi_getprops($this->defaultstore, array(PR_IPM_WASTEBASKET_ENTRYID));
00712         if (isset($storeprops[PR_IPM_WASTEBASKET_ENTRYID])) {
00713             $wastebasket = mapi_msgstore_openentry($this->store, $storeprops[PR_IPM_WASTEBASKET_ENTRYID]);
00714             $wastebasketprops = mapi_getprops($wastebasket, array(PR_SOURCE_KEY));
00715             if (isset($wastebasketprops[PR_SOURCE_KEY])) {
00716                 $this->wastebasket = bin2hex($wastebasketprops[PR_SOURCE_KEY]);
00717                 ZLog::Write(LOGLEVEL_DEBUG, sprintf("Got waste basket with id '%s'", $this->wastebasket));
00718                 return $this->wastebasket;
00719             }
00720         }
00721         return false;
00722     }
00723 
00732     public function GetAttachmentData($attname) {
00733         ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZarafaBackend->GetAttachmentData('%s')", $attname));
00734         list($id, $attachnum) = explode(":", $attname);
00735 
00736         if(!isset($id) || !isset($attachnum))
00737             throw new StatusException(sprintf("ZarafaBackend->GetAttachmentData('%s'): Error, attachment requested for non-existing item", $attname), SYNC_ITEMOPERATIONSSTATUS_INVALIDATT);
00738 
00739         $entryid = hex2bin($id);
00740         $message = mapi_msgstore_openentry($this->store, $entryid);
00741         if(!$message)
00742             throw new StatusException(sprintf("ZarafaBackend->GetAttachmentData('%s'): Error, unable to open item for attachment data for id '%s' with: 0x%X", $attname, $id, mapi_last_hresult()), SYNC_ITEMOPERATIONSSTATUS_INVALIDATT);
00743 
00744         $attach = mapi_message_openattach($message, $attachnum);
00745         if(!$attach)
00746             throw new StatusException(sprintf("ZarafaBackend->GetAttachmentData('%s'): Error, unable to open attachment number '%s' with: 0x%X", $attname, $attachnum, mapi_last_hresult()), SYNC_ITEMOPERATIONSSTATUS_INVALIDATT);
00747 
00748         $stream = mapi_openpropertytostream($attach, PR_ATTACH_DATA_BIN);
00749         if(!$stream)
00750             throw new StatusException(sprintf("ZarafaBackend->GetAttachmentData('%s'): Error, unable to open attachment data stream: 0x%X", $attname, mapi_last_hresult()), SYNC_ITEMOPERATIONSSTATUS_INVALIDATT);
00751 
00752         //get the mime type of the attachment
00753         $contenttype = mapi_getprops($attach, array(PR_ATTACH_MIME_TAG, PR_ATTACH_MIME_TAG_W));
00754         $attachment = new SyncItemOperationsAttachment();
00755         // put the mapi stream into a wrapper to get a standard stream
00756         $attachment->data = MapiStreamWrapper::Open($stream);
00757         if (isset($contenttype[PR_ATTACH_MIME_TAG]))
00758             $attachment->contenttype = $contenttype[PR_ATTACH_MIME_TAG];
00759         elseif (isset($contenttype[PR_ATTACH_MIME_TAG_W]))
00760             $attachment->contenttype = $contenttype[PR_ATTACH_MIME_TAG_W];
00761             //TODO default contenttype
00762         return $attachment;
00763     }
00764 
00765 
00778     public function EmptyFolder($folderid, $includeSubfolders = true) {
00779         $folderentryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($folderid));
00780         if (!$folderentryid)
00781             throw new StatusException(sprintf("BackendZarafa->EmptyFolder('%s','%s'): Error, unable to open folder (no entry id)", $folderid, Utils::PrintAsString($includeSubfolders)), SYNC_ITEMOPERATIONSSTATUS_SERVERERROR);
00782         $folder = mapi_msgstore_openentry($this->store, $folderentryid);
00783 
00784         if (!$folder)
00785             throw new StatusException(sprintf("BackendZarafa->EmptyFolder('%s','%s'): Error, unable to open parent folder (open entry)", $folderid, Utils::PrintAsString($includeSubfolders)), SYNC_ITEMOPERATIONSSTATUS_SERVERERROR);
00786 
00787         $flags = 0;
00788         if ($includeSubfolders)
00789             $flags = DEL_ASSOCIATED;
00790 
00791         ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendZarafa->EmptyFolder('%s','%s'): emptying folder",$folderid, Utils::PrintAsString($includeSubfolders)));
00792 
00793         // empty folder!
00794         mapi_folder_emptyfolder($folder, $flags);
00795         if (mapi_last_hresult())
00796             throw new StatusException(sprintf("BackendZarafa->EmptyFolder('%s','%s'): Error, mapi_folder_emptyfolder() failed: 0x%X", $folderid, Utils::PrintAsString($includeSubfolders), mapi_last_hresult()), SYNC_ITEMOPERATIONSSTATUS_SERVERERROR);
00797 
00798         return true;
00799     }
00800 
00813     public function MeetingResponse($requestid, $folderid, $response) {
00814         // Use standard meeting response code to process meeting request
00815         $reqentryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($folderid), hex2bin($requestid));
00816         if (!$reqentryid)
00817             throw new StatusException(sprintf("BackendZarafa->MeetingResponse('%s', '%s', '%s'): Error, unable to entryid of the message 0x%X", $requestid, $folderid, $response, mapi_last_hresult()), SYNC_MEETRESPSTATUS_INVALIDMEETREQ);
00818 
00819         $mapimessage = mapi_msgstore_openentry($this->store, $reqentryid);
00820         if(!$mapimessage)
00821             throw new StatusException(sprintf("BackendZarafa->MeetingResponse('%s','%s', '%s'): Error, unable to open request message for response 0x%X", $requestid, $folderid, $response, mapi_last_hresult()), SYNC_MEETRESPSTATUS_INVALIDMEETREQ);
00822 
00823         $meetingrequest = new Meetingrequest($this->store, $mapimessage);
00824 
00825         if(!$meetingrequest->isMeetingRequest())
00826             throw new StatusException(sprintf("BackendZarafa->MeetingResponse('%s','%s', '%s'): Error, attempt to respond to non-meeting request", $requestid, $folderid, $response), SYNC_MEETRESPSTATUS_INVALIDMEETREQ);
00827 
00828         if($meetingrequest->isLocalOrganiser())
00829             throw new StatusException(sprintf("BackendZarafa->MeetingResponse('%s','%s', '%s'): Error, attempt to response to meeting request that we organized", $requestid, $folderid, $response), SYNC_MEETRESPSTATUS_INVALIDMEETREQ);
00830 
00831         // Process the meeting response. We don't have to send the actual meeting response
00832         // e-mail, because the device will send it itself.
00833         switch($response) {
00834             case 1:     // accept
00835             default:
00836                 $entryid = $meetingrequest->doAccept(false, false, false, false, false, false, true); // last true is the $userAction
00837                 break;
00838             case 2:        // tentative
00839                 $entryid = $meetingrequest->doAccept(true, false, false, false, false, false, true); // last true is the $userAction
00840                 break;
00841             case 3:        // decline
00842                 $meetingrequest->doDecline(false);
00843                 break;
00844         }
00845 
00846         // F/B will be updated on logoff
00847 
00848         // We have to return the ID of the new calendar item, so do that here
00849         if (isset($entryid)) {
00850             $newitem = mapi_msgstore_openentry($this->store, $entryid);
00851             $newprops = mapi_getprops($newitem, array(PR_SOURCE_KEY));
00852             $calendarid = bin2hex($newprops[PR_SOURCE_KEY]);
00853         }
00854 
00855         // on recurring items, the MeetingRequest class responds with a wrong entryid
00856         if ($requestid == $calendarid) {
00857                ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendZarafa->MeetingResponse('%s','%s', '%s'): returned calender id is the same as the requestid - re-searching", $requestid, $folderid, $response));
00858 
00859             $props = MAPIMapping::GetMeetingRequestProperties();
00860             $props = getPropIdsFromStrings($this->store, $props);
00861 
00862             $messageprops = mapi_getprops($mapimessage, Array($props["goidtag"]));
00863             $goid = $messageprops[$props["goidtag"]];
00864 
00865             $items = $meetingrequest->findCalendarItems($goid);
00866 
00867             if (is_array($items)) {
00868                $newitem = mapi_msgstore_openentry($this->store, $items[0]);
00869                $newprops = mapi_getprops($newitem, array(PR_SOURCE_KEY));
00870                $calendarid = bin2hex($newprops[PR_SOURCE_KEY]);
00871                ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendZarafa->MeetingResponse('%s','%s', '%s'): found other calendar entryid", $requestid, $folderid, $response));
00872             }
00873         }
00874 
00875         if ($calendarid == "" || $requestid == $calendarid)
00876             throw new StatusException(sprintf("BackendZarafa->MeetingResponse('%s','%s', '%s'): Error finding the accepted meeting response in the calendar", $requestid, $folderid, $response), SYNC_MEETRESPSTATUS_INVALIDMEETREQ);
00877 
00878         // delete meeting request from Inbox
00879         $folderentryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($folderid));
00880         $folder = mapi_msgstore_openentry($this->store, $folderentryid);
00881         mapi_folder_deletemessages($folder, array($reqentryid), 0);
00882 
00883         return $calendarid;
00884     }
00885 
00895     public function HasChangesSink() {
00896         if (!$this->notifications) {
00897             ZLog::Write(LOGLEVEL_DEBUG, "ZarafaBackend->HasChangesSink(): sink is not available");
00898             return false;
00899         }
00900 
00901         $this->changesSink = @mapi_sink_create();
00902 
00903         if (! $this->changesSink) {
00904             ZLog::Write(LOGLEVEL_DEBUG, "ZarafaBackend->HasChangesSink(): sink could not be created");
00905             return false;
00906         }
00907 
00908         ZLog::Write(LOGLEVEL_DEBUG, "ZarafaBackend->HasChangesSink(): created");
00909         return true;
00910     }
00911 
00922     public function ChangesSinkInitialize($folderid) {
00923         ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZarafaBackend->ChangesSinkInitialize(): folderid '%s'", $folderid));
00924 
00925         $entryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($folderid));
00926         if (!$entryid)
00927             return false;
00928 
00929         // add entryid to the monitored folders
00930         $this->changesSinkFolders[$entryid] = $folderid;
00931 
00932         // check if this store is already monitored, else advise it
00933         if (!in_array($this->store, $this->changesSinkStores)) {
00934             mapi_msgstore_advise($this->store, null, fnevObjectModified | fnevObjectCreated | fnevObjectMoved | fnevObjectDeleted, $this->changesSink);
00935             $this->changesSinkStores[] = $this->store;
00936             ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZarafaBackend->ChangesSinkInitialize(): advised store '%s'", $this->store));
00937         }
00938         return true;
00939     }
00940 
00952     public function ChangesSink($timeout = 30) {
00953         $notifications = array();
00954         $sinkresult = @mapi_sink_timedwait($this->changesSink, $timeout * 1000);
00955         foreach ($sinkresult as $sinknotif) {
00956             // check if something in the monitored folders changed
00957             if (isset($sinknotif['parentid']) && array_key_exists($sinknotif['parentid'], $this->changesSinkFolders)) {
00958                 $notifications[] = $this->changesSinkFolders[$sinknotif['parentid']];
00959             }
00960             // deletes and moves
00961             if (isset($sinknotif['oldparentid']) && array_key_exists($sinknotif['oldparentid'], $this->changesSinkFolders)) {
00962                 $notifications[] = $this->changesSinkFolders[$sinknotif['oldparentid']];
00963             }
00964         }
00965         return $notifications;
00966     }
00967 
00976     public function Settings($settings) {
00977         if ($settings instanceof SyncOOF) {
00978             $this->settingsOOF($settings);
00979         }
00980 
00981         if ($settings instanceof SyncUserInformation) {
00982             $this->settingsUserInformation($settings);
00983         }
00984 
00985         return $settings;
00986     }
00987 
00988 
01002     public function SupportsType($searchtype) {
01003         return ($searchtype == ISearchProvider::SEARCH_GAL) || ($searchtype == ISearchProvider::SEARCH_MAILBOX);
01004     }
01005 
01017     public function GetGALSearchResults($searchquery, $searchrange){
01018         // only return users from who the displayName or the username starts with $name
01019         //TODO: use PR_ANR for this restriction instead of PR_DISPLAY_NAME and PR_ACCOUNT
01020         $addrbook = mapi_openaddressbook($this->session);
01021         if ($addrbook)
01022             $ab_entryid = mapi_ab_getdefaultdir($addrbook);
01023         if ($ab_entryid)
01024             $ab_dir = mapi_ab_openentry($addrbook, $ab_entryid);
01025         if ($ab_dir)
01026             $table = mapi_folder_getcontentstable($ab_dir);
01027 
01028         if (!$table)
01029             throw new StatusException(sprintf("ZarafaBackend->GetGALSearchResults(): could not open addressbook: 0x%X", mapi_last_hresult()), SYNC_SEARCHSTATUS_STORE_CONNECTIONFAILED);
01030 
01031         $restriction = MAPIUtils::GetSearchRestriction(u2w($searchquery));
01032         mapi_table_restrict($table, $restriction);
01033         mapi_table_sort($table, array(PR_DISPLAY_NAME => TABLE_SORT_ASCEND));
01034 
01035         if (mapi_last_hresult())
01036             throw new StatusException(sprintf("ZarafaBackend->GetGALSearchResults(): could not apply restriction: 0x%X", mapi_last_hresult()), SYNC_SEARCHSTATUS_STORE_TOOCOMPLEX);
01037 
01038         //range for the search results, default symbian range end is 50, wm 99,
01039         //so we'll use that of nokia
01040         $rangestart = 0;
01041         $rangeend = 50;
01042 
01043         if ($searchrange != '0') {
01044             $pos = strpos($searchrange, '-');
01045             $rangestart = substr($searchrange, 0, $pos);
01046             $rangeend = substr($searchrange, ($pos + 1));
01047         }
01048         $items = array();
01049 
01050         $querycnt = mapi_table_getrowcount($table);
01051         //do not return more results as requested in range
01052         $querylimit = (($rangeend + 1) < $querycnt) ? ($rangeend + 1) : $querycnt;
01053         $items['range'] = ($querylimit > 0) ? $rangestart.'-'.($querylimit - 1) : '0-0';
01054         $items['searchtotal'] = $querycnt;
01055         if ($querycnt > 0)
01056             $abentries = mapi_table_queryrows($table, array(PR_ACCOUNT, PR_DISPLAY_NAME, PR_SMTP_ADDRESS, PR_BUSINESS_TELEPHONE_NUMBER, PR_GIVEN_NAME, PR_SURNAME, PR_MOBILE_TELEPHONE_NUMBER, PR_HOME_TELEPHONE_NUMBER, PR_TITLE, PR_COMPANY_NAME, PR_OFFICE_LOCATION), $rangestart, $querylimit);
01057 
01058         for ($i = 0; $i < $querylimit; $i++) {
01059             $items[$i][SYNC_GAL_DISPLAYNAME] = w2u($abentries[$i][PR_DISPLAY_NAME]);
01060 
01061             if (strlen(trim($items[$i][SYNC_GAL_DISPLAYNAME])) == 0)
01062                 $items[$i][SYNC_GAL_DISPLAYNAME] = w2u($abentries[$i][PR_ACCOUNT]);
01063 
01064             $items[$i][SYNC_GAL_ALIAS] = w2u($abentries[$i][PR_ACCOUNT]);
01065             //it's not possible not get first and last name of an user
01066             //from the gab and user functions, so we just set lastname
01067             //to displayname and leave firstname unset
01068             //this was changed in Zarafa 6.40, so we try to get first and
01069             //last name and fall back to the old behaviour if these values are not set
01070             if (isset($abentries[$i][PR_GIVEN_NAME]))
01071                 $items[$i][SYNC_GAL_FIRSTNAME] = w2u($abentries[$i][PR_GIVEN_NAME]);
01072             if (isset($abentries[$i][PR_SURNAME]))
01073                 $items[$i][SYNC_GAL_LASTNAME] = w2u($abentries[$i][PR_SURNAME]);
01074 
01075             if (!isset($items[$i][SYNC_GAL_LASTNAME])) $items[$i][SYNC_GAL_LASTNAME] = $items[$i][SYNC_GAL_DISPLAYNAME];
01076 
01077             $items[$i][SYNC_GAL_EMAILADDRESS] = w2u($abentries[$i][PR_SMTP_ADDRESS]);
01078             //check if an user has an office number or it might produce warnings in the log
01079             if (isset($abentries[$i][PR_BUSINESS_TELEPHONE_NUMBER]))
01080                 $items[$i][SYNC_GAL_PHONE] = w2u($abentries[$i][PR_BUSINESS_TELEPHONE_NUMBER]);
01081             //check if an user has a mobile number or it might produce warnings in the log
01082             if (isset($abentries[$i][PR_MOBILE_TELEPHONE_NUMBER]))
01083                 $items[$i][SYNC_GAL_MOBILEPHONE] = w2u($abentries[$i][PR_MOBILE_TELEPHONE_NUMBER]);
01084             //check if an user has a home number or it might produce warnings in the log
01085             if (isset($abentries[$i][PR_HOME_TELEPHONE_NUMBER]))
01086                 $items[$i][SYNC_GAL_HOMEPHONE] = w2u($abentries[$i][PR_HOME_TELEPHONE_NUMBER]);
01087 
01088             if (isset($abentries[$i][PR_COMPANY_NAME]))
01089                 $items[$i][SYNC_GAL_COMPANY] = w2u($abentries[$i][PR_COMPANY_NAME]);
01090 
01091             if (isset($abentries[$i][PR_TITLE]))
01092                 $items[$i][SYNC_GAL_TITLE] = w2u($abentries[$i][PR_TITLE]);
01093 
01094             if (isset($abentries[$i][PR_OFFICE_LOCATION]))
01095                 $items[$i][SYNC_GAL_OFFICE] = w2u($abentries[$i][PR_OFFICE_LOCATION]);
01096         }
01097         return $items;
01098     }
01099 
01107     public function GetMailboxSearchResults($cpo) {
01108         $searchFolder = $this->getSearchFolder();
01109         $searchRestriction = $this->getSearchRestriction($cpo);
01110         $searchRange = explode('-', $cpo->GetSearchRange());
01111         $searchFolderId = $cpo->GetSearchFolderid();
01112         $searchFolders = array();
01113         // search only in required folders
01114         if (!empty($searchFolderId)) {
01115             $searchFolderEntryId = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($searchFolderId));
01116             $searchFolders[] = $searchFolderEntryId;
01117         }
01118         // if no folder was required then search in the entire store
01119         else {
01120             $tmp = mapi_getprops($this->store, array(PR_ENTRYID,PR_DISPLAY_NAME,PR_IPM_SUBTREE_ENTRYID));
01121             $searchFolders[] = $tmp[PR_IPM_SUBTREE_ENTRYID];
01122         }
01123         $items = array();
01124         $flags = 0;
01125         // if subfolders are required, do a recursive search
01126         if ($cpo->GetSearchDeepTraversal()) {
01127             $flags |= SEARCH_RECURSIVE;
01128         }
01129 
01130         mapi_folder_setsearchcriteria($searchFolder, $searchRestriction, $searchFolders, $flags);
01131 
01132         $table = mapi_folder_getcontentstable($searchFolder);
01133         $searchStart = time();
01134         // do the search and wait for all the results available
01135         while (time() - $searchStart < SEARCH_WAIT) {
01136             $searchcriteria = mapi_folder_getsearchcriteria($searchFolder);
01137             if(($searchcriteria["searchstate"] & SEARCH_REBUILD) == 0)
01138                 break; // Search is done
01139             sleep(1);
01140         }
01141 
01142         // if the search range is set limit the result to it, otherwise return all found messages
01143         $rows = (is_array($searchRange) && isset($searchRange[0]) && isset($searchRange[1])) ?
01144             mapi_table_queryrows($table, array(PR_SOURCE_KEY, PR_PARENT_SOURCE_KEY), $searchRange[0], $searchRange[1] - $searchRange[0] + 1) :
01145             mapi_table_queryrows($table, array(PR_SOURCE_KEY, PR_PARENT_SOURCE_KEY), 0, SEARCH_MAXRESULTS);
01146 
01147         $cnt = count($rows);
01148         $items['searchtotal'] = $cnt;
01149         $items["range"] = $cpo->GetSearchRange();
01150         for ($i = 0; $i < $cnt; $i++) {
01151             $items[$i]['class'] = 'Email';
01152             $items[$i]['longid'] = bin2hex($rows[$i][PR_PARENT_SOURCE_KEY]) . ":" . bin2hex($rows[$i][PR_SOURCE_KEY]);
01153             $items[$i]['folderid'] = bin2hex($rows[$i][PR_PARENT_SOURCE_KEY]);
01154         }
01155         return $items;
01156     }
01157 
01165     public function TerminateSearch($pid) {
01166         ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZarafaBackend->TerminateSearch(): terminating search for pid %d", $pid));
01167         $storeProps = mapi_getprops($this->store, array(PR_STORE_SUPPORT_MASK, PR_FINDER_ENTRYID));
01168         if (($storeProps[PR_STORE_SUPPORT_MASK] & STORE_SEARCH_OK) != STORE_SEARCH_OK) {
01169             ZLog::Write(LOGLEVEL_WARN, "Store doesn't support search folders. Public store doesn't have FINDER_ROOT folder");
01170             return false;
01171         }
01172 
01173         $finderfolder = mapi_msgstore_openentry($this->store, $storeProps[PR_FINDER_ENTRYID]);
01174         if(mapi_last_hresult() != NOERROR) {
01175             ZLog::Write(LOGLEVEL_WARN, sprintf("Unable to open search folder (0x%X)", mapi_last_hresult()));
01176             return false;
01177         }
01178 
01179         $hierarchytable = mapi_folder_gethierarchytable($finderfolder);
01180         mapi_table_restrict($hierarchytable,
01181             array(RES_CONTENT,
01182                 array(
01183                     FUZZYLEVEL      => FL_PREFIX,
01184                     ULPROPTAG       => PR_DISPLAY_NAME,
01185                     VALUE           => array(PR_DISPLAY_NAME=>"Z-Push Search Folder ".$pid)
01186                 )
01187             ),
01188             TBL_BATCH);
01189 
01190         $folders = mapi_table_queryallrows($hierarchytable, array(PR_ENTRYID, PR_DISPLAY_NAME, PR_LAST_MODIFICATION_TIME));
01191         foreach($folders as $folder) {
01192             mapi_folder_deletefolder($finderfolder, $folder[PR_ENTRYID]);
01193         }
01194         return true;
01195     }
01196 
01203     public function Disconnect() {
01204         return true;
01205     }
01206 
01219     public function GetMAPIStoreForFolderId($store, $folderid) {
01220         if ($store == false) {
01221             ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZarafaBackend->GetMAPIStoreForFolderId('%s', '%s'): no store specified, returning default store", $store, $folderid));
01222             return $this->defaultstore;
01223         }
01224 
01225         // setup the correct store
01226         if ($this->Setup($store, false, $folderid)) {
01227             return $this->store;
01228         }
01229         else {
01230             ZLog::Write(LOGLEVEL_WARN, sprintf("ZarafaBackend->GetMAPIStoreForFolderId('%s', '%s'): store is not available", $store, $folderid));
01231             return false;
01232         }
01233     }
01234 
01235 
01249     private function openMessageStore($user) {
01250         // During PING requests the operations store has to be switched constantly
01251         // the cache prevents the same store opened several times
01252         if (isset($this->storeCache[$user]))
01253            return  $this->storeCache[$user];
01254 
01255         $entryid = false;
01256         $return_public = false;
01257 
01258         if (strtoupper($user) == 'SYSTEM')
01259             $return_public = true;
01260 
01261         // loop through the storestable if authenticated user of public folder
01262         if ($user == $this->mainUser || $return_public === true) {
01263             // Find the default store
01264             $storestables = mapi_getmsgstorestable($this->session);
01265             $result = mapi_last_hresult();
01266 
01267             if ($result == NOERROR){
01268                 $rows = mapi_table_queryallrows($storestables, array(PR_ENTRYID, PR_DEFAULT_STORE, PR_MDB_PROVIDER));
01269 
01270                 foreach($rows as $row) {
01271                     if(!$return_public && isset($row[PR_DEFAULT_STORE]) && $row[PR_DEFAULT_STORE] == true) {
01272                         $entryid = $row[PR_ENTRYID];
01273                         break;
01274                     }
01275                     if ($return_public && isset($row[PR_MDB_PROVIDER]) && $row[PR_MDB_PROVIDER] == ZARAFA_STORE_PUBLIC_GUID) {
01276                         $entryid = $row[PR_ENTRYID];
01277                         break;
01278                     }
01279                 }
01280             }
01281         }
01282         else
01283             $entryid = @mapi_msgstore_createentryid($this->defaultstore, $user);
01284 
01285         if($entryid) {
01286             $store = @mapi_openmsgstore($this->session, $entryid);
01287 
01288             if (!$store) {
01289                 ZLog::Write(LOGLEVEL_WARN, sprintf("ZarafaBackend->openMessageStore('%s'): Could not open store", $user));
01290                 return false;
01291             }
01292 
01293             // add this store to the cache
01294             if (!isset($this->storeCache[$user]))
01295                 $this->storeCache[$user] = $store;
01296 
01297             ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZarafaBackend->openMessageStore('%s'): Found '%s' store: '%s'", $user, (($return_public)?'PUBLIC':'DEFAULT'),$store));
01298             return $store;
01299         }
01300         else {
01301             ZLog::Write(LOGLEVEL_WARN, sprintf("ZarafaBackend->openMessageStore('%s'): No store found for this user", $user));
01302             return false;
01303         }
01304     }
01305 
01306     private function hasSecretaryACLs($store, $folderid) {
01307         $entryid = mapi_msgstore_entryidfromsourcekey($store, hex2bin($folderid));
01308         if (!$entryid)  return false;
01309 
01310         $folder = mapi_msgstore_openentry($store, $entryid);
01311         if (!$folder) return false;
01312 
01313         $props = mapi_getprops($folder, array(PR_RIGHTS));
01314         if (isset($props[PR_RIGHTS]) &&
01315             ($props[PR_RIGHTS] & ecRightsReadAny) &&
01316             ($props[PR_RIGHTS] & ecRightsCreate) &&
01317             ($props[PR_RIGHTS] & ecRightsEditOwned) &&
01318             ($props[PR_RIGHTS] & ecRightsDeleteOwned) &&
01319             ($props[PR_RIGHTS] & ecRightsEditAny) &&
01320             ($props[PR_RIGHTS] & ecRightsDeleteAny) &&
01321             ($props[PR_RIGHTS] & ecRightsFolderVisible) ) {
01322             return true;
01323         }
01324         return false;
01325     }
01326 
01335     private function settingsOOF(&$oof) {
01336         //if oof state is set it must be set of oof and get otherwise
01337         if (isset($oof->oofstate)) {
01338             $this->settingsOOFSEt($oof);
01339         }
01340         else {
01341             $this->settingsOOFGEt($oof);
01342         }
01343     }
01344 
01353     private function settingsOOFGEt(&$oof) {
01354         $oofprops = mapi_getprops($this->defaultstore, array(PR_EC_OUTOFOFFICE, PR_EC_OUTOFOFFICE_MSG, PR_EC_OUTOFOFFICE_SUBJECT));
01355         $oof->oofstate = SYNC_SETTINGSOOF_DISABLED;
01356         $oof->Status = SYNC_SETTINGSSTATUS_SUCCESS;
01357         if ($oofprops != false) {
01358             $oof->oofstate = isset($oofprops[PR_EC_OUTOFOFFICE]) ? ($oofprops[PR_EC_OUTOFOFFICE] ? SYNC_SETTINGSOOF_GLOBAL : SYNC_SETTINGSOOF_DISABLED) : SYNC_SETTINGSOOF_DISABLED;
01359             //TODO external and external unknown
01360             $oofmessage = new SyncOOFMessage();
01361             $oofmessage->appliesToInternal = "";
01362             $oofmessage->enabled = $oof->oofstate;
01363             $oofmessage->replymessage = w2u(isset($oofprops[PR_EC_OUTOFOFFICE_MSG]) ? $oofprops[PR_EC_OUTOFOFFICE_MSG] : "");
01364             $oofmessage->bodytype = $oof->bodytype;
01365             unset($oofmessage->appliesToExternal, $oofmessage->appliesToExternalUnknown);
01366             $oof->oofmessage[] = $oofmessage;
01367         }
01368         else {
01369             ZLog::Write(LOGLEVEL_WARN, "Unable to get out of office information");
01370         }
01371 
01372         //unset body type for oof in order not to stream it
01373         unset($oof->bodyType);
01374 
01375     }
01376 
01385     private function settingsOOFSEt(&$oof) {
01386         $oof->Status = SYNC_SETTINGSSTATUS_SUCCESS;
01387         $props = array();
01388         if ($oof->oofstate == SYNC_SETTINGSOOF_GLOBAL || $oof->oofstate == SYNC_SETTINGSOOF_TIMEBASED) {
01389             $props[PR_EC_OUTOFOFFICE] = true;
01390             foreach ($oof->oofmessage as $oofmessage) {
01391                 if (isset($oofmessage->appliesToInternal)) {
01392                     $props[PR_EC_OUTOFOFFICE_MSG] = isset($oofmessage->replymessage) ? u2w($oofmessage->replymessage) : "";
01393                     $props[PR_EC_OUTOFOFFICE_SUBJECT] = "Out of office";
01394                 }
01395             }
01396         }
01397         elseif($oof->oofstate == SYNC_SETTINGSOOF_DISABLED) {
01398             $props[PR_EC_OUTOFOFFICE] = false;
01399         }
01400 
01401         if (!empty($props)) {
01402             @mapi_setprops($this->defaultstore, $props);
01403             $result = mapi_last_hresult();
01404             if ($result != NOERROR) {
01405                 ZLog::Write(LOGLEVEL_ERROR, sprintf("Setting oof information failed (%X)", $result));
01406                 return false;
01407             }
01408         }
01409 
01410         return true;
01411     }
01412 
01421     private function settingsUserInformation(&$userinformation) {
01422         if (!isset($this->defaultstore) || !isset($this->mainUser)) {
01423             ZLog::Write(LOGLEVEL_ERROR, "The store or user are not available for getting user information");
01424             return false;
01425         }
01426         $user = mapi_zarafa_getuser($this->defaultstore, $this->mainUser);
01427         if ($user != false) {
01428             $userinformation->Status = SYNC_SETTINGSSTATUS_USERINFO_SUCCESS;
01429             $userinformation->emailaddresses[] = $user["emailaddress"];
01430             return true;
01431         }
01432         ZLog::Write(LOGLEVEL_ERROR, sprintf("Getting user information failed: mapi_zarafa_getuser(%X)", mapi_last_hresult()));
01433         return false;
01434     }
01435 
01444     private function getImportanceAndPriority($xPriority, &$mapiprops, $sendMailProps) {
01445         switch($xPriority) {
01446             case 1:
01447             case 2:
01448                 $priority = PRIO_URGENT;
01449                 $importance = IMPORTANCE_HIGH;
01450                 break;
01451             case 4:
01452             case 5:
01453                 $priority = PRIO_NONURGENT;
01454                 $importance = IMPORTANCE_LOW;
01455                 break;
01456             case 3:
01457             default:
01458                 $priority = PRIO_NORMAL;
01459                 $importance = IMPORTANCE_NORMAL;
01460                 break;
01461         }
01462         $mapiprops[$sendMailProps["importance"]] = $importance;
01463         $mapiprops[$sendMailProps["priority"]] = $priority;
01464     }
01465 
01472     private function addRecipients($headers, &$mapimessage) {
01473         $toaddr = $ccaddr = $bccaddr = array();
01474 
01475         $Mail_RFC822 = new Mail_RFC822();
01476         if(isset($headers["to"]))
01477             $toaddr = $Mail_RFC822->parseAddressList($headers["to"]);
01478         if(isset($headers["cc"]))
01479             $ccaddr = $Mail_RFC822->parseAddressList($headers["cc"]);
01480         if(isset($headers["bcc"]))
01481             $bccaddr = $Mail_RFC822->parseAddressList($headers["bcc"]);
01482 
01483         if(empty($toaddr))
01484             throw new StatusException(sprintf("ZarafaBackend->SendMail(): 'To' address in RFC822 message not found or unparsable. To header: '%s'", ((isset($headers["to"]))?$headers["to"]:'')), SYNC_COMMONSTATUS_MESSHASNORECIP);
01485 
01486         // Add recipients
01487         $recips = array();
01488         foreach(array(MAPI_TO => $toaddr, MAPI_CC => $ccaddr, MAPI_BCC => $bccaddr) as $type => $addrlist) {
01489             foreach($addrlist as $addr) {
01490                 $mapirecip[PR_ADDRTYPE] = "SMTP";
01491                 $mapirecip[PR_EMAIL_ADDRESS] = $addr->mailbox . "@" . $addr->host;
01492                 if(isset($addr->personal) && strlen($addr->personal) > 0)
01493                     $mapirecip[PR_DISPLAY_NAME] = u2wi($addr->personal);
01494                 else
01495                     $mapirecip[PR_DISPLAY_NAME] = $mapirecip[PR_EMAIL_ADDRESS];
01496 
01497                 $mapirecip[PR_RECIPIENT_TYPE] = $type;
01498                 $mapirecip[PR_ENTRYID] = mapi_createoneoff($mapirecip[PR_DISPLAY_NAME], $mapirecip[PR_ADDRTYPE], $mapirecip[PR_EMAIL_ADDRESS]);
01499 
01500                 array_push($recips, $mapirecip);
01501             }
01502         }
01503 
01504         mapi_message_modifyrecipients($mapimessage, 0, $recips);
01505     }
01506 
01514     private function getForwardHeaders($message) {
01515         $messageprops = mapi_getprops($message, array(PR_SENT_REPRESENTING_NAME, PR_DISPLAY_TO, PR_DISPLAY_CC, PR_SUBJECT, PR_CLIENT_SUBMIT_TIME));
01516 
01517         $fwheader = "\r\n\r\n";
01518         $fwheader .= "-----Original Message-----\r\n";
01519         if(isset($messageprops[PR_SENT_REPRESENTING_NAME]))
01520             $fwheader .= "From: " . $messageprops[PR_SENT_REPRESENTING_NAME] . "\r\n";
01521         if(isset($messageprops[PR_DISPLAY_TO]) && strlen($messageprops[PR_DISPLAY_TO]) > 0)
01522             $fwheader .= "To: " . $messageprops[PR_DISPLAY_TO] . "\r\n";
01523         if(isset($messageprops[PR_DISPLAY_CC]) && strlen($messageprops[PR_DISPLAY_CC]) > 0)
01524             $fwheader .= "Cc: " . $messageprops[PR_DISPLAY_CC] . "\r\n";
01525         if(isset($messageprops[PR_CLIENT_SUBMIT_TIME]))
01526             $fwheader .= "Sent: " . strftime("%x %X", $messageprops[PR_CLIENT_SUBMIT_TIME]) . "\r\n";
01527         if(isset($messageprops[PR_SUBJECT]))
01528             $fwheader .= "Subject: " . $messageprops[PR_SUBJECT] . "\r\n";
01529 
01530         return $fwheader."\r\n";
01531     }
01532 
01541     private function copyAttachments(&$toMessage, $fromMessage) {
01542         $attachtable = mapi_message_getattachmenttable($fromMessage);
01543         $rows = mapi_table_queryallrows($attachtable, array(PR_ATTACH_NUM));
01544 
01545         foreach($rows as $row) {
01546             if(isset($row[PR_ATTACH_NUM])) {
01547                 $attach = mapi_message_openattach($fromMessage, $row[PR_ATTACH_NUM]);
01548 
01549                 $newattach = mapi_message_createattach($toMessage);
01550 
01551                 // Copy all attachments from old to new attachment
01552                 $attachprops = mapi_getprops($attach);
01553                 mapi_setprops($newattach, $attachprops);
01554 
01555                 if(isset($attachprops[mapi_prop_tag(PT_ERROR, mapi_prop_id(PR_ATTACH_DATA_BIN))])) {
01556                     // Data is in a stream
01557                     $srcstream = mapi_openpropertytostream($attach, PR_ATTACH_DATA_BIN);
01558                     $dststream = mapi_openpropertytostream($newattach, PR_ATTACH_DATA_BIN, MAPI_MODIFY | MAPI_CREATE);
01559 
01560                     while(1) {
01561                         $data = mapi_stream_read($srcstream, 4096);
01562                         if(strlen($data) == 0)
01563                             break;
01564 
01565                         mapi_stream_write($dststream, $data);
01566                     }
01567 
01568                     mapi_stream_commit($dststream);
01569                 }
01570                 mapi_savechanges($newattach);
01571             }
01572         }
01573     }
01574 
01583     private function getSearchFolder() {
01584         // create new or open existing search folder
01585         $searchFolderRoot = $this->getSearchFoldersRoot($this->store);
01586         if($searchFolderRoot === false) {
01587             // error in finding search root folder
01588             // or store doesn't support search folders
01589             return false;
01590         }
01591 
01592         $searchFolder = $this->createSearchFolder($searchFolderRoot);
01593 
01594         if($searchFolder !== false && mapi_last_hresult() == NOERROR) {
01595             return $searchFolder;
01596         }
01597         return false;
01598     }
01599 
01608     private function getSearchFoldersRoot() {
01609         // check if we can create search folders
01610         $storeProps = mapi_getprops($this->store, array(PR_STORE_SUPPORT_MASK, PR_FINDER_ENTRYID));
01611         if(($storeProps[PR_STORE_SUPPORT_MASK] & STORE_SEARCH_OK) != STORE_SEARCH_OK) {
01612             ZLog::Write(LOGLEVEL_WARN, "Store doesn't support search folders. Public store doesn't have FINDER_ROOT folder");
01613             return false;
01614         }
01615 
01616         // open search folders root
01617         $searchRootFolder = mapi_msgstore_openentry($this->store, $storeProps[PR_FINDER_ENTRYID]);
01618         if(mapi_last_hresult() != NOERROR) {
01619             ZLog::Write(LOGLEVEL_WARN, sprintf("Unable to open search folder (0x%X)", mapi_last_hresult()));
01620             return false;
01621         }
01622 
01623         return $searchRootFolder;
01624     }
01625 
01626 
01635     private function createSearchFolder($searchFolderRoot) {
01636         $folderName = "Z-Push Search Folder ".@getmypid();
01637         $searchFolders = mapi_folder_gethierarchytable($searchFolderRoot);
01638         $restriction = array(
01639             RES_CONTENT,
01640             array(
01641                     FUZZYLEVEL      => FL_PREFIX,
01642                     ULPROPTAG       => PR_DISPLAY_NAME,
01643                     VALUE           => array(PR_DISPLAY_NAME=>$folderName)
01644             )
01645         );
01646         //restrict the hierarchy to the z-push search folder only
01647         mapi_table_restrict($searchFolders, $restriction);
01648         if (mapi_table_getrowcount($searchFolders)) {
01649             $searchFolder = mapi_table_queryrows($searchFolders, array(PR_ENTRYID), 0, 1);
01650 
01651             return mapi_msgstore_openentry($this->store, $searchFolder[0][PR_ENTRYID]);
01652         }
01653         return mapi_folder_createfolder($searchFolderRoot, $folderName, null, 0, FOLDER_SEARCH);
01654     }
01655 
01662     private function getSearchRestriction($cpo) {
01663         $searchText = $cpo->GetSearchFreeText();
01664 
01665         $searchGreater = strtotime($cpo->GetSearchValueGreater());
01666         $searchLess = strtotime($cpo->GetSearchValueLess());
01667 
01668         // split the search on whitespache and look for every word
01669         $searchText = preg_split("/\W+/", $searchText);
01670         $searchProps = array(PR_BODY, PR_SUBJECT, PR_DISPLAY_TO, PR_DISPLAY_CC, PR_SENDER_NAME, PR_SENDER_EMAIL_ADDRESS, PR_SENT_REPRESENTING_NAME, PR_SENT_REPRESENTING_EMAIL_ADDRESS);
01671         $resAnd = array();
01672         foreach($searchText as $term) {
01673             $resOr = array();
01674 
01675             foreach($searchProps as $property) {
01676                 array_push($resOr,
01677                     array(RES_CONTENT,
01678                         array(
01679                             FUZZYLEVEL => FL_SUBSTRING|FL_IGNORECASE,
01680                             ULPROPTAG => $property,
01681                             VALUE => u2w($term)
01682                         )
01683                     )
01684                 );
01685             }
01686             array_push($resAnd, array(RES_OR, $resOr));
01687         }
01688 
01689         // add time range restrictions
01690         if ($searchGreater) {
01691             array_push($resAnd, array(RES_PROPERTY, array(RELOP => RELOP_GE, ULPROPTAG => PR_MESSAGE_DELIVERY_TIME, VALUE => array(PR_MESSAGE_DELIVERY_TIME => $searchGreater)))); // RES_AND;
01692         }
01693         if ($searchLess) {
01694             array_push($resAnd, array(RES_PROPERTY, array(RELOP => RELOP_LE, ULPROPTAG => PR_MESSAGE_DELIVERY_TIME, VALUE => array(PR_MESSAGE_DELIVERY_TIME => $searchLess))));
01695         }
01696         $mapiquery = array(RES_AND, $resAnd);
01697 
01698         return $mapiquery;
01699     }
01700 }
01701 
01705 class BackendICS extends BackendZarafa {}
01706 
01707 ?>