. * * 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 = "