. * * Consult LICENSE file for details ************************************************/ // config file require_once("backend/imap/config.php"); require_once("backend/imap/mime_calendar.php"); require_once("backend/imap/mime_encode.php"); require_once("backend/imap/user_identity.php"); // Add the path for Andrew's Web Libraries to include_path // because it is required for the emails with ics attachments // @see https://jira.z-hub.io/browse/ZP-1149 set_include_path(get_include_path() . PATH_SEPARATOR . '/usr/share/awl/inc' . PATH_SEPARATOR . dirname(__FILE__) . '/'); class BackendIMAP extends BackendDiff implements ISearchProvider { private $wasteID; private $sentID; private $server; private $mbox; private $mboxFolder; private $username; private $password; private $domain; private $sinkfolders = array(); private $sinkstates = array(); private $changessinkinit = false; private $folderhierarchy; private $excludedFolders; private static $mimeTypes = false; private $imapParams = array(); private $dontStat = array(); //keys in this array represent mailboxes which can't be stat'd (ie, /NoSELECT status) //define constants for imap mailbox attributes const LATT_NOINFERIORS = 1; const LATT_NOSELECT = 2; const LATT_MARKED = 4; const LATT_UNMARKED = 8; const LATT_REFERRAL = 16; const LATT_HASCHILDREN = 32; const LATT_HASNOCHILDREN = 64; public function __construct() { if (BackendIMAP::$mimeTypes === false) { BackendIMAP::$mimeTypes = $this->SystemExtensionMimeTypes(); } $this->wasteID = false; $this->sentID = false; $this->mboxFolder = ""; if (!function_exists("imap_open")) throw new FatalException("BackendIMAP(): php-imap module is not installed", 0, null, LOGLEVEL_FATAL); if (!function_exists("mb_detect_order")) { ZLog::Write(LOGLEVEL_WARN, sprintf("BackendIMAP(): php-mbstring module is not installed, you should install it for better encoding conversions")); } if (defined("IMAP_DISABLE_AUTHENTICATOR")) { ZLog::Write(LOGLEVEL_INFO, sprintf("BackendIMAP(): The following authentication methods are disabled: %s", IMAP_DISABLE_AUTHENTICATOR)); $this->imapParams = array("DISABLE_AUTHENTICATOR" => array_map('trim' ,explode(',', IMAP_DISABLE_AUTHENTICATOR))); } if (!defined('IMAP_SEARCH_CHARSET')) { ZLog::Write(LOGLEVEL_INFO, "BackendIMAP(): Using UTF-8 as Default Charset for Searches"); define('IMAP_SEARCH_CHARSET', 'UTF-8'); } } /**---------------------------------------------------------------------------------------------------------- * default backend methods */ /** * Authenticates the user * * @param string $username * @param string $domain * @param string $password * * @access public * @return boolean * @throws FatalException if php-imap module can not be found */ public function Logon($username, $domain, $password) { $this->wasteID = false; $this->sentID = false; $this->server = "{" . IMAP_SERVER . ":" . IMAP_PORT . "/imap" . IMAP_OPTIONS . "}"; if (!function_exists("imap_open")) throw new FatalException("BackendIMAP(): php-imap module is not installed", 0, null, LOGLEVEL_FATAL); if (defined('IMAP_FOLDER_CONFIGURED') && IMAP_FOLDER_CONFIGURED == false) throw new FatalException("BackendIMAP(): You didn't configure your IMAP folder names. Do it before!", 0, null, LOGLEVEL_FATAL); /* BEGIN fmbiete's contribution r1527, ZP-319 */ $this->excludedFolders = array(); if (defined('IMAP_EXCLUDED_FOLDERS') && strlen(IMAP_EXCLUDED_FOLDERS) > 0) { $this->excludedFolders = explode("|", IMAP_EXCLUDED_FOLDERS); ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->Logon(): Excluding Folders (%s)", IMAP_EXCLUDED_FOLDERS)); } /* END fmbiete's contribution r1527, ZP-319 */ // open the IMAP-mailbox $this->mbox = @imap_open($this->server , $username, $password, OP_HALFOPEN, 0, $this->imapParams); $this->mboxFolder = ""; if ($this->mbox) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->Logon(): User '%s' is authenticated on '%s'", $username, $this->server)); $this->username = $username; $this->password = $password; $this->domain = $domain; return true; } else { ZLog::Write(LOGLEVEL_ERROR, sprintf("BackendIMAP->Logon(): can't connect as user '%s' on '%s': %s", $username, $this->server, imap_last_error())); return false; } } /** * Logs off * Called before shutting down the request to close the IMAP connection * writes errors to the log * * @access public * @return boolean */ public function Logoff() { $this->close_connection(); $this->SaveStorages(); } /** * Sends an e-mail * This messages needs to be saved into the 'sent items' folder * * @param SyncSendMail $sm SyncSendMail object * * @access public * @return boolean * @throws StatusException */ public function SendMail($sm) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->SendMail(): RFC822: %d bytes forward-id: '%s' reply-id: '%s' parent-id: '%s' SaveInSent: '%s' ReplaceMIME: '%s'", strlen($sm->mime), Utils::PrintAsString($sm->forwardflag ? (isset($sm->source->itemid) ? $sm->source->itemid : "error no itemid") : false), Utils::PrintAsString($sm->replyflag ? (isset($sm->source->itemid) ? $sm->source->itemid : "error no itemid") : false), Utils::PrintAsString((isset($sm->source->folderid) ? $sm->source->folderid : false)), Utils::PrintAsString(($sm->saveinsent)), Utils::PrintAsString(isset($sm->replacemime)))); // by splitting the message in several lines we can easily grep later if(ZLog::IsWbxmlDebugEnabled()) { $logWbxmlMime = ""; $startpos = 0; // limit log to about 10 KB and use ZLog:Write() truncation while ($startpos < 10240) { if ($endpos = strpos($sm->mime, "\n", $startpos)) { $logWbxmlMime .= "RFC822: " . trim(substr($sm->mime, $startpos, ($endpos - $startpos))) . PHP_EOL; $startpos = ++$endpos; } else { if (strlen(trim(substr($sm->mime, $startpos))) > 0) { $logWbxmlMime .= "RFC822: " . trim(substr($sm->mime, $startpos)) . PHP_EOL; } break; } } ZLog::Write(LOGLEVEL_WBXML, $logWbxmlMime); unset($logWbxmlMime); } $sourceMessage = $sourceMail = false; // If we have a reference to a source message and we are not replacing mime (since we wouldn't use it) if (isset($sm->source->folderid) && isset($sm->source->itemid) && (!isset($sm->replacemime) || $sm->replacemime === false)) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->SendMail(): We have a source message and we try to fetch it")); $parent = $this->getImapIdFromFolderId($sm->source->folderid); if ($parent === false) { throw new StatusException(sprintf("BackendIMAP->SendMail(): Could not get imapid from source folderid '%'", $sm->source->folderid), SYNC_COMMONSTATUS_ITEMNOTFOUND); } else { $this->imap_reopen_folder($parent); $sourceMail = @imap_fetchheader($this->mbox, $sm->source->itemid, FT_UID) . @imap_body($this->mbox, $sm->source->itemid, FT_PEEK | FT_UID); $mobj = new Mail_mimeDecode($sourceMail); $sourceMessage = $mobj->decode(array('decode_headers' => false, 'decode_bodies' => true, 'include_bodies' => true, 'rfc_822bodies' => true, 'charset' => 'utf-8')); unset($mobj); //We will need $sourceMail if the message is forwarded and not inlined // If it's a reply, we mark the original message as answered if ($sm->replyflag) { if (!@imap_setflag_full($this->mbox, $sm->source->itemid, "\\Answered", ST_UID)) { ZLog::Write(LOGLEVEL_WARN, sprintf("BackendIMAP->SendMail(): Unable to mark the message as Answered")); } } // If it's a forward, we mark the original message as forwarded if ($sm->forwardflag) { if (!@imap_setflag_full($this->mbox, $sm->source->itemid, "\$Forwarded", ST_UID)) { ZLog::Write(LOGLEVEL_WARN, sprintf("BackendIMAP->SendMail(): Unable to mark the message as Forwarded")); } } } } ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->SendMail(): We get the new message")); $mobj = new Mail_mimeDecode($sm->mime); $message = $mobj->decode(array('decode_headers' => 'utf-8', 'decode_bodies' => true, 'include_bodies' => true, 'rfc_822bodies' => true, 'charset' => 'utf-8')); unset($mobj); ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->SendMail(): We get the From and To")); $Mail_RFC822 = new Mail_RFC822(); $this->setFromHeaderValue($message->headers); $fromaddr = $this->parseAddr($Mail_RFC822->parseAddressList($message->headers["from"])); $toaddr = ""; if (isset($message->headers["to"])) { // don't validate atoms, headers might be UTF-8 not ASCII $toaddr = $Mail_RFC822->parseAddressList($message->headers["to"], null, null, false, null); $message->headers["to"] = Utils::CheckAndFixEncodingInHeadersOfSentMail($toaddr); $toaddr = $this->parseAddr($toaddr); ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->SendMail(): To defined: %s", $toaddr)); } if (isset($message->headers["cc"])) { $message->headers["cc"] = Utils::CheckAndFixEncodingInHeadersOfSentMail($Mail_RFC822->parseAddressList($message->headers["cc"], null, null, false, null)); } unset($Mail_RFC822); if (isset($message->headers["subject"]) && mb_detect_encoding($message->headers["subject"], "UTF-8") != false && preg_match('/[^\x00-\x7F]/', $message->headers["subject"]) == 1) { mb_internal_encoding("UTF-8"); ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->SendMail(): Subject in raw UTF-8: %s", $message->headers["subject"])); $message->headers["subject"] = mb_encode_mimeheader($message->headers["subject"]); } $this->setReturnPathValue($message->headers, $fromaddr); $finalBody = ""; $finalHeaders = array(); // if it's a S/MIME message or has VCALENDAR objects I don't do anything with it if (is_smime($message) || has_calendar_object($message)) { $mobj = new Mail_mimeDecode($sm->mime); $parts = $mobj->getSendArray(); unset($mobj); if ($parts === false) { throw new StatusException(sprintf("BackendIMAP->SendMail(): Could not getSendArray for SMIME messages"), SYNC_COMMONSTATUS_MAILSUBMISSIONFAILED); } else { list($recipients, $finalHeaders, $finalBody) = $parts; $this->setFromHeaderValue($finalHeaders); $this->setReturnPathValue($finalHeaders, $fromaddr); } } else { //http://pear.php.net/manual/en/package.mail.mail-mime.example.php //http://pear.php.net/manual/en/package.mail.mail-mimedecode.decode.php //http://pear.php.net/manual/en/package.mail.mail-mimepart.addsubpart.php // I don't mind if the new message is multipart or not, I always will create a multipart. It's simpler $finalEmail = new Mail_mimePart('', array('content_type' => 'multipart/mixed')); if ($sm->replyflag && (!isset($sm->replacemime) || $sm->replacemime === false)) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->SendMail(): Replying message")); $this->addTextParts($finalEmail, $message, $sourceMessage, true); if (isset($message->parts)) { // We add extra parts from the replying message add_extra_sub_parts($finalEmail, $message->parts); } // A replied message doesn't include the original attachments } else if ($sm->forwardflag && (!isset($sm->replacemime) || $sm->replacemime === false)) { if (!defined('IMAP_INLINE_FORWARD') || IMAP_INLINE_FORWARD === false) { ZLog::Write(LOGLEVEL_DEBUG, "BackendIMAP->SendMail(): Forwarding message as attached file - eml"); $finalEmail->addSubPart($sourceMail, array('content_type' => 'message/rfc822', 'encoding' => 'base64', 'disposition' => 'attachment', 'dfilename' => 'forwarded_message.eml')); } else { ZLog::Write(LOGLEVEL_DEBUG, "BackendIMAP->SendMail(): Forwarding inlined message"); $this->addTextParts($finalEmail, $message, $sourceMessage, false); if (isset($message->parts)) { // We add extra parts from the forwarding message add_extra_sub_parts($finalEmail, $message->parts); } if (isset($sourceMessage->parts)) { // We add extra parts from the forwarded message add_extra_sub_parts($finalEmail, $sourceMessage->parts); } } } else { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->SendMail(): is a new message or we are replacing mime")); $this->addTextPartsMessage($finalEmail, $message); if (isset($message->parts)) { // We add extra parts from the new message add_extra_sub_parts($finalEmail, $message->parts); } } // We encode the final message $boundary = '=_' . md5(rand() . microtime()); $finalEmail = $finalEmail->encode($boundary); $finalHeaders = array('MIME-Version' => '1.0'); // We copy all the non-existent headers, minus content_type ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->SendMail(): Copying new headers")); foreach ($message->headers as $k => $v) { if (strcasecmp($k, 'content-type') != 0 && strcasecmp($k, 'content-transfer-encoding') != 0 && strcasecmp($k, 'mime-version') != 0) { if (!isset($finalHeaders[$k])) $finalHeaders[ucwords($k)] = $v; } } foreach ($finalEmail['headers'] as $k => $v) { if (!isset($finalHeaders[$k])) $finalHeaders[$k] = $v; } $finalBody = "This is a multi-part message in MIME format.\n" . $finalEmail['body']; unset($finalEmail); } unset($sourceMail); unset($message); unset($sourceMessage); ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->SendMail(): Final mail to send:")); if(ZLog::IsWbxmlDebugEnabled()) { $logWbxmlHeaders = ""; foreach ($finalHeaders as $k => $v) { $logWbxmlHeaders .= $k . ": " . $v . PHP_EOL; } ZLog::Write(LOGLEVEL_WBXML, $logWbxmlHeaders, false); unset($logWbxmlHeaders); $logWbxmlBody = ""; foreach (preg_split("/((\r)?\n)/", $finalBody) as $bodyline) { $logWbxmlBody .= "Body: " . $bodyline . PHP_EOL; } ZLog::Write(LOGLEVEL_WBXML, $logWbxmlBody, false); unset($logWbxmlBody); } $send = $this->sendMessage($fromaddr, $toaddr, $finalHeaders, $finalBody); if ($send) { if (isset($sm->saveinsent)) { $this->saveSentMessage($finalHeaders, $finalBody); } else { ZLog::Write(LOGLEVEL_DEBUG, "BackendIMAP->SendMail(): Not saving in SentFolder"); } } unset($finalHeaders); unset($finalBody); return $send; } /** * Add text parts to a mimepart object, with reply or forward tags * * @param Mail_mimePart $email reference to the object * @param Mail_mimeDecode $message reference to the message * @param Mail_mimeDecode $sourceMessage reference to the original message * @param boolean $isReply true if it's a reply, false if it's a forward * * @access private * @return void */ private function addTextParts(&$email, &$message, &$sourceMessage, $isReply = true) { $htmlBody = $plainBody = ''; Mail_mimeDecode::getBodyRecursive($message, "html", $htmlBody); Mail_mimeDecode::getBodyRecursive($message, "plain", $plainBody); $htmlSource = $plainSource = ''; Mail_mimeDecode::getBodyRecursive($sourceMessage, "html", $htmlSource); Mail_mimeDecode::getBodyRecursive($sourceMessage, "plain", $plainSource); $separator = ''; if ($isReply) { $separator = ">\r\n"; $separatorHtml = "
"; $separatorHtmlEnd = "
"; } else { $separator = ""; $separatorHtml = "
"; $separatorHtmlEnd = "
"; } $altEmail = new Mail_mimePart('', array('content_type' => 'multipart/alternative')); if (strlen($htmlBody) > 0) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->addTextParts(): The message has HTML body")); if (strlen($htmlSource) > 0) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->addTextParts(): The original message had HTML body")); $altEmail->addSubPart($htmlBody . $separatorHtml . $htmlSource . $separatorHtmlEnd, array('content_type' => 'text/html; charset=utf-8', 'encoding' => 'base64')); } else { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->addTextParts(): The original message had not HTML body, we use original PLAIN body to create HTML")); $altEmail->addSubPart($htmlBody . $separatorHtml . "

" . $plainSource . "

" . $separatorHtmlEnd, array('content_type' => 'text/html; charset=utf-8', 'encoding' => 'base64')); } } if (strlen($plainBody) > 0) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->addTextParts(): The message has PLAIN body")); if (strlen($plainSource) > 0) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->addTextParts(): The original message had PLAIN body")); $altEmail->addSubPart($plainBody . $separator . str_replace("\n", "\n> ", "> " . $plainSource), array('content_type' => 'text/plain; charset=utf-8', 'encoding' => 'base64')); } else { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->addTextParts(): The original message had not PLAIN body, we use original HTML body to create PLAIN")); $altEmail->addSubPart($plainBody . $separator . str_replace("\n", "\n> ", "> " . Utils::ConvertHtmlToText($htmlSource)), array('content_type' => 'text/plain; charset=utf-8', 'encoding' => 'base64')); } } $boundary = '=_' . md5(rand() . microtime()); $altEmail = $altEmail->encode($boundary); $email->addSubPart($altEmail['body'], array('content_type' => 'multipart/alternative;'."\n".' boundary="'.$boundary.'"')); unset($altEmail); unset($htmlBody); unset($htmlSource); unset($plainBody); unset($plainSource); } /** * Add text parts to a mimepart object * * @param Mail_mimePart $email reference to the object * @param Mail_mimeDecode $message reference to the message * * @access private * @return void */ private function addTextPartsMessage(&$email, &$message) { $altEmail = new Mail_mimePart('', array('content_type' => 'multipart/alternative')); foreach (array("plain", "html", "calendar") as $type) { $body = ''; Mail_mimeDecode::getBodyRecursive($message, $type, $body); if (strlen($body) > 0) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->addTextPartsMessage(): The message has %s body", $type)); $altEmail->addSubPart($body, array('content_type' => sprintf("text/%s; charset=utf-8", $type), 'encoding' => 'base64')); } } unset($body); $boundary = '=_' . md5(rand() . microtime()); $altEmail = $altEmail->encode($boundary); $email->addSubPart($altEmail['body'], array('content_type' => 'multipart/alternative;'."\n".' boundary="'.$boundary.'"')); unset($altEmail); } /** * Returns the waste basket * * @access public * @return string */ public function GetWasteBasket() { // TODO this could be retrieved from the DeviceFolderCache if ($this->wasteID == false) { //try to get the waste basket without doing complete hierarchy sync $folder_name = IMAP_FOLDER_TRASH; if (defined('IMAP_FOLDER_PREFIX') && strlen(IMAP_FOLDER_PREFIX) > 0) $folder_name = IMAP_FOLDER_PREFIX . $this->getServerDelimiter() . $folder_name; $wastebaskt = @imap_getmailboxes($this->mbox, $this->server, $folder_name); if (isset($wastebaskt[0])) { $this->wasteID = $this->convertImapId(substr($wastebaskt[0]->name, strlen($this->server))); return $this->wasteID; } //try get waste id from hierarchy if it wasn't possible with above for some reason $this->GetHierarchy(); } return $this->wasteID; } /** * Returns the content of the named attachment as stream. The passed attachment identifier is * the exact string that is returned in the 'AttName' property of an SyncAttachment. * Any information necessary to find the attachment must be encoded in that 'attname' property. * * @param string $attname * * @access public * @return SyncItemOperationsAttachment * @throws StatusException */ public function GetAttachmentData($attname) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->GetAttachmentData('%s')", $attname)); list($folderid, $id, $part) = explode(":", $attname); if (!isset($folderid) || !isset($id) || !isset($part)) throw new StatusException(sprintf("BackendIMAP->GetAttachmentData('%s'): Error, attachment name key can not be parsed", $attname), SYNC_ITEMOPERATIONSSTATUS_INVALIDATT); // convert back to work on an imap-id $folderImapid = $this->getImapIdFromFolderId($folderid); $this->imap_reopen_folder($folderImapid); $mail = @imap_fetchheader($this->mbox, $id, FT_UID) . @imap_body($this->mbox, $id, FT_PEEK | FT_UID); if (empty($mail)) { throw new StatusException(sprintf("BackendIMAP->GetAttachmentData('%s'): Error, message not found, maybe was moved", $attname), SYNC_ITEMOPERATIONSSTATUS_INVALIDATT); } $mobj = new Mail_mimeDecode($mail); unset($mail); $message = $mobj->decode(array('decode_headers' => 'utf-8', 'decode_bodies' => true, 'include_bodies' => true, 'rfc_822bodies' => true, 'charset' => 'utf-8')); unset($mobj); if (!isset($message->parts)) { throw new StatusException(sprintf("BackendIMAP->GetAttachmentData('%s'): Error, message without parts. Requesting part key: '%d'", $attname, $part), SYNC_ITEMOPERATIONSSTATUS_INVALIDATT); } /* BEGIN fmbiete's contribution r1528, ZP-320 */ //trying parts $mparts = $message->parts; for ($i = 0; $i < count($mparts); $i++) { $auxpart = $mparts[$i]; //recursively add parts if($auxpart->ctype_primary == "multipart" && ($auxpart->ctype_secondary == "mixed" || $auxpart->ctype_secondary == "alternative" || $auxpart->ctype_secondary == "related")) { foreach($auxpart->parts as $spart) $mparts[] = $spart; } } /* END fmbiete's contribution r1528, ZP-320 */ if (!isset($mparts[$part]->body)) throw new StatusException(sprintf("BackendIMAP->GetAttachmentData('%s'): Error, requested part key can not be found: '%d'", $attname, $part), SYNC_ITEMOPERATIONSSTATUS_INVALIDATT); $attachment = new SyncItemOperationsAttachment(); /* BEGIN fmbiete's contribution r1528, ZP-320 */ $attachment->data = StringStreamWrapper::Open($mparts[$part]->body); if (isset($mparts[$part]->ctype_primary) && isset($mparts[$part]->ctype_secondary)) $attachment->contenttype = $mparts[$part]->ctype_primary .'/'.$mparts[$part]->ctype_secondary; unset($mparts); unset($message); ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->GetAttachmentData contenttype %s", $attachment->contenttype)); /* END fmbiete's contribution r1528, ZP-320 */ return $attachment; } /** * Deletes all contents of the specified folder. * This is generally used to empty the trash (wastebasked), but could also be used on any * other folder. * * @param string $folderid * @param boolean $includeSubfolders (opt) also delete sub folders, default true * * @access public * @return boolean * @throws StatusException */ public function EmptyFolder($folderid, $includeSubfolders = true) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->EmptyFolder('%s', '%s')", $folderid, Utils::PrintAsString($includeSubfolders))); $folderImapid = $this->getImapIdFromFolderId($folderid); if ($folderImapid === false) { throw new StatusException(sprintf("BackendIMAP->EmptyFolder('%s','%s'): Error, unable to open folder (no entry id)", $folderid, Utils::PrintAsString($includeSubfolders)), SYNC_ITEMOPERATIONSSTATUS_SERVERERROR); } if (!$this->imap_reopen_folder($folderImapid)) { throw new StatusException(sprintf("BackendIMAP->EmptyFolder('%s','%s'): Error, unable to open parent folder (open entry)", $folderid, Utils::PrintAsString($includeSubfolders)), SYNC_ITEMOPERATIONSSTATUS_SERVERERROR); } ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->EmptyFolder('%s','%s'): emptying folder", $folderid, Utils::PrintAsString($includeSubfolders))); // TODO: make transactional all these deletes: see comment bellow if (@imap_delete($this->mbox, "1:*")) { @imap_expunge($this->mbox); // An error erasing any subfolder won't return an error to the device, because we should undelete the already expunged messages, and we cannot undelete a folder if ($includeSubfolders) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->EmptyFolder('%s','%s'): deleting subfolders", $folderid, Utils::PrintAsString($includeSubfolders))); // Find subfolders $subfolders = @imap_getmailboxes($this->mbox, $this->server . $folderImapid, "*"); if (is_array($subfolders)) { // delete mailbox and its content foreach ($subfolders as $val) { $subname = substr($val->name, strlen($this->server)); if (!@imap_deletemailbox($this->mbox, $val->name)) { ZLog::Write(LOGLEVEL_ERROR, sprintf("BackendIMAP->EmptyFolder('%s','%s'): Error deleting subfolder %s", $folderid, Utils::PrintAsString($includeSubfolders), $subname)); } } } else { ZLog::Write(LOGLEVEL_ERROR, sprintf("BackendIMAP->EmptyFolder('%s','%s'): Error getting subfolder list", $folderid, Utils::PrintAsString($includeSubfolders))); } } } else { throw new StatusException(sprintf("BackendIMAP->EmptyFolder('%s','%s'): Error, imap_delete() failed; '%s'", $folderid, Utils::PrintAsString($includeSubfolders), @imap_last_error()), SYNC_ITEMOPERATIONSSTATUS_SERVERERROR); } return true; } /** * Indicates if the backend has a ChangesSink. * A sink is an active notification mechanism which does not need polling. * The IMAP backend simulates a sink by polling status information of the folder * * @access public * @return boolean */ public function HasChangesSink() { return true; } /** * The folder should be considered by the sink. * Folders which were not initialized should not result in a notification * of IBacken->ChangesSink(). * * @param string $folderid * * @access public * @return boolean false if found can not be found */ public function ChangesSinkInitialize($folderid) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->ChangesSinkInitialize(): folderid '%s'", $folderid)); $imapid = $this->getImapIdFromFolderId($folderid); if (!$this->changessinkinit) { // First folder, store the actual folder structure $list= $this->get_attributes_list(); foreach ($list as $l) { //ZLog::Write(LOGLEVEL_INFO, sprintf("BackendIMAP->ChangesSinkInitialize(): adding '%s' with attributes: '%s'", $l['name'], print_r($l,true))); $this->folderhierarchy[] = $l['name']; if (isset($l['noSelect']) && $l['noSelect'] != false) { $dontStatFolder = str_replace( $this->server, '', $l['name']); //ZLog::Write(LOGLEVEL_INFO, sprintf("BackendIMAP->ChangesSinkInitialize(): adding '%s' to dontStatFolders()", $dontStatFolder)); $this->dontStatFolders[$dontStatFolder] = true; } } } if (($imapid !== false) && !(isset($this->dontStatFolders[$imapid]) )) { $this->sinkfolders[] = $imapid; $this->changessinkinit = true; } return $this->changessinkinit; } /** * The actual ChangesSink. * For max. the $timeout value this method should block and if no changes * are available return an empty array. * If changes are available a list of folderids is expected. * * @param int $timeout max. amount of seconds to block * * @access public * @return array */ public function ChangesSink($timeout = 30) { $notifications = array(); $stopat = time() + $timeout - 1; //We can get here and the ChangesSink not be initialized yet if (!$this->changessinkinit) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP>ChangesSink - Not initialized ChangesSink, sleep and exit")); // We sleep and do nothing else sleep($timeout); return $notifications; } // Reconnect IMAP server $this->imap_reconnect(); // Check folder hierarchy and create change if (count(array_diff($this->folderhierarchy, $this->get_folder_list())) > 0) { ZLog::Write(LOGLEVEL_DEBUG, "BackendIMAP->ChangesSink(): Changes in folder hierarchy detected!!"); throw new StatusException("BackendIMAP->ChangesSink(): HierarchySync required.", SyncCollections::HIERARCHY_CHANGED); } $flaggedMessages = array(); $answeredMessages = array(); $forwardedMessages = array(); // only check once to reduce pressure in the IMAP server foreach ($this->sinkfolders as $i => $imapid) { $this->imap_reopen_folder($imapid); // courier-imap only clears the status cache after checking @imap_check($this->mbox); // get count of recent, unseen and overall messages $status = @imap_status($this->mbox, $this->server . $imapid, SA_ALL); // get count of flagged messages (ZP-1561) $flaggedMessages = @imap_search($this->mbox, 'FLAGGED'); $flaggedMessages = is_array($flaggedMessages) ? count($flaggedMessages) : 0; // search for answered messages $answeredMessages = @imap_search($this->mbox, 'ANSWERED'); $answeredMessages = is_array($answeredMessages) ? count($answeredMessages) : 0; // search for forwarded messages $forwardedMessages = @imap_search($this->mbox, 'KEYWORD $Forwarded'); $forwardedMessages = is_array($forwardedMessages) ? count($forwardedMessages) : 0; if (!$status) { ZLog::Write(LOGLEVEL_WARN, sprintf("ChangesSink: could not stat folder '%s': %s ", $this->getFolderIdFromImapId($imapid), imap_last_error())); } else { $newstate = "M:". $status->messages ."-R:". $status->recent ."-U:". $status->unseen ."-F:". $flaggedMessages ."-A:". $answeredMessages ."-W:". $forwardedMessages; if (! isset($this->sinkstates[$imapid]) ) { $this->sinkstates[$imapid] = $newstate; } if ($this->sinkstates[$imapid] != $newstate) { $notifications[] = $this->getFolderIdFromImapId($imapid); $this->sinkstates[$imapid] = $newstate; ZLog::Write(LOGLEVEL_DEBUG, "BackendIMAP->ChangesSink(): ChangesSink detected!!"); } } } // Close IMAP connection, we will reconnect in the next execution. This will reduce IMAP pressure $this->close_connection(); // Wait to timeout if (empty($notifications)) { while ($stopat > time()) { sleep(1); } } return $notifications; } /**---------------------------------------------------------------------------------------------------------- * implemented DiffBackend methods */ /** * Returns a list (array) of folders. * * @access public * @return array/boolean false if the list could not be retrieved */ public function GetFolderList() { $folders = array(); $list = $this->get_folder_list(); foreach ($list as $val) { // don't return the excluded folders $notExcluded = true; for ($i = 0, $cnt = count($this->excludedFolders); $notExcluded && $i < $cnt; $i++) { // expr1, expr2 modified by mku ZP-329 // fix exclude folders with special chars by mku ZP-329 if (strpos(strtolower($val), strtolower(Utils::Utf8_to_utf7imap($this->excludedFolders[$i]))) !== false) { $notExcluded = false; ZLog::Write(LOGLEVEL_DEBUG, sprintf("Pattern: <%s> found, excluding folder: '%s'", $this->excludedFolders[$i], $val)); // sprintf added by mku ZP-329 } } if ($notExcluded) { $box = array(); // cut off serverstring $imapid = substr($val, strlen($this->server)); $box["id"] = $this->convertImapId($imapid); $folders[] = $box; } } return $folders; } /** * Returns an actual SyncFolder object * * @param string $id id of the folder * * @access public * @return object SyncFolder with information */ public function GetFolder($id) { $folder = new SyncFolder(); $folder->serverid = $id; // convert back to work on an imap-id $imapid = $this->getImapIdFromFolderId($id); // explode hierarchy $fhir = explode($this->getServerDelimiter(), $imapid); // TODO WasteID or SentID could be saved for later ussage if (strcasecmp($imapid, $this->create_name_folder(IMAP_FOLDER_INBOX)) == 0) { $folder->parentid = "0"; $folder->displayname = "Inbox"; $folder->type = SYNC_FOLDER_TYPE_INBOX; } else if (strcasecmp($imapid, $this->create_name_folder(IMAP_FOLDER_DRAFT)) == 0) { $folder->parentid = "0"; $folder->displayname = "Drafts"; $folder->type = SYNC_FOLDER_TYPE_DRAFTS; } else if (strcasecmp($imapid, $this->create_name_folder(IMAP_FOLDER_SENT)) == 0) { $folder->parentid = "0"; $folder->displayname = "Sent"; $folder->type = SYNC_FOLDER_TYPE_SENTMAIL; $this->sentID = $id; } else if (strcasecmp($imapid, $this->create_name_folder(IMAP_FOLDER_TRASH)) == 0) { $folder->parentid = "0"; $folder->displayname = "Trash"; $folder->type = SYNC_FOLDER_TYPE_WASTEBASKET; $this->wasteID = $id; } else if (strcasecmp($imapid, $this->create_name_folder(IMAP_FOLDER_SPAM)) == 0) { $folder->parentid = "0"; $folder->displayname = "Junk"; $folder->type = SYNC_FOLDER_TYPE_USER_MAIL; } else if (strcasecmp($imapid, $this->create_name_folder(IMAP_FOLDER_ARCHIVE)) == 0) { $folder->parentid = "0"; $folder->displayname = "Archive"; $folder->type = SYNC_FOLDER_TYPE_USER_MAIL; } else { if (defined('IMAP_FOLDER_PREFIX') && strlen(IMAP_FOLDER_PREFIX) > 0) { if (strcasecmp($fhir[0], IMAP_FOLDER_PREFIX) == 0) { // Discard prefix array_shift($fhir); } else { ZLog::Write(LOGLEVEL_WARN, sprintf("BackendIMAP->GetFolder('%s'): '%s'; using server delimiter '%s', first part '%s' is not equal to the prefix defined '%s'. Something is wrong with your config.", $id, $imapid, $this->getServerDelimiter(), $fhir[0], IMAP_FOLDER_PREFIX)); } } if (count($fhir) == 1) { $folder->displayname = Utils::Utf7imap_to_utf8($fhir[0]); $folder->parentid = "0"; } else { $this->getModAndParentNames($fhir, $folder->displayname, $imapparent); $folder->displayname = Utils::Utf7imap_to_utf8($folder->displayname); if ($imapparent === null) { ZLog::Write(LOGLEVEL_WARN, sprintf("BackendIMAP->GetFolder('%s'): '%s'; we didn't found a valid parent name for the folder, but we should... contact the developers for further info", $id, $imapid)); $folder->parentid = "0"; // We put the folder as root folder, so we see it } else { $folder->parentid = $this->convertImapId($imapparent); } } $folder->type = SYNC_FOLDER_TYPE_USER_MAIL; } //advanced debugging ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->GetFolder('%s'): '%s'", $id, $folder)); return $folder; } /** * Returns folder stats. An associative array with properties is expected. * * @param string $id id of the folder * * @access public * @return array */ public function StatFolder($id) { $folder = $this->GetFolder($id); $stat = array(); $stat["id"] = $id; $stat["parent"] = $folder->parentid; $stat["mod"] = $folder->displayname; return $stat; } /** * Creates or modifies a folder * The folder type is ignored in IMAP, as all folders are Email folders * * @param string $folderid id of the parent folder * @param string $oldid if empty -> new folder created, else folder is to be renamed * @param string $displayname new folder name (to be created, or to be renamed to) * @param int $type folder type * * @access public * @return boolean status * @throws StatusException could throw specific SYNC_FSSTATUS_* exceptions * */ public function ChangeFolder($folderid, $oldid, $displayname, $type){ ZLog::Write(LOGLEVEL_INFO, sprintf("BackendIMAP->ChangeFolder('%s','%s','%s','%s')", $folderid, $oldid, $displayname, $type)); // if $id is set => rename mailbox, otherwise create if ($oldid) { // rename doesn't work properly with IMAP // the activesync client doesn't support a 'changing ID' // TODO this would be solved by implementing hex ids (Mantis #459) //$csts = imap_renamemailbox($this->mbox, $this->server . imap_utf7_encode(str_replace(".", $this->getServerDelimiter(), $oldid)), $newname); ZLog::Write(LOGLEVEL_ERROR, "BackendIMAP->ChangeFolder() : we do not support rename for now"); return false; } else { // build name for new mailboxBackendMaildir $displayname = Utils::Utf8_to_utf7imap($displayname); if ($folderid == "0") { $newimapid = $displayname; } else { $imapid = $this->getImapIdFromFolderId($folderid); $newimapid = $imapid . $this->getServerDelimiter() . $displayname; } $csts = imap_createmailbox($this->mbox, $this->server . $newimapid); ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->ChangeFolder() createmailbox: '%s'", $newimapid)); if ($csts) { imap_subscribe($this->mbox, $this->server . $newimapid); $newid = $this->convertImapId($newimapid); return $this->StatFolder($newid); } else { ZLog::Write(LOGLEVEL_WARN, "BackendIMAP->ChangeFolder() : mailbox creation failed"); return false; } } } /** * Deletes a folder * * @param string $id * @param string $parent is normally false * * @access public * @return boolean status - false if e.g. does not exist * @throws StatusException could throw specific SYNC_FSSTATUS_* exceptions * */ public function DeleteFolder($id, $parentid){ $imapid = $this->getImapIdFromFolderId($id); if ($imapid) { return imap_deletemailbox($this->mbox, $this->server.$imapid); } return false; } /** * Returns a list (array) of messages * * @param string $folderid id of the parent folder * @param long $cutoffdate timestamp in the past from which on messages should be returned * * @access public * @return array/false array with messages or false if folder is not available */ public function GetMessageList($folderid, $cutoffdate) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->GetMessageList('%s','%s')", $folderid, $cutoffdate)); $folderid = $this->getImapIdFromFolderId($folderid); if ($folderid == false) throw new StatusException("Folderid not found in cache", SYNC_STATUS_FOLDERHIERARCHYCHANGED); $messages = array(); $this->imap_reopen_folder($folderid, true); if ($cutoffdate > 0) { // IMAP SINCE search criteria $searchCriteria = "SINCE ". date("d-M-Y", $cutoffdate); // search messages in time range $search = @imap_search($this->mbox, $searchCriteria); if ($search === false) { ZLog::Write(LOGLEVEL_INFO, sprintf("BackendIMAP->GetMessageList('%s','%s'): 0 result for the search or error: %s", $folderid, $cutoffdate, imap_last_error())); return $messages; } $sequence = implode(",", $search); // search for forwarded messages in time range, because imap_fetch_overview() does not return $Forwarded flag $forwardedMessages = @imap_search($this->mbox, 'KEYWORD $Forwarded ' . $searchCriteria, SE_UID); } else { $sequence = "1:*"; // search for forwarded messages $forwardedMessages = @imap_search($this->mbox, 'KEYWORD $Forwarded', SE_UID); } ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->GetMessageList(): searching with sequence '%s'", $sequence)); $overviews = @imap_fetch_overview($this->mbox, $sequence); if (!is_array($overviews) || count($overviews) == 0) { $error = imap_last_error(); if (strlen($error) > 0 && imap_num_msg($this->mbox) > 0) { ZLog::Write(LOGLEVEL_WARN, sprintf("BackendIMAP->GetMessageList('%s','%s'): Failed to retrieve overview: %s", $folderid, $cutoffdate, imap_last_error())); } return $messages; } foreach ($overviews as $overview) { // Determine the message's date and apply the cutoff; if the overview's ->udate property is // not available, fall back to the "Date:" header as it appears in the email. $date = 0; if (isset($overview->udate)) { $date = $overview->udate; } else if (isset($overview->date)) { $date = $this->cleanupDate($overview->date); } if ($date < $cutoffdate) { // Message is out of range; ignore it continue; } // cut of deleted messages if (isset($overview->deleted) && $overview->deleted) continue; if (isset($overview->uid)) { $message = array(); $message["mod"] = $date; $message["id"] = $overview->uid; // 'seen' aka 'read' if (isset($overview->seen) && $overview->seen) { $message["flags"] = 1; } else { $message["flags"] = 0; } // 'answered' if (isset($overview->answered) && $overview->answered) { $message["answered"] = 1; } else { $message["answered"] = 0; } // 'forwarded' if (is_array($forwardedMessages) && in_array($overview->uid, $forwardedMessages)) { $message["forwarded"] = 1; } else { $message["forwarded"] = 0; } // 'flagged' aka 'FollowUp' aka 'starred' if (isset($overview->flagged) && $overview->flagged) { $message["star"] = 1; } else { $message["star"] = 0; } $messages[] = $message; } } return $messages; } /** * Returns the actual SyncXXX object type. * * @param string $folderid id of the parent folder * @param string $id id of the message * @param ContentParameters $contentparameters parameters of the requested message (truncation, mimesupport etc) * * @access public * @return object/false false if the message could not be retrieved */ public function GetMessage($folderid, $id, $contentparameters) { $truncsize = Utils::GetTruncSize($contentparameters->GetTruncation()); $mimesupport = $contentparameters->GetMimeSupport(); $bodypreference = $contentparameters->GetBodyPreference(); /* fmbiete's contribution r1528, ZP-320 */ ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->GetMessage('%s', '%s', '%s')", $folderid, $id, implode(",", $bodypreference))); $folderImapid = $this->getImapIdFromFolderId($folderid); $is_sent_folder = strcasecmp($folderImapid, $this->create_name_folder(IMAP_FOLDER_SENT)) == 0; // Get flags, etc $stat = $this->StatMessage($folderid, $id); if ($stat) { $this->imap_reopen_folder($folderImapid); $mail_headers = @imap_fetchheader($this->mbox, $id, FT_UID); $mail = $mail_headers . @imap_body($this->mbox, $id, FT_PEEK | FT_UID); if (empty($mail)) { throw new StatusException(sprintf("BackendIMAP->GetMessage(): Error, message not found, maybe was moved"), SYNC_ITEMOPERATIONSSTATUS_INVALIDATT); } $mobj = new Mail_mimeDecode($mail); $message = $mobj->decode(array('decode_headers' => 'utf-8', 'decode_bodies' => true, 'include_bodies' => true, 'rfc_822bodies' => true, 'charset' => 'utf-8')); unset($mobj); Utils::CheckAndFixEncodingInHeaders($mail, $message); $is_multipart = is_multipart($message); $is_smime = is_smime($message); $is_encrypted = $is_smime ? is_encrypted($message) : false; ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->GetMessage(): Message is multipart: %d, smime: %d, smime encrypted: %d", $is_multipart, $is_smime, $is_encrypted)); //Select body type preference $bpReturnType = SYNC_BODYPREFERENCE_PLAIN; if ($bodypreference !== false) { $bpReturnType = Utils::GetBodyPreferenceBestMatch($bodypreference); // changed by mku ZP-330 } ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->GetMessage(): getBodyPreferenceBestMatch: %d", $bpReturnType)); // Detect Apple iOS devices $isAppleIosDevice = false; $deviceType = strtolower(Request::GetDeviceType()); if ($deviceType == 'iphone' || $deviceType == 'ipad' || $deviceType == 'ipod') { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->GetMessage():: iOS device %s->%s detected", Request::GetDeviceType(), Request::GetUserAgent())); $isAppleIosDevice = true; } // Prefered format is MIME -OR- message is SMIME -OR- the device supports MIME (iPhone) and doesn't really understand HTML if ($bpReturnType == SYNC_BODYPREFERENCE_MIME || ($bpReturnType == SYNC_BODYPREFERENCE_HTML && $isAppleIosDevice) || $is_smime || in_array(SYNC_BODYPREFERENCE_MIME, $bodypreference)) { $bpReturnType = SYNC_BODYPREFERENCE_MIME; } // We need the text body even though MIME is used, for the preview $textBody = ""; Mail_mimeDecode::getBodyRecursive($message, "html", $textBody, true); if (strlen($textBody) > 0) { if ($bpReturnType != SYNC_BODYPREFERENCE_MIME) { $bpReturnType = SYNC_BODYPREFERENCE_HTML; } } else { Mail_mimeDecode::getBodyRecursive($message, "plain", $textBody, true); if ($bpReturnType != SYNC_BODYPREFERENCE_MIME) { $bpReturnType = SYNC_BODYPREFERENCE_PLAIN; } } ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->GetMessage(): after thinking a bit we will use: %d", $bpReturnType)); $output = new SyncMail(); if (Request::GetProtocolVersion() >= 12.0) { $output->asbody = new SyncBaseBody(); Utils::CheckAndFixEncoding($textBody); $data = ""; switch($bpReturnType) { case SYNC_BODYPREFERENCE_PLAIN: $data = $textBody; break; case SYNC_BODYPREFERENCE_HTML: $data = $textBody; break; case SYNC_BODYPREFERENCE_MIME: if ($is_smime) { if ($is_encrypted) { // #190, KD 2015-06-04 - If message body is encrypted only send the headers, as data should only be in the attachment $data = $mail_headers; } else { $data = $mail; } } else { $data = build_mime_message($message); } break; case SYNC_BODYPREFERENCE_RTF: ZLog::Write(LOGLEVEL_DEBUG, "BackendIMAP->GetMessage RTF Format NOT SUPPORTED"); // TODO: this is broken. This is no RTF. $data = base64_encode($textBody); break; } // truncate body, if requested. // MIME should not be truncated, but encrypted messages are truncated always to the headers size if ($bpReturnType == SYNC_BODYPREFERENCE_MIME) { if ($is_encrypted) { $output->asbody->truncated = 1; } else { $output->asbody->truncated = 0; } } else { if (strlen($data) > $truncsize) { $data = Utils::Utf8_truncate($data, $truncsize); $output->asbody->truncated = 1; } else { $output->asbody->truncated = 0; } } // indicate in open that the data is HTML so it can be truncated correctly if required $output->asbody->data = StringStreamWrapper::Open($data, ($bpReturnType == SYNC_BODYPREFERENCE_HTML)); $output->asbody->estimatedDataSize = strlen($data); unset($data); $output->asbody->type = $bpReturnType; if ($bpReturnType == SYNC_BODYPREFERENCE_MIME) { // NativeBodyType can be only (1 => PLAIN, 2 => HTML, 3 => RTF). MIME uses 1 $output->nativebodytype = SYNC_BODYPREFERENCE_PLAIN; } else { $output->nativebodytype = $bpReturnType; } $bpo = $contentparameters->BodyPreference($output->asbody->type); if (Request::GetProtocolVersion() >= 14.0 && $bpo->GetPreview()) { // Preview must be always plaintext $previewText = ""; Mail_mimeDecode::getBodyRecursive($message, "plain", $previewText, true); if (strlen($previewText) == 0) { Mail_mimeDecode::getBodyRecursive($message, "html", $previewText, true); $previewText = Utils::ConvertHtmlToText($previewText); } $output->asbody->preview = Utils::Utf8_truncate($previewText, $bpo->GetPreview()); } } /* END fmbiete's contribution r1528, ZP-320 */ else { // ASV_2.5 //DEPRECATED : very old devices, and incomplete code $output->bodytruncated = 0; $data = ""; if ($bpReturnType == SYNC_BODYPREFERENCE_MIME) { $data = $mail; } else { $data = $textBody; } $output->mimesize = strlen($data); if (strlen($data) > $truncsize && $bpReturnType != SYNC_BODYPREFERENCE_MIME) { $output->mimedata = StringStreamWrapper::Open(Utils::Utf8_truncate($data, $truncsize)); $output->mimetruncated = 1; } else { $output->mimetruncated = 0; $output->mimedata = StringStreamWrapper::Open($data); } unset($data); } unset($textBody); unset($mail_headers); $output->datereceived = isset($message->headers["date"]) ? $this->cleanupDate($message->headers["date"]) : null; if ($is_smime) { // #190, KD 2015-06-04 - Add Encrypted (and possibly signed) to the classifications emitted if ($is_encrypted) { $output->messageclass = "IPM.Note.SMIME"; } else { $output->messageclass = "IPM.Note.SMIME.MultipartSigned"; } } else { $output->messageclass = "IPM.Note"; } $output->subject = isset($message->headers["subject"]) ? $message->headers["subject"] : ""; $output->read = $stat["flags"]; $output->from = isset($message->headers["from"]) ? $message->headers["from"] : null; if (isset($message->headers["thread-topic"])) { $output->threadtopic = $message->headers["thread-topic"]; } else { $output->threadtopic = $output->subject; } // Language Code Page ID: http://msdn.microsoft.com/en-us/library/windows/desktop/dd317756%28v=vs.85%29.aspx $output->internetcpid = INTERNET_CPID_UTF8; if (Request::GetProtocolVersion() >= 12.0) { $output->contentclass = DEFAULT_EMAIL_CONTENTCLASS; $output->flag = new SyncMailFlags(); if (isset($stat["star"]) && $stat["star"]) { //flagstatus 0: clear, 1: complete, 2: active $output->flag->flagstatus = SYNC_FLAGSTATUS_ACTIVE; //flagtype: for follow up $output->flag->flagtype = "FollowUp"; } else { $output->flag->flagstatus = SYNC_FLAGSTATUS_CLEAR; } if (Request::GetProtocolVersion() >= 14.0) { //flagstatus 0: unknown, 1: replied, 2: replied-to-all, 3: forwarded if (isset($stat["answered"]) && $stat["answered"]) { $output->lastverbexecuted = SYNC_MAIL_LASTVERB_REPLYSENDER; } elseif (isset($stat["forwarded"]) && $stat["forwarded"]) { $output->lastverbexecuted = SYNC_MAIL_LASTVERB_FORWARD; } else { $output->lastverbexecuted = SYNC_MAIL_LASTVERB_UNKNOWN; } } } $Mail_RFC822 = new Mail_RFC822(); $toaddr = $ccaddr = $replytoaddr = array(); if(isset($message->headers["to"])) $toaddr = $Mail_RFC822->parseAddressList($message->headers["to"], null, null, false, null); if(isset($message->headers["cc"])) $ccaddr = $Mail_RFC822->parseAddressList($message->headers["cc"], null, null, false, null); if(isset($message->headers["reply-to"])) $replytoaddr = $Mail_RFC822->parseAddressList($message->headers["reply-to"], null, null, false, null); $output->to = array(); $output->cc = array(); $output->reply_to = array(); foreach(array("to" => $toaddr, "cc" => $ccaddr, "reply_to" => $replytoaddr) as $type => $addrlist) { if ($addrlist === false) { //If we couldn't parse the addresslist we put the raw header (decoded) if ($type == "reply_to") { array_push($output->$type, $message->headers["reply-to"]); } else { array_push($output->$type, $message->headers[$type]); } } else { foreach($addrlist as $addr) { // If the address was a group we have "groupname" and "addresses" atributes if (isset($addr->addresses)) { if (count($addr->addresses) == 0) { // readd the empty group delimiter array_push($output->$type, sprintf("%s:;", $addr->groupname)); if (!isset($output->displayto) && strlen($addr->groupname) > 0) { $output->displayto = $addr->groupname; } } else { foreach($addr->addresses as $addr_group) { $name = $this->add_address_to_list($output->$type, $addr_group); if (!isset($output->displayto) && strlen($name) > 0) { $output->displayto = $name; } } } } else { // Not a group $name = $this->add_address_to_list($output->$type, $addr); if (!isset($output->displayto) && strlen($name) > 0) { $output->displayto = $name; } } } } } // convert mime-importance to AS-importance using RFC4021, X-Priority or default to "normal" (ZP-320) //AS: 0 - low, 1 - normal, 2 - important if (isset($message->headers["importance"])) { //Importance: high, normal, low $mimeImportance = strtolower($message->headers["importance"]); if ($mimeImportance == "normal") { $output->importance = 1; } elseif ($mimeImportance == "high") { $output->importance = 2; } elseif ($mimeImportance == "low") { $output->importance = 0; } } elseif (isset($message->headers["x-priority"])) { //X-Priority: 1 - highest, 2 - high, 3 - normal, 4 - low, 5 - lowest $mimeImportance = preg_replace("/\D+/", "", $message->headers["x-priority"]); if ($mimeImportance == 3) { $output->importance = 1; } elseif ($mimeImportance < 3) { $output->importance = 2; } elseif ($mimeImportance > 3) { $output->importance = 0; } } else { $output->importance = 1; } // Attachments are also needed for MIME messages if(isset($message->parts)) { $mparts = $message->parts; for ($i=0; $i < count($mparts); $i++) { $part = $mparts[$i]; //recursively add subparts to later processing if ((isset($part->ctype_primary) && $part->ctype_primary == "multipart") && (isset($part->ctype_secondary) && ($part->ctype_secondary == "mixed" || $part->ctype_secondary == "alternative" || $part->ctype_secondary == "related"))) { if (isset($part->parts)) { foreach($part->parts as $spart) $mparts[] = $spart; } // Go to the for again continue; } if (is_calendar($part)) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->GetMessage(): text/calendar part found, trying to convert")); $output->meetingrequest = new SyncMeetingRequest(); parse_meeting_calendar($part, $output, $is_sent_folder); } else { //add part as attachment if it's disposition indicates so or if it is not a text part if ((isset($part->disposition) && ($part->disposition == "attachment" || $part->disposition == "inline")) || (isset($part->ctype_primary) && $part->ctype_primary != "text")) { if (isset($part->d_parameters['filename'])) $attname = $part->d_parameters['filename']; else if (isset($part->ctype_parameters['name'])) $attname = $part->ctype_parameters['name']; else if (isset($part->headers['content-description'])) $attname = $part->headers['content-description']; else $attname = "unknown attachment"; /* BEGIN fmbiete's contribution r1528, ZP-320 */ if (Request::GetProtocolVersion() >= 12.0) { if (!isset($output->asattachments) || !is_array($output->asattachments)) $output->asattachments = array(); $attachment = new SyncBaseAttachment(); $attachment->estimatedDataSize = isset($part->d_parameters['size']) ? $part->d_parameters['size'] : (isset($part->body) ? strlen($part->body) : 0); $attachment->displayname = $attname; $attachment->filereference = $folderid . ":" . $id . ":" . $i; $attachment->method = 1; //Normal attachment $attachment->contentid = isset($part->headers['content-id']) ? str_replace("<", "", str_replace(">", "", $part->headers['content-id'])) : ""; if (isset($part->disposition) && $part->disposition == "inline") { $attachment->isinline = 1; // #209 - KD 2015-06-16 If we got a filename use it, otherwise guess if (!isset($part->filename)) { // We try to fix the name for the inline file. // FIXME: This is a dirty hack as the used in the Zarafa backend, if you have a better method let me know! if (isset($part->ctype_primary) && isset($part->ctype_secondary)) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->GetMessage(): Guessing extension for inline attachment [primary_type %s secondary_type %s]", $part->ctype_primary, $part->ctype_secondary)); if (isset(BackendIMAP::$mimeTypes[$part->ctype_primary.'/'.$part->ctype_secondary])) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->GetMessage(): primary_type %s secondary_type %s", $part->ctype_primary, $part->ctype_secondary)); $attachment->displayname = "inline_".$i.".".BackendIMAP::$mimeTypes[$part->ctype_primary.'/'.$part->ctype_secondary]; } else { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->GetMessage(): no extension found in '%s'!!", SYSTEM_MIME_TYPES_MAPPING)); } } else { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->GetMessage(): no primary_type or secondary_type")); } } } else { $attachment->isinline = 0; } array_push($output->asattachments, $attachment); } else { //ASV_2.5 if (!isset($output->attachments) || !is_array($output->attachments)) $output->attachments = array(); $attachment = new SyncAttachment(); $attachment->attsize = isset($part->d_parameters['size']) ? $part->d_parameters['size'] : (isset($part->body) ? strlen($part->body) : 0); $attachment->displayname = $attname; $attachment->attname = $folderid . ":" . $id . ":" . $i; $attachment->attmethod = 1; $attachment->attoid = isset($part->headers['content-id']) ? str_replace("<", "", str_replace(">", "", $part->headers['content-id'])) : ""; array_push($output->attachments, $attachment); } /* END fmbiete's contribution r1528, ZP-320 */ } } } } unset($message); unset($mail); return $output; } return false; } /** * Returns message stats, analogous to the folder stats from StatFolder(). * * @param string $folderid id of the folder * @param string $id id of the message * * @access public * @return array/boolean */ public function StatMessage($folderid, $id) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->StatMessage('%s','%s')", $folderid, $id)); $folderImapid = $this->getImapIdFromFolderId($folderid); $this->imap_reopen_folder($folderImapid); $overview = @imap_fetch_overview($this->mbox, $id, FT_UID); if (!$overview) { ZLog::Write(LOGLEVEL_WARN, sprintf("BackendIMAP->StatMessage('%s','%s'): Failed to retrieve overview: %s", $folderid, $id, imap_last_error())); return false; } // without uid it's not a valid message if (empty($overview[0]->uid)) return false; // search for the specific uid and keyword/flag, because imap_fetch_overview() does not return $Forwarded flag $forwardedMessages = @imap_search($this->mbox, 'KEYWORD $Forwarded', SE_UID); $entry = array(); if (isset($overview[0]->udate)) { $entry["mod"] = $overview[0]->udate; } else if (isset($overview[0]->date)) { $entry["mod"] = $this->cleanupDate($overview[0]->date); } else { $entry["mod"] = 0; } $entry["id"] = $overview[0]->uid; // 'seen' aka 'read' if (isset($overview[0]->seen) && $overview[0]->seen) { $entry["flags"] = 1; } else { $entry["flags"] = 0; } // 'answered' if (isset($overview[0]->answered) && $overview[0]->answered) { $entry["answered"] = 1; } else { $entry["answered"] = 0; } // 'forwarded' if (is_array($forwardedMessages) && in_array($overview[0]->uid, $forwardedMessages)) { $entry["forwarded"] = 1; } else { $entry["forwarded"] = 0; } // 'flagged' aka 'FollowUp' aka 'starred' if (isset($overview[0]->flagged) && $overview[0]->flagged) { $entry["star"] = 1; } else { $entry["star"] = 0; } return $entry; } /** * Called when a message has been changed on the mobile. * Added support for FollowUp flag * * @param string $folderid id of the folder * @param string $id id of the message * @param SyncXXX $message the SyncObject containing a message * @param ContentParameters $contentparameters * * @access public * @return array same return value as StatMessage() * @throws StatusException could throw specific SYNC_STATUS_* exceptions */ public function ChangeMessage($folderid, $id, $message, $contentparameters) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->ChangeMessage('%s','%s','%s')", $folderid, $id, get_class($message))); // TODO this could throw several StatusExceptions like e.g. SYNC_STATUS_OBJECTNOTFOUND, SYNC_STATUS_SYNCCANNOTBECOMPLETED if (isset($message->flag)) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->ChangeMessage('Setting flag')")); $folderImapid = $this->getImapIdFromFolderId($folderid); $this->imap_reopen_folder($folderImapid); if ($this->imap_inside_cutoffdate(Utils::GetCutOffDate($contentparameters->GetFilterType()), $id)) { if (isset($message->flag->flagstatus) && $message->flag->flagstatus == 2) { ZLog::Write(LOGLEVEL_DEBUG, "Set On FollowUp -> IMAP Flagged"); $status = @imap_setflag_full($this->mbox, $id, "\\Flagged", ST_UID); } else { ZLog::Write(LOGLEVEL_DEBUG, "Clearing Flagged"); $status = @imap_clearflag_full($this->mbox, $id, "\\Flagged", ST_UID); } if ($status) { ZLog::Write(LOGLEVEL_DEBUG, "Flagged changed"); } else { ZLog::Write(LOGLEVEL_DEBUG, "Flagged failed"); } } else { throw new StatusException(sprintf("BackendIMAP->ChangeMessage(): Message is outside the sync range"), SYNC_STATUS_SYNCCANNOTBECOMPLETED); } } return $this->StatMessage($folderid, $id); } /** * Changes the 'read' flag of a message on disk * * @param string $folderid id of the folder * @param string $id id of the message * @param int $flags read flag of the message * @param ContentParameters $contentparameters * * @access public * @return boolean status of the operation * @throws StatusException could throw specific SYNC_STATUS_* exceptions */ public function SetReadFlag($folderid, $id, $flags, $contentparameters) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->SetReadFlag('%s','%s','%s')", $folderid, $id, $flags)); $folderImapid = $this->getImapIdFromFolderId($folderid); $this->imap_reopen_folder($folderImapid); if ($this->imap_inside_cutoffdate(Utils::GetCutOffDate($contentparameters->GetFilterType()), $id)) { if ($flags == 0) { // set as "Unseen" (unread) $status = @imap_clearflag_full($this->mbox, $id, "\\Seen", ST_UID); } else { // set as "Seen" (read) $status = @imap_setflag_full($this->mbox, $id, "\\Seen", ST_UID); } } else { throw new StatusException(sprintf("BackendIMAP->SetReadFlag(): Message is outside the sync range"), SYNC_STATUS_OBJECTNOTFOUND); } return $status; } /** * Called when the user has requested to delete (really delete) a message * * @param string $folderid id of the folder * @param string $id id of the message * @param ContentParameters $contentparameters * * @access public * @return boolean status of the operation * @throws StatusException could throw specific SYNC_STATUS_* exceptions */ public function DeleteMessage($folderid, $id, $contentparameters) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->DeleteMessage('%s','%s')", $folderid, $id)); $folderImapid = $this->getImapIdFromFolderId($folderid); if (strcasecmp($folderImapid, $this->create_name_folder(IMAP_FOLDER_TRASH)) != 0) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->DeleteMessage('%s','%s') move message to trash folder", $folderid, $id)); return $this->MoveMessage($folderid, $id, $this->create_name_folder(IMAP_FOLDER_TRASH), $contentparameters); } $this->imap_reopen_folder($folderImapid); if ($this->imap_inside_cutoffdate(Utils::GetCutOffDate($contentparameters->GetFilterType()), $id)) { $s1 = @imap_delete ($this->mbox, $id, FT_UID); $s11 = @imap_setflag_full($this->mbox, $id, "\\Deleted", FT_UID); $s2 = @imap_expunge($this->mbox); ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->DeleteMessage('%s','%s'): result: s-delete: '%s' s-expunge: '%s' setflag: '%s'", $folderid, $id, $s1, $s2, $s11)); } else { throw new StatusException(sprintf("BackendIMAP->DeleteMessage(): Message is outside the sync range"), SYNC_STATUS_OBJECTNOTFOUND); } return ($s1 && $s2 && $s11); } /** * Called when the user moves an item on the PDA from one folder to another * * @param string $folderid id of the source folder * @param string $id id of the message * @param string $newfolderid id of the destination folder * @param ContentParameters $contentparameters * * @access public * @return boolean status of the operation * @throws StatusException could throw specific SYNC_MOVEITEMSSTATUS_* exceptions */ public function MoveMessage($folderid, $id, $newfolderid, $contentparameters) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->MoveMessage('%s','%s','%s')", $folderid, $id, $newfolderid)); $folderImapid = $this->getImapIdFromFolderId($folderid); $newfolderImapid = $this->getImapIdFromFolderId($newfolderid); if ($folderImapid == $newfolderImapid) { throw new StatusException(sprintf("BackendIMAP->MoveMessage('%s','%s','%s'): Error, destination folder is source folder. Canceling the move.", $folderid, $id, $newfolderid), SYNC_MOVEITEMSSTATUS_SAMESOURCEANDDEST); } $this->imap_reopen_folder($folderImapid); if ($this->imap_inside_cutoffdate(Utils::GetCutOffDate($contentparameters->GetFilterType()), $id)) { // read message flags $overview = @imap_fetch_overview($this->mbox, $id, FT_UID); if (!is_array($overview) || count($overview) == 0) { throw new StatusException(sprintf("BackendIMAP->MoveMessage('%s','%s','%s'): Error, unable to retrieve overview of source message: %s", $folderid, $id, $newfolderid, imap_last_error()), SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID); } else { // get next UID for destination folder // when moving a message we have to announce through ActiveSync the new messageID in the // destination folder. This is a "guessing" mechanism as IMAP does not inform that value. // when lots of simultaneous operations happen in the destination folder this could fail. // in the worst case the moved message is displayed twice on the mobile. $destStatus = imap_status($this->mbox, $this->server . $newfolderImapid, SA_ALL); if (!$destStatus) throw new StatusException(sprintf("BackendIMAP->MoveMessage('%s','%s','%s'): Error, unable to open destination folder: %s", $folderid, $id, $newfolderid, imap_last_error()), SYNC_MOVEITEMSSTATUS_INVALIDDESTID); $newid = $destStatus->uidnext; // move message $s1 = imap_mail_move($this->mbox, $id, $newfolderImapid, CP_UID); if (!$s1) throw new StatusException(sprintf("BackendIMAP->MoveMessage('%s','%s','%s'): Error, copy to destination folder failed: %s", $folderid, $id, $newfolderid, imap_last_error()), SYNC_MOVEITEMSSTATUS_CANNOTMOVE); // delete message in from-folder $s2 = imap_expunge($this->mbox); // open new folder $stat = $this->imap_reopen_folder($newfolderImapid); if (!$stat) throw new StatusException(sprintf("BackendIMAP->MoveMessage('%s','%s','%s'): Error, opening the destination folder: %s", $folderid, $id, $newfolderid, imap_last_error()), SYNC_MOVEITEMSSTATUS_CANNOTMOVE); // remove all flags $s3 = @imap_clearflag_full($this->mbox, $newid, "\\Seen \\Answered \\Flagged \\Deleted \\Draft", FT_UID); $newflags = ""; $move_to_trash = strcasecmp($newfolderImapid, $this->create_name_folder(IMAP_FOLDER_TRASH)) == 0; if ($overview[0]->seen || ($move_to_trash && defined('IMAP_AUTOSEEN_ON_DELETE') && IMAP_AUTOSEEN_ON_DELETE == true)) $newflags .= "\\Seen"; if ($overview[0]->flagged) $newflags .= " \\Flagged"; if ($overview[0]->answered) $newflags .= " \\Answered"; $s4 = @imap_setflag_full ($this->mbox, $newid, $newflags, FT_UID); ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->MoveMessage('%s','%s','%s'): result s-move: '%s' s-expunge: '%s' unset-Flags: '%s' set-Flags: '%s'", $folderid, $id, $newfolderid, Utils::PrintAsString($s1), Utils::PrintAsString($s2), Utils::PrintAsString($s3), Utils::PrintAsString($s4))); // return the new id "as string" return $newid . ""; } } else { throw new StatusException(sprintf("BackendIMAP->MoveMessage(): Message is outside the sync range"), SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID); } } /** * Processes a response to a meeting request. * * @param string $requestid id of the object containing the request * @param string $folderid id of the parent folder of $requestid * @param string $response * * @access public * @return string id of the created/updated calendar obj * @throws StatusException */ public function MeetingResponse($requestid, $folderid, $response) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->MeetingResponse('%s','%s','%s')", $requestid, $folderid, $response)); $folderImapid = $this->getImapIdFromFolderId($folderid); $this->imap_reopen_folder($folderImapid); $mail = @imap_fetchheader($this->mbox, $requestid, FT_UID) . @imap_body($this->mbox, $requestid, FT_PEEK | FT_UID); if (empty($mail)) { throw new StatusException("BackendIMAP->MeetingResponse(): Error, message not found, maybe was moved", SYNC_ITEMOPERATIONSSTATUS_INVALIDATT); } // Get the original calendar request, so we don't need to create it from scratch $mobj = new Mail_mimeDecode($mail); unset($mail); $message = $mobj->decode(array('decode_headers' => 'utf-8', 'decode_bodies' => true, 'include_bodies' => true, 'rfc_822bodies' => true, 'charset' => 'utf-8')); unset($mobj); $body_part = null; if(isset($message->parts)) { $mparts = $message->parts; for ($i=0; $i < count($mparts); $i++) { $part = $mparts[$i]; //recursively add parts if ((isset($part->ctype_primary) && $part->ctype_primary == "multipart") && (isset($part->ctype_secondary) && ($part->ctype_secondary == "mixed" || $part->ctype_secondary == "alternative" || $part->ctype_secondary == "related"))) { foreach($part->parts as $spart) $mparts[] = $spart; continue; } if (is_calendar($part)) { ZLog::Write(LOGLEVEL_DEBUG, "BackendIMAP->MeetingResponse - text/calendar part found, trying to reply"); $body_part = reply_meeting_calendar($part, $response, $this->GetUserDetails($this->username)['emailaddress']); } } unset($mparts); } unset($message); if ($body_part === null) { throw new StatusException("BackendIMAP->MeetingResponse(): Error, no calendar part modified", SYNC_ITEMOPERATIONSSTATUS_INVALIDATT); } $uuid_calendar = ""; switch($response) { case 1: // ACCEPTED case 2: // TENTATIVE $uuid_calendar = create_calendar_dav($body_part); break; case 3: // DECLINED // Do nothing break; } // We don't need to send a reply, because the client will do it // Remove message: answered invitation // Roundcube client doesn't remove the original message, but Zarafa backend does $s1 = @imap_delete ($this->mbox, $requestid, FT_UID); $s11 = @imap_setflag_full($this->mbox, $requestid, "\\Deleted", FT_UID); $s2 = @imap_expunge($this->mbox); ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->MeetingResponse('%s','%s'): removing message result: s-delete: '%s' s-expunge: '%s' setflag: '%s'", $folderid, $requestid, $s1, $s2, $s11)); return $uuid_calendar; } /** * Resolves recipients * * @param SyncObject $resolveRecipients * * @access public * @return SyncObject $resolveRecipients */ public function ResolveRecipients($resolveRecipients) { // TODO: return false; } /** * Returns the email address and the display name of the user. Used by autodiscover. * * @param string $username The username * * @access public * @return Array */ public function GetUserDetails($username) { $email = $username; if (!USE_FULLEMAIL_FOR_LOGIN) { $email = getDefaultEmailValue($username, $this->domain); } return array('emailaddress' => $email, 'fullname' => getDefaultFullNameValue($username, $this->domain)); } /** * Applies settings to and gets informations from the device * * @param SyncObject $settings (SyncOOF, SyncUserInformation, SyncRightsManagementTemplates possible) * * @access public * @return SyncObject $settings */ public function Settings($settings) { if ($settings instanceof SyncOOF) { $this->settingsOOF($settings); } elseif ($settings instanceof SyncUserInformation) { $this->settingsUserInformation($settings); } elseif ($settings instanceof SyncRightsManagementTemplates) { $settings->Status = SYNC_COMMONSTATUS_IRMFEATUREDISABLED; } return $settings; } /** * Indicates which AS version is supported by the backend. * * @access public * @return string AS version constant */ public function GetSupportedASVersion() { return ZPush::ASV_14; } /** * Returns the BackendIMAP as it implements the ISearchProvider interface * This could be overwritten by the global configuration * * @access public * @return object Implementation of ISearchProvider */ public function GetSearchProvider() { return $this; } /**---------------------------------------------------------------------------------------------------------- * public ISearchProvider methods */ /** * Indicates if a search type is supported by this SearchProvider * * @param string $searchtype * * @access public * @return boolean */ public function SupportsType($searchtype) { return ($searchtype == ISearchProvider::SEARCH_MAILBOX); } /** * Queries the IMAP backend * * @param string $searchquery string to be searched for * @param string $searchrange specified searchrange * @param SyncResolveRecipientsPicture $searchpicture limitations for picture * * @access public * @return array search results * @throws StatusException */ public function GetGALSearchResults($searchquery, $searchrange, $searchpicture) { return false; } /** * Searches for the emails on the server * * @param ContentParameter $cpo * @param string $prefix If used with the combined backend here will come the backend id and delimiter * * @return array */ public function GetMailboxSearchResults($cpo, $prefix = '') { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->GetMailboxSearchResults()")); $searchFolderId = $cpo->GetSearchFolderid(); $searchRange = explode('-', $cpo->GetSearchRange()); $filter = $this->getSearchRestriction($cpo); // Open the folder to search $search = true; if (empty($searchFolderId)) { $searchFolderId = $this->getFolderIdFromImapId($this->create_name_folder(IMAP_FOLDER_INBOX), false); } // Convert searchFolderId to IMAP id $imapId = $this->getImapIdFromFolderId($searchFolderId); $items = array(); $listMessages = array(); $numMessages = 0; ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->GetMailboxSearchResults: Filter <%s>", $filter)); if ($cpo->GetSearchDeepTraversal()) { // Recursive search ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->GetMailboxSearchResults: Recursive search %s", $imapId)); $listFolders = @imap_list($this->mbox, $this->server, "*"); if ($listFolders === false) { ZLog::Write(LOGLEVEL_WARN, sprintf("BackendIMAP->GetMailboxSearchResults: Error recursive list %s", imap_last_error())); } else { foreach ($listFolders as $subFolder) { if (@imap_reopen($this->mbox, $subFolder)) { $imapSubFolder = str_replace($this->server, "", $subFolder); $subFolderId = $this->getFolderIdFromImapId($imapSubFolder); if ($subFolderId !== false) { // only search found folders $subList = @imap_search($this->mbox, $filter, SE_UID, IMAP_SEARCH_CHARSET); if ($subList !== false) { $numMessages += count($subList); ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->GetMailboxSearchResults: SubSearch in %s : %s ocurrences", $imapSubFolder, count($subList))); $listMessages[] = array($subFolderId => $subList); } } } } } } else { // Search in folder if (@imap_reopen($this->mbox, $this->server . $imapId)) { $subList = @imap_search($this->mbox, $filter, SE_UID, IMAP_SEARCH_CHARSET); if ($subList !== false) { $numMessages += count($subList); ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->GetMailboxSearchResults: Search in %s : %s ocurrences", $imapId, count($subList))); $listMessages[] = array($searchFolderId => $subList); } } } if ($numMessages > 0) { // range for the search results $rangestart = 0; $rangeend = SEARCH_MAXRESULTS - 1; if (is_array($searchRange) && isset($searchRange[0]) && isset($searchRange[1])) { $rangestart = $searchRange[0]; $rangeend = $searchRange[1]; } $querycnt = $numMessages; $querylimit = (($rangeend + 1) < $querycnt) ? ($rangeend + 1) : $querycnt + 1; $items['range'] = $rangestart.'-'.($querylimit - 1); $items['searchtotal'] = $querycnt; ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->GetMailboxSearchResults: %s entries found, returning %s", $items['searchtotal'], $items['range'])); $p = 0; $pc = 0; for ($i = $rangestart, $j = 0; $i <= $rangeend && $i < $querycnt; $i++, $j++) { $keys = array_keys($listMessages[$p]); $cntFolder = count($listMessages[$p][$keys[0]]); if ($pc >= $cntFolder) { $p++; $pc = 0; $keys = array_keys($listMessages[$p]); } ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->GetMailboxSearchResults: %s %s %s %s", $p, $pc, $keys[0], $listMessages[$p][$keys[0]][$pc])); $foundFolderId = $keys[0]; $items[$j]['class'] = 'Email'; $items[$j]['longid'] = $prefix . $foundFolderId . ":" . $listMessages[$p][$foundFolderId][$pc]; $items[$j]['folderid'] = $prefix . $foundFolderId; $pc++; } } else { $items['range'] = $cpo->GetSearchRange(); $items['searchtotal'] = 0; ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->GetMailboxSearchResults: No messages found!")); } return $items; } /** * Terminates a search for a given PID * * @param int $pid * * @return boolean */ public function TerminateSearch($pid) { return true; } /** * Disconnects from IMAP * * @access public * @return boolean */ public function Disconnect() { // Don't close the mailbox, we will need it open in the Backend methods return true; } /** * Creates a search restriction * * @param ContentParameter $cpo * @return string */ private function getSearchRestriction($cpo) { $searchText = $cpo->GetSearchFreeText(); $searchGreater = $cpo->GetSearchValueGreater() ? strftime("%Y-%m-%d", strtotime($cpo->GetSearchValueGreater())) : ''; $searchLess = $cpo->GetSearchValueLess() ? strftime("%Y-%m-%d", strtotime($cpo->GetSearchValueLess())) : ''; $filter = ''; if ($searchGreater != '') { $filter .= ' SINCE "' . $searchGreater . '"'; } else { // Only search in sync messages $limitdate = new DateTime(); switch (SYNC_FILTERTIME_MAX) { case SYNC_FILTERTYPE_1DAY: $limitdate = $limitdate->sub(new DateInterval("P1D")); break; case SYNC_FILTERTYPE_3DAYS: $limitdate = $limitdate->sub(new DateInterval("P3D")); break; case SYNC_FILTERTYPE_1WEEK: $limitdate = $limitdate->sub(new DateInterval("P1W")); break; case SYNC_FILTERTYPE_2WEEKS: $limitdate = $limitdate->sub(new DateInterval("P2W")); break; case SYNC_FILTERTYPE_1MONTH: $limitdate = $limitdate->sub(new DateInterval("P1M")); break; case SYNC_FILTERTYPE_3MONTHS: $limitdate = $limitdate->sub(new DateInterval("P3M")); break; case SYNC_FILTERTYPE_6MONTHS: $limitdate = $limitdate->sub(new DateInterval("P6M")); break; default: $limitdate = false; break; } if ($limitdate !== false) { // date format : 7 Jan 2012 $filter .= ' SINCE "' . ($limitdate->format("d M Y")) . '"'; } } if ($searchLess != '') { $filter .= ' BEFORE "' . $searchLess . '"'; } $filter .= ' TEXT "' . $searchText . '"'; return $filter; } /**---------------------------------------------------------------------------------------------------------- * protected IMAP methods */ /** * Unmasks a hex folderid and returns the imap folder id * * @param string $folderid hex folderid generated by convertImapId() * * @access protected * @return string imap folder id */ protected function getImapIdFromFolderId($folderid) { $this->InitializePermanentStorage(); if (isset($this->permanentStorage->fmFidFimap)) { if (isset($this->permanentStorage->fmFidFimap[$folderid])) { $imapId = $this->permanentStorage->fmFidFimap[$folderid]; ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->getImapIdFromFolderId('%s') = %s", $folderid, $imapId)); return $imapId; } else { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->getImapIdFromFolderId('%s') = %s", $folderid, 'not found')); return false; } } ZLog::Write(LOGLEVEL_WARN, sprintf("BackendIMAP->getImapIdFromFolderId('%s') = %s", $folderid, 'not initialized!')); return false; } /** * Retrieves a hex folderid previousily masked imap * * @param string $imapid Imap folder id * * @access protected * @return string hex folder id */ protected function getFolderIdFromImapId($imapid, $case_sensitive = true) { $this->InitializePermanentStorage(); if (!isset($this->permanentStorage->fmFimapFid)) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->getFolderIdFromImapId('%s') IMAP cache folder not found, creating one", $imapid)); // folderId to folderImap mapping $this->permanentStorage->fmFidFimap = array(); // folderImap to folderId mapping $this->permanentStorage->fmFimapFid = array(); // folderImap to folderId mapping - lowercase $this->permanentStorage->fmFimapFidLowercase = array(); $this->GetFolderList(); } if ($case_sensitive && isset($this->permanentStorage->fmFimapFid[$imapid])) { $folderid = $this->permanentStorage->fmFimapFid[$imapid]; ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->getFolderIdFromImapId('%s') = %s", $imapid, $folderid)); return $folderid; } if (!$case_sensitive && isset($this->permanentStorage->fmFimapFidLowercase[strtolower($imapid)])) { $folderid = $this->permanentStorage->fmFimapFidLowercase[strtolower($imapid)]; ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->getFolderIdFromImapId('%s', false) = %s", $imapid, $folderid)); return $folderid; } ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->getFolderIdFromImapId('%s', '%s') = %s", $imapid, Utils::PrintAsString($case_sensitive), 'not found')); return false; } /** * Masks a imap folder id into a generated hex folderid * The method getFolderIdFromImapId() is consulted so that an * imapid always returns the same hex folder id * * @param string $imapid Imap folder id * * @access protected * @return string hex folder id */ protected function convertImapId($imapid) { $this->InitializePermanentStorage(); // check if this imap id was converted before $folderid = $this->getFolderIdFromImapId($imapid); // nothing found, so generate a new id and put it in the cache if (!$folderid) { // generate folderid and add it to the mapping $folderid = sprintf('%04x%04x', mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff )); // // folderId to folderImap mapping // if (!isset($this->permanentStorage->fmFidFimap)) // $this->permanentStorage->fmFidFimap = array(); $a = $this->permanentStorage->fmFidFimap; $a[$folderid] = $imapid; $this->permanentStorage->fmFidFimap = $a; // // folderImap to folderid mapping // if (!isset($this->permanentStorage->fmFimapFid)) // $this->permanentStorage->fmFimapFid = array(); $b = $this->permanentStorage->fmFimapFid; $b[$imapid] = $folderid; $this->permanentStorage->fmFimapFid = $b; // if (!isset($this->permanentStorage->fmFimapFidLowercase)) // $this->permanentStorage->fmFimapFidLowercase = array(); $c = $this->permanentStorage->fmFimapFidLowercase; $c[strtolower($imapid)] = $folderid; $this->permanentStorage->fmFimapFidLowercase = $c; } ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->convertImapId('%s') = %s", $imapid, $folderid)); return $folderid; } /** * Returns the serverdelimiter for folder parsing * * @access protected * @return string delimiter */ protected function getServerDelimiter() { $this->InitializePermanentStorage(); if (isset($this->permanentStorage->serverdelimiter)) { return $this->permanentStorage->serverdelimiter; } $list = @imap_getmailboxes($this->mbox, $this->server, "*"); if (is_array($list) && count($list) > 0) { // get the delimiter from the first folder $delimiter = $list[0]->delimiter; $this->permanentStorage->serverdelimiter = $delimiter; } else { // default $delimiter = "."; } return $delimiter; } /** * Helper to re-initialize the folder to speed things up * Remember what folder is currently open and only change if necessary * * @param string $folderid id of the folder * @param boolean $force re-open the folder even if currently opened * * @access protected * @return boolean if folder is opened */ protected function imap_reopen_folder($folderid, $force = false) { // Reconnect $this->imap_reconnect(); // to see changes, the folder has to be reopened! if ($this->mboxFolder != $folderid || $force) { $s = @imap_reopen($this->mbox, $this->server . $folderid); // TODO throw status exception if (!$s) { ZLog::Write(LOGLEVEL_WARN, sprintf("BackendIMAP->imap_reopen_folder('%s'): failed to change folder: %s", $folderid, implode(", ", imap_errors()))); return false; } $this->mboxFolder = $folderid; } return true; } /** * Reconnect IMAP connection if needed * * @access private */ private function imap_reconnect() { if ($this->mbox) { imap_ping($this->mbox); } else { $this->mbox = @imap_open($this->server, $this->username, $this->password, OP_HALFOPEN, 0, $this->imapParams); $this->mboxFolder = ""; } } /** * Creates a new IMAP folder. * * @param string $foldername full folder name * * @access private * @return boolean success */ private function imap_create_folder($foldername) { $name = Utils::Utf8_to_utf7imap($foldername); $res = @imap_createmailbox($this->mbox, $name); if ($res) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->imap_create_folder('%s'): new folder created", $foldername)); } else { ZLog::Write(LOGLEVEL_WARN, sprintf("BackendIMAP->imap_create_folder('%s'): failed to create folder: %s", $foldername, implode(", ", imap_errors()))); } return $res; } /** * Check if the message was sent before the cutoffdate. * * @access private * @param integer $cutoffdate EPOCH of the bottom sync range. 0 if no range is defined * @param integer $id Message id * @return boolean */ private function imap_inside_cutoffdate($cutoffdate, $id) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->imap_inside_cutoffdate(): Checking if the messages is withing the cutoffdate %d, %s", $cutoffdate, $id)); $is_inside = false; if ($cutoffdate == 0) { // No cutoffdate, all the messages are in range $is_inside = true; ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->imap_inside_cutoffdate(): No cutoffdate, all the messages are in range")); } else { $overview = imap_fetch_overview($this->mbox, $id, FT_UID); if (is_array($overview)) { if (isset($overview[0]->date)) { $epoch_sent = strtotime($overview[0]->date); if ( $epoch_sent === false ) { $pattern1 = '/^(Sun|Mon|Tue|Wed|Thu|Fri|Sat), [0-9]+ (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) [0-9]+ [0-9]+:[0-9]+(:[0-9]+)* [+-]+[0-9]+/'; $pattern2 = '/^(Sun|Mon|Tue|Wed|Thu|Fri|Sat), [0-9]+ (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) [0-9]+ [0-9]+:[0-9]+(:[0-9]+)* /'; if (preg_match($pattern1, $overview[0]->date, $matches) == 1) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->imap_inside_cutoffdate(): date: %s, match: %s", $overview[0]->date, $matches[0])); $epoch_sent = strtotime($matches[0]); } else if (preg_match($pattern2, $overview[0]->date, $matches) == 1) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->imap_inside_cutoffdate(): date: %s, match: %s", $overview[0]->date, $matches[0])); $epoch_sent = strtotime($matches[0].' UTC'); } } ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->imap_inside_cutoffdate(): cutoffdate: %s, epoch_sent: %s", $cutoffdate, $epoch_sent)); $is_inside = ($cutoffdate <= $epoch_sent); ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->imap_inside_cutoffdate(): Message is %s cutoffdate range", ($is_inside ? "INSIDE" : "OUTSIDE"))); } else { // No sent date defined, that's a buggy message but we will think that the message is in range $is_inside = true; ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->imap_inside_cutoffdate(): No sent date defined, that's a buggy message but we will think that the message is in range")); } } else { // No overview, maybe the message is no longer there $is_inside = false; ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->imap_inside_cutoffdate(): No overview, maybe the message is no longer there")); } } return $is_inside; } /** * Adds a message with seen flag to a specified folder (used for saving sent items) * * @param string $folderid id of the folder * @param string $header header of the message * @param long $body body of the message * * @access protected * @return boolean status */ protected function addSentMessage($folderid, $header, $body) { $header_body = str_replace("\n", "\r\n", str_replace("\r", "", $header . "\n\n" . $body)); return @imap_append($this->mbox, $this->server . $folderid, $header_body, "\\Seen"); } /** * Parses an mimedecode address array back to a simple "," separated string * * @param array $ad addresses array * * @access protected * @return string mail address(es) string */ protected function parseAddr($ad) { $addr_string = ""; if (isset($ad) && is_array($ad)) { foreach($ad as $addr) { if ($addr_string) $addr_string .= ","; $addr_string .= $addr->mailbox . "@" . $addr->host; } } return $addr_string; } /** * Recursive way to get mod and parent - repeat until only one part is left * or the folder is identified as an IMAP folder * * @param string $fhir folder hierarchy string * @param string &$displayname reference of the displayname * @param long &$parent reference of the parent folder * * @access protected * @return */ protected function getModAndParentNames($fhir, &$displayname, &$parent) { // if mod is already set add the previous part to it as it might be a folder which has delimiter in its name $displayname = (isset($displayname) && strlen($displayname) > 0) ? $displayname = array_pop($fhir) . $this->getServerDelimiter() . $displayname : array_pop($fhir); $parent = implode($this->getServerDelimiter(), $fhir); if (count($fhir) == 1 || $this->checkIfIMAPFolder($parent)) { return; } //recursion magic $this->getModAndParentNames($fhir, $displayname, $parent); } /** * Prepare the folder name to get the type and parent. * * @param string $folder_name * @return string * @access private */ private function create_name_folder($folder_name) { $foldername = $folder_name; // If we have defined a folder prefix, and it's not empty if (defined('IMAP_FOLDER_PREFIX') && IMAP_FOLDER_PREFIX != "") { // If inbox uses prefix or we are not evaluating inbox if (IMAP_FOLDER_PREFIX_IN_INBOX == true || strcasecmp($foldername, IMAP_FOLDER_INBOX) != 0) { $foldername = IMAP_FOLDER_PREFIX . $this->getServerDelimiter() . $foldername; } } return $foldername; } /** * Checks if a specified name is a folder in the IMAP store * * @param string $foldername a foldername * * @access protected * @return boolean */ protected function checkIfIMAPFolder($folderName) { $folder_name = $folderName; if (defined('IMAP_FOLDER_PREFIX') && strlen(IMAP_FOLDER_PREFIX) > 0) { // TODO: We don't care about the inbox exception with the prefix, because we won't check inbox $folder_name = IMAP_FOLDER_PREFIX . $this->getServerDelimiter() . $folder_name; } $list_subfolders = @imap_list($this->mbox, $this->server, $folder_name); return is_array($list_subfolders); } /** * Removes parenthesis (comments) from the date string because * strtotime returns false if received date has them * * @param string $receiveddate a date as a string * * @access protected * @return integer */ protected function cleanupDate($receiveddate) { if (is_array($receiveddate)) { // Header Date could be repeated in the message, we only check the first $receiveddate = $receiveddate[0]; } $receivedtime = strtotime(preg_replace('/\(.*\)/', "", $receiveddate)); if ($receivedtime === false || $receivedtime == -1) { ZLog::Write(LOGLEVEL_WARN, sprintf("cleanupDate('%s'): strtotime() failed - message might be broken.", $receiveddate)); return null; } return $receivedtime; } /** * Returns a list of mime-types with extension files * * @access private * @return array[mime-type => extension] */ private function SystemExtensionMimeTypes() { $out = array(); if (file_exists(SYSTEM_MIME_TYPES_MAPPING)) { $file = fopen(SYSTEM_MIME_TYPES_MAPPING, 'r'); while(($line = fgets($file)) !== false) { $line = trim(preg_replace('/#.*/', '', $line)); if(!$line) continue; $parts = preg_split('/\s+/', $line); if(count($parts) == 1) continue; $type = array_shift($parts); foreach($parts as $part) { if (!isset($out[$type])) { $out[$type] = $part; } } } fclose($file); } return $out; } /** * Sends a message * * @access private * @param $fromaddr From address * @param $toaddr To address * @param $headers Headers array * @param $body Body array * @return boolean True if sent * @throws StatusException */ private function sendMessage($fromaddr, $toaddr, $headers, $body) { global $imap_smtp_params; $sendingMethod = 'mail'; if (defined('IMAP_SMTP_METHOD')) { $sendingMethod = IMAP_SMTP_METHOD; if ($sendingMethod == 'smtp') { if (isset($imap_smtp_params['username']) && $imap_smtp_params['username'] == 'imap_username') { $imap_smtp_params['username'] = $this->username; } if (isset($imap_smtp_params['password']) && $imap_smtp_params['password'] == 'imap_password') { $imap_smtp_params['password'] = $this->password; } } } if (is_array($toaddr)) { $recipients = $toaddr; } else { $recipients = array($toaddr); } // Cc and Bcc headers are sent, but we need to make sure that the recipient list contains them foreach (array("CC", "cc", "Cc", "BCC", "Bcc", "bcc") as $key) { if (!empty($headers[$key])) { if (is_array($headers[$key])) { $recipients = array_merge($recipients, $headers[$key]); } else { $recipients[] = $headers[$key]; } } } ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->sendMessage(): SendingMail with %s", $sendingMethod)); $mail = Mail::factory($sendingMethod, $sendingMethod == "mail" ? "-f " . $fromaddr : $imap_smtp_params); $send = $mail->send($recipients, $headers, $body); ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->sendMessage(): send return value %s", $send)); if ($send !== true) { throw new StatusException(sprintf("BackendIMAP->sendMessage(): The email could not be sent"), SYNC_COMMONSTATUS_MAILSUBMISSIONFAILED); } return $send; } /** * Saves a copy of a message in the Sent folder * * @access public * @param $finalHeaders Array of headers * @param $finalBody Body part * @return boolean If the message is saved */ private function saveSentMessage($finalHeaders, $finalBody) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->saveSentMessage(): saving message in Sent Items folder")); $headers = ""; foreach ($finalHeaders as $k => $v) { if (strlen($headers) > 0) { $headers .= "\n"; } $headers .= "$k: $v"; } if ($this->sentID === false) { $this->sentID = $this->getFolderIdFromImapId($this->create_name_folder(IMAP_FOLDER_SENT), false); } $saved = false; if ($this->sentID) { $imapid = $this->getImapIdFromFolderId($this->sentID); $saved = $this->addSentMessage($imapid, $headers, $finalBody); ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->saveSentMessage(): Outgoing mail saved in 'Sent' folder '%s' ['%s']", $imapid, $this->sentID)); } else { ZLog::Write(LOGLEVEL_ERROR, "BackendIMAP->saveSentMessage(): The email could not be saved to Sent Items folder. Check your configuration."); } unset($headers); return $saved; } /** * Set the from header value if not set or we are overwriting by configuration. * * @param array &$headers * @return void * @access private */ private function setFromHeaderValue(&$headers) { $from = getDefaultFromValue($this->username, $this->domain); if (isset($headers["from"])) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->getFromHeaderValue(): from defined: %s", $headers["from"])); if (strlen(IMAP_DEFAULTFROM) > 0) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->getFromHeaderValue(): Overwriting From: %s", $from)); $headers["from"] = $from; } } elseif (isset($headers["From"])) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->getFromHeaderValue(): From defined: %s", $headers["From"])); if (strlen(IMAP_DEFAULTFROM) > 0) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->getFromHeaderValue(): Overwriting From: %s", $from)); $headers["From"] = $from; } } else { // not From header found ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->getFromHeaderValue(): No From address defined, we try for a default one")); $headers["from"] = $from; } } /** * Set the Return-Path header value if not set * * @param array &$headers * @param string $fromaddr * @return void * @access private */ private function setReturnPathValue(&$headers, $fromaddr) { if (!(isset($headers["return-path"]) || isset($headers["Return-Path"]))) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->setReturnPathValue(): No Return-Path address defined, we use From")); $headers["return-path"] = $fromaddr; } } /** * The meta function for out of office settings. * * @param SyncObject $oof * * @access private * @return void */ private function settingsOOF(&$oof) { //if oof state is set it must be set of oof and get otherwise if (!isset($oof->oofstate)) { $oof->oofstate = SYNC_SETTINGSOOF_DISABLED; $oof->Status = SYNC_SETTINGSSTATUS_SUCCESS; //unset body type for oof in order not to stream it unset($oof->bodytype); return true; } else { return false; } } /** * Gets the user's email address from server * * @param SyncObject $userinformation * * @access private * @return void */ private function settingsUserInformation(&$userinformation) { $userinformation->Status = SYNC_SETTINGSSTATUS_USERINFO_SUCCESS; if (Request::GetProtocolVersion() >= 14.1) { $account = new SyncAccount(); $emailaddresses = new SyncEmailAddresses(); $emailaddresses->smtpaddress[] = $this->username; $emailaddresses->primarysmtpaddress = $this->username; $account->emailaddresses = $emailaddresses; $userinformation->accounts[] = $account; } else { $userinformation->emailaddresses[] = $this->username; } return true; } /** * Gets the folder list * * @access private * @return array */ private function get_folder_list() { $folders = array(); $list = @imap_getmailboxes($this->mbox, $this->server, "*"); if (is_array($list)) { $list = array_reverse($list); foreach ($list as $l) { $folders[] = $l->name; } } return $folders; } /** * Add one address to the list * * @access private * @param array $addresses * @param RFC822 address object $addr * @return string */ private function add_address_to_list(&$addresses, $addr) { $name = ""; if (isset($addr->mailbox) && isset($addr->host) && isset($addr->personal)) { $address = sprintf("%s@%s", $addr->mailbox, $addr->host); $name = $addr->personal; if(strlen($name) == 0 || $name == $address) { $fulladdr = $address; } else { if (preg_match('/^\".*\"$/', $name)) { $fulladdr = sprintf("%s <%s>", $name, $address); } else { $fulladdr = sprintf("\"%s\" <%s>", $name, $address); } } array_push($addresses, $fulladdr); } return $name; } /** * Close the IMAP connection. * * @access private */ private function close_connection() { if ($this->mbox) { // list all errors $errors = imap_errors(); if (is_array($errors)) { foreach ($errors as $e) { if (stripos($e, "fail") !== false) { $level = LOGLEVEL_WARN; } else { $level = LOGLEVEL_DEBUG; } ZLog::Write($level, "BackendIMAP->close_connection(): IMAP said: " . $e); } } @imap_close($this->mbox); ZLog::Write(LOGLEVEL_DEBUG, "BackendIMAP->close_connection(): disconnected from IMAP server"); $this->mbox = false; } } /** * Gets the folder list attributes * * @access private * @return array of ( ['name'], ['noInferiors'], ['noSelect'], ['marked'], ['referral'], ['children'] ) */ private function get_attributes_list() { $attributes = array(); $list = @imap_getmailboxes($this->mbox, $this->server, "*"); if (is_array($list)) { $list = array_reverse($list); foreach ($list as $l) { $attr = array( 'name' => $l->name, 'noInferiors' => (($l->attributes & LATT_NOINFERIORS) != false) , 'noSelect' => (($l->attributes & LATT_NOSELECT) != false) , 'referral' => (($l->attributes & LATT_REFERRAL) != false) ); if ($l->attributes & LATT_MARKED) { $attr['marked'] = true; } elseif ($l->attributes & LATT_UNMARKED) { $attr['marked'] = false; } if ($l->attributes & LATT_HASCHILDREN) { $attr['children'] = true; } elseif ($l->attributes & LATT_HASNOCHILDREN) { $attr['children'] = false; } $attributes[] = $attr; $attr = array(); } } return $attributes; } };