6 * @subpackage Controller
7 * @license http://www.gnu.org/licenses/agpl.html AGPL Version 3
8 * @author Philipp Schüle <p.schuele@metaways.de>
9 * @copyright Copyright (c) 2009-2013 Metaways Infosystems GmbH (http://www.metaways.de)
11 * @todo parse mail body and add <a> to telephone numbers?
15 * message controller for Felamimail
18 * @subpackage Controller
20 class Felamimail_Controller_Message extends Tinebase_Controller_Record_Abstract
23 * application name (is needed in checkRight())
27 protected $_applicationName = 'Felamimail';
30 * holds the instance of the singleton
32 * @var Felamimail_Controller_Message
34 private static $_instance = NULL;
39 * @var Felamimail_Controller_Cache_Message
41 protected $_cacheController = NULL;
46 * @var Felamimail_Backend_Cache_Sql_Message
48 protected $_backend = NULL;
55 protected $_punycodeConverter = NULL;
58 * foreign application content types
62 protected $_supportedForeignContentTypes = array(
63 'Calendar' => Felamimail_Model_Message::CONTENT_TYPE_CALENDAR,
64 'Addressbook' => Felamimail_Model_Message::CONTENT_TYPE_VCARD,
70 * don't use the constructor. use the singleton
72 private function __construct()
74 $this->_modelName = 'Felamimail_Model_Message';
75 $this->_doContainerACLChecks = FALSE;
76 $this->_backend = new Felamimail_Backend_Cache_Sql_Message();
78 $this->_cacheController = Felamimail_Controller_Cache_Message::getInstance();
82 * don't clone. Use the singleton.
85 private function __clone()
90 * the singleton pattern
92 * @return Felamimail_Controller_Message
94 public static function getInstance()
96 if (self::$_instance === NULL) {
97 self::$_instance = new Felamimail_Controller_Message();
100 return self::$_instance;
104 * Removes accounts where current user has no access to
106 * @param Tinebase_Model_Filter_FilterGroup $_filter
107 * @param string $_action get|update
109 * @todo move logic to Felamimail_Model_MessageFilter
111 public function checkFilterACL(Tinebase_Model_Filter_FilterGroup $_filter, $_action = 'get')
113 $accountFilter = $_filter->getFilter('account_id');
115 // force a $accountFilter filter (ACL) / all accounts of user
116 if ($accountFilter === NULL || $accountFilter['operator'] !== 'equals' || ! empty($accountFilter['value'])) {
117 $_filter->createFilter('account_id', 'equals', array());
122 * append a new message to given folder
124 * @param string|Felamimail_Model_Folder $_folder id of target folder
125 * @param string|resource $_message full message content
126 * @param array $_flags flags for new message
128 public function appendMessage($_folder, $_message, $_flags = null)
130 $folder = ($_folder instanceof Felamimail_Model_Folder) ? $_folder : Felamimail_Controller_Folder::getInstance()->get($_folder);
131 $message = (is_resource($_message)) ? stream_get_contents($_message) : $_message;
132 $flags = ($_flags !== null) ? (array) $_flags : null;
134 $imapBackend = $this->_getBackendAndSelectFolder(NULL, $folder);
135 $imapBackend->appendMessage($message, $folder->globalname, $flags);
139 * get complete message by id
141 * @param string|Felamimail_Model_Message $_id
142 * @param string $_partId
143 * @param boolean $_setSeen
144 * @return Felamimail_Model_Message
146 public function getCompleteMessage($_id, $_partId = NULL, $_setSeen = FALSE)
148 if ($_id instanceof Felamimail_Model_Message) {
151 $message = $this->get($_id);
154 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ .
155 ' Getting message content ' . $message->messageuid
158 $folder = Felamimail_Controller_Folder::getInstance()->get($message->folder_id);
159 $account = Felamimail_Controller_Account::getInstance()->get($folder->account_id);
161 $this->_checkMessageAccount($message, $account);
163 $message = $this->_getCompleteMessageContent($message, $account, $_partId);
166 Felamimail_Controller_Message_Flags::getInstance()->setSeenFlag($message);
169 $this->prepareAndProcessParts($message);
171 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . print_r($message->toArray(), true));
177 * check if account of message is belonging to user
179 * @param Felamimail_Model_Message $message
180 * @param Felamimail_Model_Account $account
181 * @throws Tinebase_Exception_AccessDenied
183 * @todo think about moving this to get() / _checkGrant()
185 protected function _checkMessageAccount($message, $account = NULL)
187 $account = ($account) ? $account : Felamimail_Controller_Account::getInstance()->get($message->account_id);
188 if ($account->user_id !== Tinebase_Core::getUser()->getId()) {
189 throw new Tinebase_Exception_AccessDenied('You are not allowed to access this message');
194 * get message content (body, headers and attachments)
196 * @param Felamimail_Model_Message $_message
197 * @param Felamimail_Model_Account $_account
198 * @param string $_partId
200 protected function _getCompleteMessageContent(Felamimail_Model_Message $_message, Felamimail_Model_Account $_account, $_partId = NULL)
202 $mimeType = ($_account->display_format == Felamimail_Model_Account::DISPLAY_HTML || $_account->display_format == Felamimail_Model_Account::DISPLAY_CONTENT_TYPE)
203 ? Zend_Mime::TYPE_HTML
204 : Zend_Mime::TYPE_TEXT;
206 $headers = $this->getMessageHeaders($_message, $_partId, true);
207 $body = $this->getMessageBody($_message, $_partId, $mimeType, $_account, true);
208 $attachments = $this->getAttachments($_message, $_partId);
210 if ($_partId === null) {
211 $message = $_message;
213 $message->body = $body;
214 $message->headers = $headers;
215 $message->attachments = $attachments;
216 // make sure the structure is present
217 $message->structure = $message->structure;
220 // create new object for rfc822 message
221 $structure = $_message->getPartStructure($_partId, FALSE);
223 $message = new Felamimail_Model_Message(array(
224 'messageuid' => $_message->messageuid,
225 'folder_id' => $_message->folder_id,
226 'received' => $_message->received,
227 'size' => (array_key_exists('size', $structure)) ? $structure['size'] : 0,
228 'partid' => $_partId,
230 'headers' => $headers,
231 'attachments' => $attachments
234 $message->parseHeaders($headers);
236 $structure = array_key_exists('messageStructure', $structure) ? $structure['messageStructure'] : $structure;
237 $message->parseStructure($structure);
244 * send reading confirmation for message
246 * @param string $messageId
248 public function sendReadingConfirmation($messageId)
250 $message = $this->get($messageId);
251 $this->_checkMessageAccount($message);
252 $message->sendReadingConfirmation();
256 * prepare message parts that could be interesting for other apps
258 * @param Felamimail_Model_Message $_message
260 public function prepareAndProcessParts(Felamimail_Model_Message $_message)
262 $preparedParts = new Tinebase_Record_RecordSet('Felamimail_Model_PreparedMessagePart');
264 foreach ($this->_supportedForeignContentTypes as $application => $contentType) {
265 if (! Tinebase_Application::getInstance()->isInstalled($application) || ! Tinebase_Core::getUser()->hasRight($application, Tinebase_Acl_Rights::RUN)) {
266 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
267 . ' ' . $application . ' not installed or access denied.');
271 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
272 . ' Looking for ' . $application . '[' . $contentType . '] content ...');
274 $parts = $_message->getBodyParts(NULL, $contentType);
275 foreach ($parts as $partId => $partData) {
276 if ($partData['contentType'] !== $contentType) {
280 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
281 . ' ' . $application . '[' . $contentType . '] content found.');
283 $preparedPart = $this->_getForeignMessagePart($_message, $partId, $partData);
285 $this->_processForeignMessagePart($application, $preparedPart);
286 $preparedParts->addRecord(new Felamimail_Model_PreparedMessagePart(array(
287 'id' => $_message->getId() . '_' . $partId,
288 'contentType' => $contentType,
289 'preparedData' => $preparedPart,
295 $_message->preparedParts = $preparedParts;
299 * get foreign message parts
301 * - calendar invitations
302 * - addressbook vcards
305 * @param Felamimail_Model_Message $_message
306 * @param string $_partId
307 * @param array $_partData
308 * @return NULL|Tinebase_Record_Abstract
310 protected function _getForeignMessagePart(Felamimail_Model_Message $_message, $_partId, $_partData)
312 $part = $this->getMessagePart($_message, $_partId);
314 $userAgent = (isset($_message->headers['user-agent'])) ? $_message->headers['user-agent'] : NULL;
315 $parameters = (isset($_partData['parameters'])) ? $_partData['parameters'] : array();
316 $decodedContent = $part->getDecodedContent();
318 switch ($part->type) {
319 case Felamimail_Model_Message::CONTENT_TYPE_CALENDAR:
320 if (! version_compare(PHP_VERSION, '5.3.0', '>=')) {
321 if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . ' PHP 5.3+ is needed for vcalendar support.');
325 $partData = new Calendar_Model_iMIP(array(
326 'id' => $_message->getId() . '_' . $_partId,
327 'ics' => $decodedContent,
328 'method' => (isset($parameters['method'])) ? $parameters['method'] : NULL,
329 'originator' => $_message->from_email,
330 'userAgent' => $userAgent,
334 if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . ' Could not create iMIP of content type ' . $part->type);
342 * process foreign iMIP part
344 * @param string $_application
345 * @param Tinebase_Record_Abstract $_iMIP
348 * @todo use iMIP factory?
350 protected function _processForeignMessagePart($_application, $_iMIP)
352 $iMIPFrontendClass = $_application . '_Frontend_iMIP';
353 if (! class_exists($iMIPFrontendClass)) {
354 if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . ' iMIP class not found in application ' . $_application);
358 $iMIPFrontend = new $iMIPFrontendClass();
360 $result = $iMIPFrontend->autoProcess($_iMIP);
361 } catch (Exception $e) {
362 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Processing failed: ' . $e->getMessage());
363 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' ' . $e->getTraceAsString());
371 * get iMIP by message and part id
373 * @param string $_iMIPId
374 * @throws Tinebase_Exception_InvalidArgument
375 * @return Tinebase_Record_Abstract
377 public function getiMIP($_iMIPId)
379 if (strpos($_iMIPId, '_') === FALSE) {
380 throw new Tinebase_Exception_InvalidArgument('messageId_partId expecetd.');
383 list($messageId, $partId) = explode('_', $_iMIPId);
385 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
386 . ' Fetching ' . $messageId . '[' . $partId . '] part with iMIP data ...');
388 $message = $this->get($messageId);
390 $iMIPPartStructure = $message->getPartStructure($partId);
391 $iMIP = $this->_getForeignMessagePart($message, $partId, $iMIPPartStructure);
399 * @param string|Felamimail_Model_Message $_id
400 * @param string $_partId (the part id, can look like this: 1.3.2 -> returns the second part of third part of first part...)
401 * @param boolean $_onlyBodyOfRfc822 only fetch body of rfc822 messages (FALSE to get headers, too)
402 * @param array $_partStructure (is fetched if NULL/omitted)
403 * @return Zend_Mime_Part
405 public function getMessagePart($_id, $_partId = NULL, $_onlyBodyOfRfc822 = FALSE, $_partStructure = NULL)
407 if ($_id instanceof Felamimail_Model_Message) {
410 $message = $this->get($_id);
413 // need to refetch part structure of RFC822 messages because message structure is used instead
414 $partContentType = ($_partId && isset($message->structure['parts'][$_partId])) ? $message->structure['parts'][$_partId]['contentType'] : NULL;
415 $partStructure = ($_partStructure !== NULL && $partContentType !== Felamimail_Model_Message::CONTENT_TYPE_MESSAGE_RFC822) ? $_partStructure : $message->getPartStructure($_partId, FALSE);
417 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
418 . ' ' . print_r($partStructure, TRUE));
420 $rawContent = $this->_getPartContent($message, $_partId, $partStructure, $_onlyBodyOfRfc822);
422 $part = $this->_createMimePart($rawContent, $partStructure);
428 * get part content (and update structure) from message part
430 * @param Felamimail_Model_Message $_message
431 * @param string $_partId
432 * @param array $_partStructure
433 * @param boolean $_onlyBodyOfRfc822 only fetch body of rfc822 messages (FALSE to get headers, too)
436 protected function _getPartContent(Felamimail_Model_Message $_message, $_partId, &$_partStructure, $_onlyBodyOfRfc822 = FALSE)
438 $imapBackend = $this->_getBackendAndSelectFolder($_message->folder_id);
442 // special handling for rfc822 messages
443 if ($_partId !== NULL && $_partStructure['contentType'] === Felamimail_Model_Message::CONTENT_TYPE_MESSAGE_RFC822) {
444 if ($_onlyBodyOfRfc822) {
445 $logmessage = 'Fetch message part (TEXT) ' . $_partId . ' of messageuid ' . $_message->messageuid;
446 if (array_key_exists('messageStructure', $_partStructure)) {
447 $_partStructure = $_partStructure['messageStructure'];
450 $logmessage = 'Fetch message part (HEADER + TEXT) ' . $_partId . ' of messageuid ' . $_message->messageuid;
451 $rawContent .= $imapBackend->getRawContent($_message->messageuid, $_partId . '.HEADER', true);
454 $section = $_partId . '.TEXT';
456 $logmessage = ($_partId !== NULL)
457 ? 'Fetch message part ' . $_partId . ' of messageuid ' . $_message->messageuid
458 : 'Fetch main of messageuid ' . $_message->messageuid;
463 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . print_r($_partStructure, TRUE));
465 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' ' . $logmessage);
467 $rawContent .= $imapBackend->getRawContent($_message->messageuid, $section, TRUE);
473 * create mime part from raw content and part structure
475 * @param string $_rawContent
476 * @param array $_partStructure
477 * @return Zend_Mime_Part
479 protected function _createMimePart($_rawContent, $_partStructure)
481 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' Content: ' . $_rawContent);
483 $stream = fopen("php://temp", 'r+');
484 fputs($stream, $_rawContent);
489 $part = new Zend_Mime_Part($stream);
490 $part->type = $_partStructure['contentType'];
491 $part->encoding = array_key_exists('encoding', $_partStructure) ? $_partStructure['encoding'] : null;
492 $part->id = array_key_exists('id', $_partStructure) ? $_partStructure['id'] : null;
493 $part->description = array_key_exists('description', $_partStructure) ? $_partStructure['description'] : null;
494 $part->charset = array_key_exists('charset', $_partStructure['parameters'])
495 ? $_partStructure['parameters']['charset']
496 : Tinebase_Mail::DEFAULT_FALLBACK_CHARSET;
497 $part->boundary = array_key_exists('boundary', $_partStructure['parameters']) ? $_partStructure['parameters']['boundary'] : null;
498 $part->location = $_partStructure['location'];
499 $part->language = $_partStructure['language'];
500 if (is_array($_partStructure['disposition'])) {
501 $part->disposition = $_partStructure['disposition']['type'];
502 if (array_key_exists('parameters', $_partStructure['disposition'])) {
503 $part->filename = array_key_exists('filename', $_partStructure['disposition']['parameters']) ? $_partStructure['disposition']['parameters']['filename'] : null;
506 if (empty($part->filename) && array_key_exists('parameters', $_partStructure) && array_key_exists('name', $_partStructure['parameters'])) {
507 $part->filename = $_partStructure['parameters']['name'];
516 * @param string|Felamimail_Model_Message $_messageId
517 * @param string $_partId
518 * @param string $_contentType
519 * @param Felamimail_Model_Account $_account
522 public function getMessageBody($_messageId, $_partId, $_contentType, $_account = NULL)
524 $message = ($_messageId instanceof Felamimail_Model_Message) ? $_messageId : $this->get($_messageId);
526 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
527 . ' Get Message body (part: ' . $_partId . ') of message id ' . $message->getId() . ' (content type ' . $_contentType . ')');
529 $cacheBody = Felamimail_Config::getInstance()->get(Felamimail_Config::CACHE_EMAIL_BODY, TRUE);
531 $cache = Tinebase_Core::getCache();
532 $cacheId = $this->_getMessageBodyCacheId($message, $_partId, $_contentType, $_account);
534 if ($cache->test($cacheId)) {
535 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Getting Message from cache.');
536 return $cache->load($cacheId);
540 $messageBody = $this->_getAndDecodeMessageBody($message, $_partId, $_contentType, $_account);
542 // activate garbage collection (@see 0008216: HTMLPurifier/TokenFactory.php : Allowed memory size exhausted)
543 $cycles = gc_collect_cycles();
544 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
545 . ' Current mem usage after gc_collect_cycles(' . $cycles . ' ): ' . memory_get_usage()/1024/1024);
548 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Put message body into Tinebase cache (for 24 hours).');
549 $cache->save($messageBody, $cacheId, array('getMessageBody'), 86400);
556 * get message body cache id
558 * @param string|Felamimail_Model_Message $_messageId
559 * @param string $_partId
560 * @param string $_contentType
561 * @param Felamimail_Model_Account $_account
564 protected function _getMessageBodyCacheId($_message, $_partId, $_contentType, $_account)
566 $cacheId = 'getMessageBody_'
568 . str_replace('.', '', $_partId)
569 . substr($_contentType, -4)
570 . (($_account !== NULL) ? 'acc' : '');
576 * get and decode message body
578 * @param Felamimail_Model_Message $_message
579 * @param string $_partId
580 * @param string $_contentType
581 * @param Felamimail_Model_Account $_account
584 * @todo multipart_related messages should deliver inline images
586 protected function _getAndDecodeMessageBody(Felamimail_Model_Message $_message, $_partId, $_contentType, $_account = NULL)
588 $structure = $_message->getPartStructure($_partId);
589 if (empty($structure)) {
590 if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
591 . ' Empty structure, could not find body parts of message ' . $_message->subject);
595 $bodyParts = $_message->getBodyParts($structure, $_contentType);
596 if (empty($bodyParts)) {
597 if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
598 . ' Could not find body parts of message ' . $_message->subject);
604 foreach ($bodyParts as $partId => $partStructure) {
605 $bodyPart = $this->getMessagePart($_message, $partId, TRUE, $partStructure);
607 $body = Tinebase_Mail::getDecodedContent($bodyPart, $partStructure);
609 if ($partStructure['contentType'] != Zend_Mime::TYPE_TEXT) {
610 $bodyCharCountBefore = strlen($body);
611 $body = $this->_purifyBodyContent($body, $_message->getId());
612 $bodyCharCountAfter = strlen($body);
614 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
615 . ' Purifying removed ' . ($bodyCharCountBefore - $bodyCharCountAfter) . ' / ' . $bodyCharCountBefore . ' characters.');
616 if ($_message->text_partid && $bodyCharCountAfter < $bodyCharCountBefore / 10) {
617 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
618 . ' Purify may have removed (more than 9/10) too many chars, using alternative text message part.');
619 return $this->_getAndDecodeMessageBody($_message, $_message->text_partid , Zend_Mime::TYPE_TEXT, $_account);
623 if (! ($_account !== NULL && $_account->display_format === Felamimail_Model_Account::DISPLAY_CONTENT_TYPE && $bodyPart->type == Zend_Mime::TYPE_TEXT)) {
624 $body = Felamimail_Message::convertContentType($partStructure['contentType'], $_contentType, $body);
625 if ($bodyPart->type == Zend_Mime::TYPE_TEXT && $_contentType == Zend_Mime::TYPE_HTML) {
626 $body = Felamimail_Message::replaceUris($body);
627 $body = Felamimail_Message::replaceEmails($body);
630 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
631 . ' Do not convert ' . $bodyPart->type . ' part to ' . $_contentType);
634 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
635 . ' Adding part ' . $partId . ' to message body.');
637 $messageBody .= $body;
644 * use html purifier to remove 'bad' tags/attributes from html body
646 * @param string $_content
647 * @param string $messageId
650 protected function _purifyBodyContent($_content, $messageId)
652 if (!defined('HTMLPURIFIER_PREFIX')) {
653 define('HTMLPURIFIER_PREFIX', realpath(dirname(__FILE__) . '/../../library/HTMLPurifier'));
656 $config = Tinebase_Core::getConfig();
657 $path = ($config->caching && $config->caching->active && $config->caching->path)
658 ? $config->caching->path : Tinebase_Core::getTempDir();
660 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
661 . ' Purifying html body. (cache path: ' . $path .')');
662 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
663 . ' Current mem usage before purify: ' . memory_get_usage()/1024/1024);
665 // add custom schema for passing message id to URIScheme
666 $configSchema = HTMLPurifier_ConfigSchema::makeFromSerial();
667 $configSchema->add('Felamimail.messageId', NULL, 'string', TRUE);
668 $config = HTMLPurifier_Config::create(NULL, $configSchema);
669 $config->set('HTML.DefinitionID', 'purify message body contents');
670 $config->set('HTML.DefinitionRev', 1);
672 // some config values to consider
674 $config->set('Attr.EnableID', true);
675 $config->set('Attr.ClassUseCDATA', true);
676 $config->set('CSS.AllowTricky', true);
678 $config->set('Cache.SerializerPath', $path);
679 $config->set('URI.AllowedSchemes', array(
686 $config->set('Felamimail.messageId', $messageId);
688 $this->_transformBodyTags($config);
691 $uri = $config->getDefinition('URI');
692 $uri->addFilter(new Felamimail_HTMLPurifier_URIFilter_TransformURI(), $config);
694 // add cid uri scheme
695 require_once(dirname(dirname(__FILE__)) . '/HTMLPurifier/URIScheme/cid.php');
697 $purifier = new HTMLPurifier($config);
698 $content = $purifier->purify($_content);
700 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
701 . ' Current mem usage after purify: ' . memory_get_usage()/1024/1024);
707 * transform some tags / attributes
709 * @param HTMLPurifier_Config $config
711 protected function _transformBodyTags(HTMLPurifier_Config $config)
713 if ($def = $config->maybeGetRawHTMLDefinition()) {
714 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
715 . ' Add target="_blank" to anchors');
716 $a = $def->addBlankElement('a');
717 $a->attr_transform_post[] = new Felamimail_HTMLPurifier_AttrTransform_AValidator();
719 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
720 . ' Add class="felamimail-body-blockquote" to blockquote tags that do not already have the class');
721 $bq = $def->addBlankElement('blockquote');
722 $bq->attr_transform_post[] = new Felamimail_HTMLPurifier_AttrTransform_BlockquoteValidator();
724 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
725 . ' Could not get HTMLDefinition, no transformation possible');
730 * get message headers
732 * @param string|Felamimail_Model_Message $_messageId
733 * @param boolean $_readOnly
735 * @throws Felamimail_Exception_IMAPMessageNotFound
737 public function getMessageHeaders($_messageId, $_partId = null, $_readOnly = false)
739 if (! $_messageId instanceof Felamimail_Model_Message) {
740 $message = $this->_backend->get($_messageId);
742 $message = $_messageId;
745 $cache = Tinebase_Core::get('cache');
746 $cacheId = 'getMessageHeaders' . $message->getId() . str_replace('.', '', $_partId);
747 if ($cache->test($cacheId)) {
748 return $cache->load($cacheId);
751 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
752 . ' Fetching headers for message uid ' . $message->messageuid . ' (part:' . $_partId . ')');
755 $imapBackend = $this->_getBackendAndSelectFolder($message->folder_id);
756 } catch (Zend_Mail_Storage_Exception $zmse) {
757 if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' ' . $zmse->getMessage());
758 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' ' . $zmse->getTraceAsString());
759 throw new Felamimail_Exception_IMAPMessageNotFound('Folder not found');
762 if ($imapBackend === null) {
763 throw new Felamimail_Exception('Failed to get imap backend');
766 $section = ($_partId === null) ? 'HEADER' : $_partId . '.HEADER';
769 $rawHeaders = $imapBackend->getRawContent($message->messageuid, $section, $_readOnly);
770 } catch (Felamimail_Exception_IMAPMessageNotFound $feimnf) {
771 $this->_backend->delete($message->getId());
775 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
776 . ' Fetched Headers: ' . $rawHeaders);
778 Zend_Mime_Decode::splitMessage($rawHeaders, $headers, $null);
780 $cache->save($headers, $cacheId, array('getMessageHeaders'), 86400);
786 * get imap backend and folder (and select folder)
788 * @param string $_folderId
789 * @param Felamimail_Backend_Folder &$_folder
790 * @param boolean $_select
791 * @param Felamimail_Backend_ImapProxy $_imapBackend
792 * @throws Felamimail_Exception_IMAPServiceUnavailable
793 * @throws Felamimail_Exception_IMAPFolderNotFound
794 * @return Felamimail_Backend_ImapProxy
796 protected function _getBackendAndSelectFolder($_folderId = NULL, &$_folder = NULL, $_select = TRUE, Felamimail_Backend_ImapProxy $_imapBackend = NULL)
798 if ($_folder === NULL || empty($_folder)) {
799 $folderBackend = new Felamimail_Backend_Folder();
800 $_folder = $folderBackend->get($_folderId);
804 $imapBackend = ($_imapBackend === NULL) ? Felamimail_Backend_ImapFactory::factory($_folder->account_id) : $_imapBackend;
806 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
807 . ' Select folder ' . $_folder->globalname);
808 $backendFolderValues = $imapBackend->selectFolder(Felamimail_Model_Folder::encodeFolderName($_folder->globalname));
810 } catch (Zend_Mail_Storage_Exception $zmse) {
811 // @todo remove the folder from cache if it could not be found on the IMAP server?
812 throw new Felamimail_Exception_IMAPFolderNotFound($zmse->getMessage());
813 } catch (Zend_Mail_Protocol_Exception $zmpe) {
814 throw new Felamimail_Exception_IMAPServiceUnavailable($zmpe->getMessage());
821 * get attachments of message
823 * @param array $_structure
826 public function getAttachments($_messageId, $_partId = null)
828 if (! $_messageId instanceof Felamimail_Model_Message) {
829 $message = $this->_backend->get($_messageId);
831 $message = $_messageId;
834 $structure = $message->getPartStructure($_partId);
836 $attachments = array();
838 if (!array_key_exists('parts', $structure)) {
842 foreach ($structure['parts'] as $part) {
843 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
844 . ' ' . print_r($part, TRUE));
846 if ($part['type'] == 'multipart') {
847 $attachments = $attachments + $this->getAttachments($message, $part['partId']);
849 $filename = $this->_getAttachmentFilename($part);
851 if ($part['type'] == 'text' &&
852 (! is_array($part['disposition']) || ($part['disposition']['type'] == Zend_Mime::DISPOSITION_INLINE && ! array_key_exists("parameters", $part['disposition'])))
854 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
855 . ' Skipping DISPOSITION_INLINE attachment with name ' . $filename);
856 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
857 . ' part: ' . print_r($part, TRUE));
861 $attachmentData = array(
862 'content-type' => $part['contentType'],
863 'filename' => $filename,
864 'partId' => $part['partId'],
865 'size' => $part['size'],
866 'description' => $part['description'],
867 'cid' => (! empty($part['id'])) ? $part['id'] : NULL,
870 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
871 . ' Got attachment with name ' . $filename);
873 $attachments[] = $attachmentData;
881 * fetch attachment filename from part
886 protected function _getAttachmentFilename($part)
888 if (is_array($part['disposition']) && array_key_exists('parameters', $part['disposition'])
889 && array_key_exists('filename', $part['disposition']['parameters']))
891 $filename = $part['disposition']['parameters']['filename'];
892 } elseif (is_array($part['parameters']) && array_key_exists('name', $part['parameters'])) {
893 $filename = $part['parameters']['name'];
895 $filename = 'Part ' . $part['partId'];
902 * delete messages from cache by folder
906 public function deleteByFolder(Felamimail_Model_Folder $_folder)
908 $this->_backend->deleteByFolderId($_folder);
912 * update folder counts and returns list of affected folders
914 * @param array $_folderCounter (folderId => unreadcounter)
915 * @return Tinebase_Record_RecordSet of affected folders
916 * @throws Felamimail_Exception
918 protected function _updateFolderCounts($_folderCounter)
920 foreach ($_folderCounter as $folderId => $counter) {
921 $folder = Felamimail_Controller_Folder::getInstance()->get($folderId);
923 // get error condition and update array by checking $counter keys
924 if (array_key_exists('incrementUnreadCounter', $counter)) {
925 // this is only used in clearFlags() atm
926 $errorCondition = ($folder->cache_unreadcount + $counter['incrementUnreadCounter'] > $folder->cache_totalcount);
927 $updatedCounters = array(
928 'cache_unreadcount' => '+' . $counter['incrementUnreadCounter'],
930 } else if (array_key_exists('decrementMessagesCounter', $counter) && array_key_exists('decrementUnreadCounter', $counter)) {
931 $errorCondition = ($folder->cache_unreadcount < $counter['decrementUnreadCounter'] || $folder->cache_totalcount < $counter['decrementMessagesCounter']);
932 $updatedCounters = array(
933 'cache_totalcount' => '-' . $counter['decrementMessagesCounter'],
934 'cache_unreadcount' => '-' . $counter['decrementUnreadCounter']
937 throw new Felamimail_Exception('Wrong folder counter given: ' . print_r($_folderCounter, TRUE));
940 if ($errorCondition) {
941 // something went wrong => recalculate counter
942 if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ .
943 ' folder counters dont match => refresh counters'
945 $updatedCounters = Felamimail_Controller_Cache_Folder::getInstance()->getCacheFolderCounter($folder);
948 Felamimail_Controller_Folder::getInstance()->updateFolderCounter($folder, $updatedCounters);
951 return Felamimail_Controller_Folder::getInstance()->getMultiple(array_keys($_folderCounter));
955 * get punycode converter
957 * @return NULL|idna_convert
959 public function getPunycodeConverter()
961 if ($this->_punycodeConverter === NULL) {
962 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Creating idna convert class for punycode conversion.');
963 $this->_punycodeConverter = new idna_convert();
966 return $this->_punycodeConverter;
970 * get resource part id
973 * @param string $messageId
975 * @throws Tinebase_Exception_NotFound
977 * @todo add param string $folderId?
979 public function getResourcePartStructure($cid, $messageId)
981 $message = $this->get($messageId);
982 $this->_checkMessageAccount($message);
984 $attachments = $this->getAttachments($messageId);
986 foreach ($attachments as $attachment) {
987 if ($attachment['cid'] === '<' . $cid . '>') {
988 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
989 . ' Found attachment ' . $attachment['partId'] . ' with cid ' . $cid);
994 throw new Tinebase_Exception_NotFound('Resource not found');