/tine20/Tinebase/Mail.php
PHP | 647 lines | 382 code | 81 blank | 184 comment | 80 complexity | c29d97b9507d26a51a01d0c2be9fb576 MD5 | raw file
- <?php
- /**
- * Tine 2.0
- *
- * @package Tinebase
- * @subpackage Mail
- * @license http://www.gnu.org/licenses/agpl.html AGPL Version 3
- * @copyright Copyright (c) 2008-2014 Metaways Infosystems GmbH (http://www.metaways.de)
- * @author Lars Kneschke <l.kneschke@metaways.de>
- */
- /**
- * This class extends the Zend_Mail class
- *
- * @package Tinebase
- * @subpackage Mail
- */
- class Tinebase_Mail extends Zend_Mail
- {
- /**
- * email address regexp
- */
- const EMAIL_ADDRESS_REGEXP = '/^([a-z0-9_\+-\.]+@[a-z0-9-\.]+\.[a-z]{2,63})$/i';
- /**
- * email address regexp (which might be contained in a longer text)
- */
- const EMAIL_ADDRESS_CONTAINED_REGEXP = '/([a-z0-9_\+-\.]+@[a-z0-9-\.]+\.[a-z]{2,63})/i';
- /**
- * Sender: address
- * @var string
- */
- protected $_sender = null;
-
- /**
- * fallback charset constant
- *
- * @var string
- */
- const DEFAULT_FALLBACK_CHARSET = 'iso-8859-15';
-
- /**
- * create Tinebase_Mail from Zend_Mail_Message
- *
- * @param Zend_Mail_Message $_zmm
- * @param string $_replyBody
- * @return Tinebase_Mail
- */
- public static function createFromZMM(Zend_Mail_Message $_zmm, $_replyBody = null)
- {
- $contentStream = fopen("php://temp", 'r+');
- fputs($contentStream, $_zmm->getContent());
- rewind($contentStream);
-
- $mp = new Zend_Mime_Part($contentStream);
- self::_getMetaDataFromZMM($_zmm, $mp);
-
- // append old body when no multipart/mixed
- if ($_replyBody !== null && $_zmm->headerExists('content-transfer-encoding')) {
- $mp = self::_appendReplyBody($mp, $_replyBody);
- } else {
- $mp->decodeContent();
- if ($_zmm->headerExists('content-transfer-encoding')) {
- switch ($_zmm->getHeader('content-transfer-encoding')) {
- case Zend_Mime::ENCODING_BASE64:
- // BASE64 encode has a bug that swallows the last char(s)
- $bodyEncoding = Zend_Mime::ENCODING_7BIT;
- break;
- default:
- $bodyEncoding = $_zmm->getHeader('content-transfer-encoding');
- }
- } else {
- $bodyEncoding = Zend_Mime::ENCODING_7BIT;
- }
- if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
- . ' Using encoding: ' . $bodyEncoding);
- $mp->encoding = $bodyEncoding;
- }
-
- $result = new Tinebase_Mail('utf-8');
- $result->setBodyText($mp);
- $result->setHeadersFromZMM($_zmm);
-
- return $result;
- }
-
- /**
- * get meta data (like contentype, charset, ...) from zmm and set it in zmp
- *
- * @param Zend_Mail_Message $zmm
- * @param Zend_Mime_Part $zmp
- */
- protected static function _getMetaDataFromZMM(Zend_Mail_Message $zmm, Zend_Mime_Part $zmp)
- {
- if ($zmm->headerExists('content-transfer-encoding')) {
- $zmp->encoding = $zmm->getHeader('content-transfer-encoding');
- } else {
- $zmp->encoding = Zend_Mime::ENCODING_7BIT;
- }
-
- if ($zmm->headerExists('content-type')) {
- $contentTypeHeader = Zend_Mime_Decode::splitHeaderField($zmm->getHeader('content-type'));
-
- $zmp->type = $contentTypeHeader[0];
-
- if (isset($contentTypeHeader['boundary'])) {
- $zmp->boundary = $contentTypeHeader['boundary'];
- }
-
- if (isset($contentTypeHeader['charset'])) {
- $zmp->charset = $contentTypeHeader['charset'];
- }
- } else {
- $zmp->type = Zend_Mime::TYPE_TEXT;
- }
-
- if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
- . ' Encoding: ' . $zmp->encoding . ' / type: ' . $zmp->type . ' / charset: ' . $zmp->charset);
- }
-
- /**
- * appends old body to mime part
- *
- * @param Zend_Mime_Part $mp
- * @param string $replyBody plain/text reply body
- * @return Zend_Mime_Part
- */
- protected static function _appendReplyBody(Zend_Mime_Part $mp, $replyBody)
- {
- $decodedContent = Tinebase_Mail::getDecodedContent($mp, NULL, FALSE);
- $type = $mp->type;
-
- if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) {
- Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . " mp content: " . $decodedContent);
- Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . " reply body: " . $replyBody);
- }
-
- if ($type === Zend_Mime::TYPE_HTML && /* checks if $replyBody does not contains tags */ $replyBody === strip_tags($replyBody)) {
- if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
- . " Converting plain/text reply body to HTML");
- $replyBody = self::convertFromTextToHTML($replyBody);
- }
-
- if ($type === Zend_Mime::TYPE_HTML && preg_match('/(<\/body>[\s\r\n]*<\/html>)/i', $decodedContent, $matches)) {
- if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
- . ' Appending reply body to html body.');
-
- $decodedContent = str_replace($matches[1], $replyBody . $matches[1], $decodedContent);
- } else {
- if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
- . " Appending reply body to mime text part.");
-
- $decodedContent .= $replyBody;
- }
-
- $mp = new Zend_Mime_Part($decodedContent);
- $mp->charset = 'utf-8';
- $mp->type = $type;
-
- return $mp;
- }
-
- /**
- * Sets the HTML body for the message
- *
- * @param string|Zend_Mime_Part $html
- * @param string $charset
- * @param string $encoding
- * @return Zend_Mail Provides fluent interface
- */
- public function setBodyHtml($html, $charset = null, $encoding = Zend_Mime::ENCODING_QUOTEDPRINTABLE)
- {
- if ($html instanceof Zend_Mime_Part) {
- $mp = $html;
- } else {
- if ($charset === null) {
- $charset = $this->_charset;
- }
-
- $mp = new Zend_Mime_Part($html);
- $mp->encoding = $encoding;
- $mp->type = Zend_Mime::TYPE_HTML;
- $mp->disposition = Zend_Mime::DISPOSITION_INLINE;
- $mp->charset = $charset;
- }
-
- $this->_bodyHtml = $mp;
-
- return $this;
- }
-
- /**
- * Sets the text body for the message.
- *
- * @param string|Zend_Mime_Part $txt
- * @param string $charset
- * @param string $encoding
- * @return Zend_Mail Provides fluent interface
- */
- public function setBodyText($txt, $charset = null, $encoding = Zend_Mime::ENCODING_QUOTEDPRINTABLE)
- {
- if ($txt instanceof Zend_Mime_Part) {
- $mp = $txt;
- } else {
- if ($charset === null) {
- $charset = $this->_charset;
- }
-
- $mp = new Zend_Mime_Part($txt);
- $mp->encoding = $encoding;
- $mp->type = Zend_Mime::TYPE_TEXT;
- $mp->disposition = Zend_Mime::DISPOSITION_INLINE;
- $mp->charset = $charset;
- }
-
- $this->_bodyText = $mp;
- return $this;
- }
- public function setBodyPGPMime($amored)
- {
- $this->_type = 'multipart/encrypted; protocol="application/pgp-encrypted"';
- // PGP/MIME Versions Identification
- $pgpIdent = new Zend_Mime_Part('Version: 1');
- $pgpIdent->encoding = '7bit';
- $pgpIdent->type = 'application/pgp-encrypted';
- $pgpIdent->description = 'PGP/MIME Versions Identification';
- $this->_bodyText = $pgpIdent;
- // OpenPGP encrypted message
- $pgpMessage = new Zend_Mime_Part($amored);
- $pgpMessage->encoding = '7bit';
- $pgpMessage->disposition = 'inline; filename=encrypted.asc';
- $pgpMessage->type = 'application/octet-stream; name=encrypted.asc';
- $pgpMessage->description = 'OpenPGP encrypted message';
- $this->_bodyHtml = $pgpMessage;
- }
- /**
- * set headers
- *
- * @param Zend_Mail_Message $_zmm
- * @return Zend_Mail Provides fluent interface
- */
- public function setHeadersFromZMM(Zend_Mail_Message $_zmm)
- {
- foreach ($_zmm->getHeaders() as $header => $values) {
- foreach ((array)$values as $value) {
- switch ($header) {
- case 'content-transfer-encoding':
- // these are implicitly set by Zend_Mail_Transport_Abstract::_getHeaders()
- case 'content-type':
- case 'mime-version':
- // do nothing
- break;
-
- case 'bcc':
- $addresses = self::parseAdresslist($value);
- foreach ($addresses as $address) {
- $this->addBcc($address['address'], $address['name']);
- }
- break;
-
- case 'cc':
- $addresses = self::parseAdresslist($value);
- foreach ($addresses as $address) {
- $this->addCc($address['address'], $address['name']);
- }
- break;
-
- case 'date':
- try {
- $this->setDate($value);
- } catch (Zend_Mail_Exception $zme) {
- if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE))
- Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . " Could not set date: " . $value);
- if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE))
- Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . " " . $zme);
- $this->setDate();
- }
- break;
-
- case 'from':
- $addresses = self::parseAdresslist($value);
- foreach ($addresses as $address) {
- $this->setFrom($address['address'], $address['name']);
- }
- break;
-
- case 'message-id':
- $this->setMessageId(trim($value,"<>"));
- break;
-
- case 'return-path':
- $this->setReturnPath($value);
- break;
-
- case 'subject':
- $this->setSubject($value);
- break;
-
- case 'to':
- $addresses = self::parseAdresslist($value);
- foreach ($addresses as $address) {
- $this->addTo($address['address'], $address['name']);
- }
- break;
- case 'reply-to':
- $this->setReplyTo($value);
- break;
- default:
- $this->addHeader($header, $value);
- break;
- }
- }
- }
-
- return $this;
- }
- /**
- * Sets Sender-header and sender of the message
- *
- * @param string $email
- * @param string $name
- * @return Zend_Mail Provides fluent interface
- * @throws Zend_Mail_Exception if called subsequent times
- */
- public function setSender($email, $name = '')
- {
- if ($this->_sender === null) {
- $email = strtr($email,"\r\n\t",'???');
- $this->_from = $email;
- $this->_storeHeader('Sender', $this->_encodeHeader('"'.$name.'"').' <'.$email.'>', true);
- } else {
- throw new Zend_Mail_Exception('Sender Header set twice');
- }
- return $this;
- }
-
- /**
- * Formats e-mail address
- *
- * NOTE: we always add quotes to the name as this caused problems when name is encoded
- * @see Zend_Mail::_formatAddress
- *
- * @param string $email
- * @param string $name
- * @return string
- */
- protected function _formatAddress($email, $name)
- {
- if ($name === '' || $name === null || $name === $email) {
- return $email;
- } else {
- $encodedName = $this->_encodeHeader($name);
- $format = '"%s" <%s>';
- return sprintf($format, $encodedName, $email);
- }
- }
- /**
- * check if Zend_Mail_Message is/contains calendar iMIP message
- *
- * @param Zend_Mail_Message $zmm
- * @return boolean
- */
- public static function isiMIPMail(Zend_Mail_Message $zmm)
- {
- foreach ($zmm as $part) {
- if (preg_match('/text\/calendar/', $part->contentType)) {
- return TRUE;
- }
- }
-
- return FALSE;
- }
-
- /**
- * get decoded body content
- *
- * @param Zend_Mime_Part $zmp
- * @param array $partStructure
- * @param boolean $appendCharsetFilter
- * @return string
- */
- public static function getDecodedContent(Zend_Mime_Part $zmp, $_partStructure = NULL, $appendCharsetFilter = TRUE)
- {
- $charset = self::_getCharset($zmp, $_partStructure);
- if ($appendCharsetFilter) {
- $charset = self::_appendCharsetFilter($zmp, $charset);
- }
- $encoding = (is_array($_partStructure) && ! empty($_partStructure['encoding']))
- ? $_partStructure['encoding']
- : $zmp->encoding;
-
- if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
- . " Trying to decode mime part content. Encoding/charset: " . $encoding . ' / ' . $charset);
-
- // need to set error handler because stream_get_contents just throws a E_WARNING
- set_error_handler('Tinebase_Mail::decodingErrorHandler', E_WARNING);
- try {
- $body = $zmp->getDecodedContent();
- restore_error_handler();
-
- } catch (Tinebase_Exception $e) {
- if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
- . " Decoding of " . $zmp->encoding . '/' . $encoding . ' encoded message failed: ' . $e->getMessage());
-
- // trying to fix decoding problems
- restore_error_handler();
- $zmp->resetStream();
- if (preg_match('/convert\.quoted-printable-decode/', $e->getMessage())) {
- if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . ' Trying workaround for http://bugs.php.net/50363.');
- $body = quoted_printable_decode(stream_get_contents($zmp->getRawStream()));
- $body = iconv($charset, 'utf-8', $body);
- } else {
- if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . ' Try again with fallback encoding.');
- $zmp->appendDecodeFilter(self::_getDecodeFilter());
- set_error_handler('Tinebase_Mail::decodingErrorHandler', E_WARNING);
- try {
- $body = $zmp->getDecodedContent();
- restore_error_handler();
- } catch (Tinebase_Exception $e) {
- restore_error_handler();
- if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . ' Fallback encoding failed. Trying base64_decode().');
- $zmp->resetStream();
- $body = base64_decode(stream_get_contents($zmp->getRawStream()));
- $body = iconv($charset, 'utf-8', $body);
- }
- }
- }
-
- return $body;
- }
- /**
- * convert charset (and return charset)
- *
- * @param Zend_Mime_Part $_part
- * @param array $_structure
- * @return string
- */
- protected static function _getCharset(Zend_Mime_Part $_part, $_structure = NULL)
- {
- return ($_structure && isset($_structure['parameters']['charset']))
- ? $_structure['parameters']['charset']
- : ($_part->charset ? $_part->charset : self::DEFAULT_FALLBACK_CHARSET);
- }
-
- /**
- * convert charset (and return charset)
- *
- * @param Zend_Mime_Part $_part
- * @param string $charset
- * @return string
- */
- protected static function _appendCharsetFilter(Zend_Mime_Part $_part, $charset)
- {
- if ($charset == 'utf8') {
- $charset = 'utf-8';
- } else if ($charset == 'us-ascii') {
- // us-ascii caused problems with iconv encoding to utf-8
- $charset = self::DEFAULT_FALLBACK_CHARSET;
- } else if (strpos($charset, '.') !== false) {
- // the stream filter does not like charsets with a dot in its name
- // stream_filter_append(): unable to create or locate filter "convert.iconv.ansi_x3.4-1968/utf-8//IGNORE"
- $charset = self::DEFAULT_FALLBACK_CHARSET;
- } else if (iconv($charset, 'utf-8', '') === false) {
- // check if charset is supported by iconv
- $charset = self::DEFAULT_FALLBACK_CHARSET;
- }
-
- $_part->appendDecodeFilter(self::_getDecodeFilter($charset));
-
- return $charset;
- }
-
- /**
- * get decode filter for stream_filter_append
- *
- * @param string $_charset
- * @return string
- */
- protected static function _getDecodeFilter($_charset = self::DEFAULT_FALLBACK_CHARSET)
- {
- if (in_array(strtolower($_charset), array('iso-8859-1', 'windows-1252', 'iso-8859-15')) && extension_loaded('mbstring')) {
- require_once 'StreamFilter/ConvertMbstring.php';
- $filter = 'convert.mbstring';
- } else {
- $filter = "convert.iconv.$_charset/utf-8//IGNORE";
- }
-
- if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Appending decode filter: ' . $filter);
-
- return $filter;
- }
-
- /**
- * error exception handler for iconv decoding errors / only gets E_WARNINGs
- *
- * NOTE: PHP < 5.3 don't throws exceptions for Catchable fatal errors per default,
- * so we convert them into exceptions manually
- *
- * @param integer $severity
- * @param string $errstr
- * @param string $errfile
- * @param integer $errline
- * @throws Tinebase_Exception
- *
- * @todo maybe we can remove that because php 5.3+ is required now
- */
- public static function decodingErrorHandler($severity, $errstr, $errfile, $errline)
- {
- Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . " $errstr in {$errfile}::{$errline} ($severity)");
-
- throw new Tinebase_Exception($errstr);
- }
-
- /**
- * parse address list
- *
- * @param string $_adressList
- * @return array
- */
- public static function parseAdresslist($_addressList)
- {
- if (strpos($_addressList, ',') !== FALSE && substr_count($_addressList, '@') == 1) {
- // we have a comma in the name -> do not split string!
- $addresses = array($_addressList);
- } else {
- // create stream to be used with fgetcsv
- $stream = fopen("php://temp", 'r+');
- fputs($stream, $_addressList);
- rewind($stream);
-
- // alternative solution to create stream; yet untested
- #$stream = fopen('data://text/plain;base64,' . base64_encode($_addressList), 'r');
-
- // split addresses
- $addresses = fgetcsv($stream);
- }
-
- if (! is_array($addresses)) {
- if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ .
- ' Could not parse addresses: ' . var_export($addresses, TRUE));
- return array();
- }
-
- foreach ($addresses as $key => $address) {
- if (preg_match('/(.*)<(.+@[^@]+)>/', $address, $matches)) {
- $name = trim(trim($matches[1]), '"');
- $address = trim($matches[2]);
- $addresses[$key] = array('name' => substr($name, 0, 250), 'address' => $address);
- } else {
- $address = preg_replace('/[,;]*/i', '', $address);
- $addresses[$key] = array('name' => null, 'address' => trim($address));
- }
- }
- return $addresses;
- }
- /**
- * convert text to html
- * - replace quotes ('> ') with blockquotes
- * - does htmlspecialchars()
- * - converts linebreaks to <br />
- *
- * @param string $text
- * @param string $blockquoteClass
- * @return string
- */
- public static function convertFromTextToHTML($text, $blockquoteClass = null)
- {
- if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' Input: ' . $text);
-
- $lines = preg_split('/\r\n|\n|\r/', $text);
- $result = array();
- $indention = 0;
- foreach ($lines as $line) {
- // get indention level and remove quotes
- if (preg_match('/^>[> ]*/', $line, $matches)) {
- $indentionLevel = substr_count($matches[0], '>');
- $line = str_replace($matches[0], '', $line);
- } else {
- $indentionLevel = 0;
- }
-
- // convert html special chars
- $line = htmlspecialchars($line, ENT_COMPAT, 'UTF-8');
-
- // set blockquote tags for current indentionLevel
- while ($indention < $indentionLevel) {
- $class = $blockquoteClass ? 'class="' . $blockquoteClass . '"' : '';
- $line = '<blockquote ' . $class . '>' . $line;
- $indention++;
- }
- while ($indention > $indentionLevel) {
- $line = '</blockquote>' . $line;
- $indention--;
- }
-
- $result[] = $line;
-
- if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' Line: ' . $line);
- }
-
- $result = implode('<br />', $result);
-
- if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' Result: ' . $result);
-
- return $result;
- }
- /**
- * get imap/smtp connection options
- *
- * do we verify imap/smtp peers?
- *
- * @param integer $timeout connection timeout
- * @return array
- *
- * TODO use separate configs for imap/smtp/sieve...
- */
- public static function getConnectionOptions($timeout = 30)
- {
- $connectionOptions = array(
- 'timeout' => $timeout,
- );
- $tinebaseImapConfig = Tinebase_Config::getInstance()->get(Tinebase_Config::IMAP);
- if (isset($tinebaseImapConfig->verifyPeer) && $tinebaseImapConfig->verifyPeer == false) {
- $connectionOptions['context'] = array(
- 'ssl' => array(
- 'verify_peer' => false,
- 'verify_peer_name' => false
- ),
- );
- }
- return $connectionOptions;
- }
- }