Merge branch '2013.03'
[tine20] / tine20 / Felamimail / Controller / Message.php
1 <?php
2 /**
3  * Tine 2.0
4  *
5  * @package     Felamimail
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)
10  * 
11  * @todo        parse mail body and add <a> to telephone numbers?
12  */
13
14 /**
15  * message controller for Felamimail
16  *
17  * @package     Felamimail
18  * @subpackage  Controller
19  */
20 class Felamimail_Controller_Message extends Tinebase_Controller_Record_Abstract
21 {
22     /**
23      * application name (is needed in checkRight())
24      *
25      * @var string
26      */
27     protected $_applicationName = 'Felamimail';
28     
29     /**
30      * holds the instance of the singleton
31      *
32      * @var Felamimail_Controller_Message
33      */
34     private static $_instance = NULL;
35     
36     /**
37      * cache controller
38      *
39      * @var Felamimail_Controller_Cache_Message
40      */
41     protected $_cacheController = NULL;
42     
43     /**
44      * message backend
45      *
46      * @var Felamimail_Backend_Cache_Sql_Message
47      */
48     protected $_backend = NULL;
49     
50     /**
51      * punycode converter
52      *
53      * @var idna_convert
54      */
55     protected $_punycodeConverter = NULL;
56     
57     /**
58      * foreign application content types
59      * 
60      * @var array
61      */
62     protected $_supportedForeignContentTypes = array(
63         'Calendar'     => Felamimail_Model_Message::CONTENT_TYPE_CALENDAR,
64         'Addressbook'  => Felamimail_Model_Message::CONTENT_TYPE_VCARD,
65     );
66     
67     /**
68      * the constructor
69      *
70      * don't use the constructor. use the singleton
71      */
72     private function __construct() 
73     {
74         $this->_modelName = 'Felamimail_Model_Message';
75         $this->_doContainerACLChecks = FALSE;
76         $this->_backend = new Felamimail_Backend_Cache_Sql_Message();
77         
78         $this->_cacheController = Felamimail_Controller_Cache_Message::getInstance();
79     }
80     
81     /**
82      * don't clone. Use the singleton.
83      *
84      */
85     private function __clone() 
86     {
87     }
88     
89     /**
90      * the singleton pattern
91      *
92      * @return Felamimail_Controller_Message
93      */
94     public static function getInstance() 
95     {
96         if (self::$_instance === NULL) {
97             self::$_instance = new Felamimail_Controller_Message();
98         }
99         
100         return self::$_instance;
101     }
102     
103     /**
104      * Removes accounts where current user has no access to
105      * 
106      * @param Tinebase_Model_Filter_FilterGroup $_filter
107      * @param string $_action get|update
108      * 
109      * @todo move logic to Felamimail_Model_MessageFilter
110      */
111     public function checkFilterACL(Tinebase_Model_Filter_FilterGroup $_filter, $_action = 'get')
112     {
113         $accountFilter = $_filter->getFilter('account_id');
114         
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());
118         }
119     }
120
121     /**
122      * append a new message to given folder
123      *
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
127      */
128     public function appendMessage($_folder, $_message, $_flags = null)
129     {
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;
133         
134         $imapBackend = $this->_getBackendAndSelectFolder(NULL, $folder);
135         $imapBackend->appendMessage($message, $folder->globalname, $flags);
136     }
137     
138     /**
139      * get complete message by id
140      *
141      * @param string|Felamimail_Model_Message  $_id
142      * @param string                            $_partId
143      * @param boolean                          $_setSeen
144      * @return Felamimail_Model_Message
145      */
146     public function getCompleteMessage($_id, $_partId = NULL, $_setSeen = FALSE)
147     {
148         if ($_id instanceof Felamimail_Model_Message) {
149             $message = $_id;
150         } else {
151             $message = $this->get($_id);
152         }
153         
154         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . 
155             ' Getting message content ' . $message->messageuid 
156         );
157         
158         $folder = Felamimail_Controller_Folder::getInstance()->get($message->folder_id);
159         $account = Felamimail_Controller_Account::getInstance()->get($folder->account_id);
160         
161         $this->_checkMessageAccount($message, $account);
162         
163         $message = $this->_getCompleteMessageContent($message, $account, $_partId);
164         
165         if ($_setSeen) {
166             Felamimail_Controller_Message_Flags::getInstance()->setSeenFlag($message);
167         }
168         
169         $this->prepareAndProcessParts($message);
170         
171         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . print_r($message->toArray(), true));
172         
173         return $message;
174     }
175     
176     /**
177      * check if account of message is belonging to user
178      * 
179      * @param Felamimail_Model_Message $message
180      * @param Felamimail_Model_Account $account
181      * @throws Tinebase_Exception_AccessDenied
182      * 
183      * @todo think about moving this to get() / _checkGrant()
184      */
185     protected function _checkMessageAccount($message, $account = NULL)
186     {
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');
190         }
191     }
192     
193     /**
194      * get message content (body, headers and attachments)
195      * 
196      * @param Felamimail_Model_Message $_message
197      * @param Felamimail_Model_Account $_account
198      * @param string $_partId
199      */
200     protected function _getCompleteMessageContent(Felamimail_Model_Message $_message, Felamimail_Model_Account $_account, $_partId = NULL)
201     {
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;
205         
206         $headers     = $this->getMessageHeaders($_message, $_partId, true);
207         $body        = $this->getMessageBody($_message, $_partId, $mimeType, $_account, true);
208         $attachments = $this->getAttachments($_message, $_partId);
209         
210         if ($_partId === null) {
211             $message = $_message;
212             
213             $message->body        = $body;
214             $message->headers     = $headers;
215             $message->attachments = $attachments;
216             // make sure the structure is present
217             $message->structure   = $message->structure;
218             
219         } else {
220             // create new object for rfc822 message
221             $structure = $_message->getPartStructure($_partId, FALSE);
222         
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,
229                 'body'        => $body,
230                 'headers'     => $headers,
231                 'attachments' => $attachments
232             ));
233         
234             $message->parseHeaders($headers);
235         
236             $structure = array_key_exists('messageStructure', $structure) ? $structure['messageStructure'] : $structure;
237             $message->parseStructure($structure);
238         }
239         
240         return $message;
241     }
242     
243     /**
244      * send reading confirmation for message
245      * 
246      * @param string $messageId
247      */
248     public function sendReadingConfirmation($messageId)
249     {
250         $message = $this->get($messageId);
251         $this->_checkMessageAccount($message);
252         $message->sendReadingConfirmation();
253     }
254     
255     /**
256      * prepare message parts that could be interesting for other apps
257      * 
258      * @param Felamimail_Model_Message $_message
259      */
260     public function prepareAndProcessParts(Felamimail_Model_Message $_message)
261     {
262         $preparedParts = new Tinebase_Record_RecordSet('Felamimail_Model_PreparedMessagePart');
263         
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.');
268                 continue;
269             }
270
271             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
272                 . ' Looking for ' . $application . '[' . $contentType . '] content ...');
273             
274             $parts = $_message->getBodyParts(NULL, $contentType);
275             foreach ($parts as $partId => $partData) {
276                 if ($partData['contentType'] !== $contentType) {
277                     continue;
278                 }
279                 
280                 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ 
281                     . ' ' . $application . '[' . $contentType . '] content found.');
282                 
283                 $preparedPart = $this->_getForeignMessagePart($_message, $partId, $partData);
284                 if ($preparedPart) {
285                     $this->_processForeignMessagePart($application, $preparedPart);
286                     $preparedParts->addRecord(new Felamimail_Model_PreparedMessagePart(array(
287                         'id'             => $_message->getId() . '_' . $partId,
288                         'contentType'     => $contentType,
289                         'preparedData'   => $preparedPart,
290                     )));
291                 }
292             }
293         }
294         
295         $_message->preparedParts = $preparedParts;
296     }
297     
298     /**
299     * get foreign message parts
300     * 
301     * - calendar invitations
302     * - addressbook vcards
303     * - ...
304     *
305     * @param Felamimail_Model_Message $_message
306     * @param string $_partId
307     * @param array $_partData
308     * @return NULL|Tinebase_Record_Abstract
309     */
310     protected function _getForeignMessagePart(Felamimail_Model_Message $_message, $_partId, $_partData)
311     {
312         $part = $this->getMessagePart($_message, $_partId);
313         
314         $userAgent = (isset($_message->headers['user-agent'])) ? $_message->headers['user-agent'] : NULL;
315         $parameters = (isset($_partData['parameters'])) ? $_partData['parameters'] : array();
316         $decodedContent = $part->getDecodedContent();
317         
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.');
322                     return NULL;
323                 }
324                 
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,
331                 ));
332                 break;
333             default:
334                 if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . ' Could not create iMIP of content type ' . $part->type);
335                 $partData = NULL;
336         }
337         
338         return $partData;
339     }
340     
341     /**
342      * process foreign iMIP part
343      * 
344      * @param string $_application
345      * @param Tinebase_Record_Abstract $_iMIP
346      * @return mixed
347      * 
348      * @todo use iMIP factory?
349      */
350     protected function _processForeignMessagePart($_application, $_iMIP)
351     {
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);
355             return NULL;
356         }
357         
358         $iMIPFrontend = new $iMIPFrontendClass();
359         try {
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());
364             $result = NULL;
365         }
366         
367         return $result;
368     }
369
370     /**
371      * get iMIP by message and part id
372      * 
373      * @param string $_iMIPId
374      * @throws Tinebase_Exception_InvalidArgument
375      * @return Tinebase_Record_Abstract
376      */
377     public function getiMIP($_iMIPId)
378     {
379         if (strpos($_iMIPId, '_') === FALSE) {
380             throw new Tinebase_Exception_InvalidArgument('messageId_partId expecetd.');
381         }
382         
383         list($messageId, $partId) = explode('_', $_iMIPId);
384         
385         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
386             . ' Fetching ' . $messageId . '[' . $partId . '] part with iMIP data ...');
387         
388         $message = $this->get($messageId);
389         
390         $iMIPPartStructure = $message->getPartStructure($partId);
391         $iMIP = $this->_getForeignMessagePart($message, $partId, $iMIPPartStructure);
392         
393         return $iMIP;
394     }
395     
396     /**
397      * get message part
398      *
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
404      */
405     public function getMessagePart($_id, $_partId = NULL, $_onlyBodyOfRfc822 = FALSE, $_partStructure = NULL)
406     {
407         if ($_id instanceof Felamimail_Model_Message) {
408             $message = $_id;
409         } else {
410             $message = $this->get($_id);
411         }
412         
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);
416         
417         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
418             . ' ' . print_r($partStructure, TRUE));
419         
420         $rawContent = $this->_getPartContent($message, $_partId, $partStructure, $_onlyBodyOfRfc822);
421         
422         $part = $this->_createMimePart($rawContent, $partStructure);
423         
424         return $part;
425     }
426     
427     /**
428      * get part content (and update structure) from message part
429      * 
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)
434      * @return string
435      */
436     protected function _getPartContent(Felamimail_Model_Message $_message, $_partId, &$_partStructure, $_onlyBodyOfRfc822 = FALSE)
437     {
438         $imapBackend = $this->_getBackendAndSelectFolder($_message->folder_id);
439         
440         $rawContent = '';
441         
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'];
448                 }
449             } else {
450                 $logmessage = 'Fetch message part (HEADER + TEXT) ' . $_partId . ' of messageuid ' . $_message->messageuid;
451                 $rawContent .= $imapBackend->getRawContent($_message->messageuid, $_partId . '.HEADER', true);
452             }
453             
454             $section = $_partId . '.TEXT';
455         } else {
456             $logmessage = ($_partId !== NULL) 
457                 ? 'Fetch message part ' . $_partId . ' of messageuid ' . $_message->messageuid 
458                 : 'Fetch main of messageuid ' . $_message->messageuid;
459             
460             $section = $_partId;
461         }
462         
463         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . print_r($_partStructure, TRUE));
464         
465         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' ' . $logmessage);
466         
467         $rawContent .= $imapBackend->getRawContent($_message->messageuid, $section, TRUE);
468         
469         return $rawContent;
470     }
471     
472     /**
473      * create mime part from raw content and part structure
474      * 
475      * @param string $_rawContent
476      * @param array $_partStructure
477      * @return Zend_Mime_Part
478      */
479     protected function _createMimePart($_rawContent, $_partStructure)
480     {
481         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' Content: ' . $_rawContent);
482         
483         $stream = fopen("php://temp", 'r+');
484         fputs($stream, $_rawContent);
485         rewind($stream);
486         
487         unset($_rawContent);
488         
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;
504             }
505         }
506         if (empty($part->filename) && array_key_exists('parameters', $_partStructure) && array_key_exists('name', $_partStructure['parameters'])) {
507             $part->filename = $_partStructure['parameters']['name'];
508         }
509         
510         return $part;
511     }
512     
513     /**
514      * get message body
515      * 
516      * @param string|Felamimail_Model_Message $_messageId
517      * @param string $_partId
518      * @param string $_contentType
519      * @param Felamimail_Model_Account $_account
520      * @return string
521      */
522     public function getMessageBody($_messageId, $_partId, $_contentType, $_account = NULL)
523     {
524         $message = ($_messageId instanceof Felamimail_Model_Message) ? $_messageId : $this->get($_messageId);
525
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 . ')');
528         
529         $cacheBody = Felamimail_Config::getInstance()->get(Felamimail_Config::CACHE_EMAIL_BODY, TRUE);
530         if ($cacheBody) {
531             $cache = Tinebase_Core::getCache();
532             $cacheId = $this->_getMessageBodyCacheId($message, $_partId, $_contentType, $_account);
533             
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);
537             }
538         }
539         
540         $messageBody = $this->_getAndDecodeMessageBody($message, $_partId, $_contentType, $_account);
541         
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);
546         
547         if ($cacheBody) {
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);
550         }
551         
552         return $messageBody;
553     }
554     
555     /**
556      * get message body cache id
557      * 
558      * @param string|Felamimail_Model_Message $_messageId
559      * @param string $_partId
560      * @param string $_contentType
561      * @param Felamimail_Model_Account $_account
562      * @return string
563      */
564     protected function _getMessageBodyCacheId($_message, $_partId, $_contentType, $_account)
565     {
566         $cacheId = 'getMessageBody_'
567             . $_message->getId()
568             . str_replace('.', '', $_partId)
569             . substr($_contentType, -4)
570             . (($_account !== NULL) ? 'acc' : '');
571         
572         return $cacheId;
573     }
574     
575     /**
576      * get and decode message body
577      * 
578      * @param Felamimail_Model_Message $_message
579      * @param string $_partId
580      * @param string $_contentType
581      * @param Felamimail_Model_Account $_account
582      * @return string
583      * 
584      * @todo multipart_related messages should deliver inline images
585      */
586     protected function _getAndDecodeMessageBody(Felamimail_Model_Message $_message, $_partId, $_contentType, $_account = NULL)
587     {
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);
592             return '';
593         }
594         
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);
599             return '';
600         }
601         
602         $messageBody = '';
603         
604         foreach ($bodyParts as $partId => $partStructure) {
605             $bodyPart = $this->getMessagePart($_message, $partId, TRUE, $partStructure);
606             
607             $body = Tinebase_Mail::getDecodedContent($bodyPart, $partStructure);
608             
609             if ($partStructure['contentType'] != Zend_Mime::TYPE_TEXT) {
610                 $bodyCharCountBefore = strlen($body);
611                 $body = $this->_purifyBodyContent($body, $_message->getId());
612                 $bodyCharCountAfter = strlen($body);
613                 
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);
620                 }
621             }
622             
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);
628                 }
629             } else {
630                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
631                     . ' Do not convert ' . $bodyPart->type . ' part to ' . $_contentType);
632             }
633             
634             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
635                 . ' Adding part ' . $partId . ' to message body.');
636             
637             $messageBody .= $body;
638         }
639         
640         return $messageBody;
641     }
642     
643     /**
644      * use html purifier to remove 'bad' tags/attributes from html body
645      *
646      * @param string $_content
647      * @param string $messageId
648      * @return string
649      */
650     protected function _purifyBodyContent($_content, $messageId)
651     {
652         if (!defined('HTMLPURIFIER_PREFIX')) {
653             define('HTMLPURIFIER_PREFIX', realpath(dirname(__FILE__) . '/../../library/HTMLPurifier'));
654         }
655         
656         $config = Tinebase_Core::getConfig();
657         $path = ($config->caching && $config->caching->active && $config->caching->path) 
658             ? $config->caching->path : Tinebase_Core::getTempDir();
659
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);
664         
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);
671         
672         // some config values to consider
673         /*
674         $config->set('Attr.EnableID', true);
675         $config->set('Attr.ClassUseCDATA', true);
676         $config->set('CSS.AllowTricky', true);
677         */
678         $config->set('Cache.SerializerPath', $path);
679         $config->set('URI.AllowedSchemes', array(
680             'http' => true,
681             'https' => true,
682             'mailto' => true,
683             'data' => true,
684             'cid' => true
685         ));
686         $config->set('Felamimail.messageId', $messageId);
687         
688         $this->_transformBodyTags($config);
689         
690         // add uri filter
691         $uri = $config->getDefinition('URI');
692         $uri->addFilter(new Felamimail_HTMLPurifier_URIFilter_TransformURI(), $config);
693         
694         // add cid uri scheme
695         require_once(dirname(dirname(__FILE__)) . '/HTMLPurifier/URIScheme/cid.php');
696         
697         $purifier = new HTMLPurifier($config);
698         $content = $purifier->purify($_content);
699
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);
702
703         return $content;
704     }
705     
706     /**
707      * transform some tags / attributes
708      * 
709      * @param HTMLPurifier_Config $config
710      */
711     protected function _transformBodyTags(HTMLPurifier_Config $config)
712     {
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();
718             
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();
723         } else {
724              if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
725                 . ' Could not get HTMLDefinition, no transformation possible');
726         }
727     }
728     
729     /**
730      * get message headers
731      * 
732      * @param string|Felamimail_Model_Message $_messageId
733      * @param boolean $_readOnly
734      * @return array
735      * @throws Felamimail_Exception_IMAPMessageNotFound
736      */
737     public function getMessageHeaders($_messageId, $_partId = null, $_readOnly = false)
738     {
739         if (! $_messageId instanceof Felamimail_Model_Message) {
740             $message = $this->_backend->get($_messageId);
741         } else {
742             $message = $_messageId;
743         }
744         
745         $cache = Tinebase_Core::get('cache');
746         $cacheId = 'getMessageHeaders' . $message->getId() . str_replace('.', '', $_partId);
747         if ($cache->test($cacheId)) {
748             return $cache->load($cacheId);
749         }
750         
751         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
752             . ' Fetching headers for message uid ' .  $message->messageuid . ' (part:' . $_partId . ')');
753         
754         try {
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');
760         }
761         
762         if ($imapBackend === null) {
763             throw new Felamimail_Exception('Failed to get imap backend');
764         }
765         
766         $section = ($_partId === null) ?  'HEADER' : $_partId . '.HEADER';
767         
768         try {
769             $rawHeaders = $imapBackend->getRawContent($message->messageuid, $section, $_readOnly);
770         } catch (Felamimail_Exception_IMAPMessageNotFound $feimnf) {
771             $this->_backend->delete($message->getId());
772             throw $feimnf;
773         }
774         
775         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ 
776             . ' Fetched Headers: ' . $rawHeaders);
777         
778         Zend_Mime_Decode::splitMessage($rawHeaders, $headers, $null);
779         
780         $cache->save($headers, $cacheId, array('getMessageHeaders'), 86400);
781         
782         return $headers;
783     }
784     
785     /**
786      * get imap backend and folder (and select folder)
787      *
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
795      */
796     protected function _getBackendAndSelectFolder($_folderId = NULL, &$_folder = NULL, $_select = TRUE, Felamimail_Backend_ImapProxy $_imapBackend = NULL)
797     {
798         if ($_folder === NULL || empty($_folder)) {
799             $folderBackend  = new Felamimail_Backend_Folder();
800             $_folder = $folderBackend->get($_folderId);
801         }
802         
803         try {
804             $imapBackend = ($_imapBackend === NULL) ? Felamimail_Backend_ImapFactory::factory($_folder->account_id) : $_imapBackend;
805             if ($_select) {
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));
809             }
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());
815         }
816         
817         return $imapBackend;
818     }
819     
820     /**
821      * get attachments of message
822      *
823      * @param  array  $_structure
824      * @return array
825      */
826     public function getAttachments($_messageId, $_partId = null)
827     {
828         if (! $_messageId instanceof Felamimail_Model_Message) {
829             $message = $this->_backend->get($_messageId);
830         } else {
831             $message = $_messageId;
832         }
833         
834         $structure = $message->getPartStructure($_partId);
835
836         $attachments = array();
837         
838         if (!array_key_exists('parts', $structure)) {
839             return $attachments;
840         }
841         
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));
845             
846             if ($part['type'] == 'multipart') {
847                 $attachments = $attachments + $this->getAttachments($message, $part['partId']);
848             } else {
849                 $filename = $this->_getAttachmentFilename($part);
850                 
851                 if ($part['type'] == 'text' && 
852                     (! is_array($part['disposition']) || ($part['disposition']['type'] == Zend_Mime::DISPOSITION_INLINE && ! array_key_exists("parameters", $part['disposition'])))
853                 ) {
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));
858                     continue;
859                 }
860                 
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,
868                 );
869                 
870                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
871                     . ' Got attachment with name ' . $filename);
872                 
873                 $attachments[] = $attachmentData;
874             }
875         }
876         
877         return $attachments;
878     }
879     
880     /**
881      * fetch attachment filename from part
882      * 
883      * @param array $part
884      * @return string
885      */
886     protected function _getAttachmentFilename($part)
887     {
888         if (is_array($part['disposition']) && array_key_exists('parameters', $part['disposition']) 
889             && array_key_exists('filename', $part['disposition']['parameters'])) 
890         {
891             $filename = $part['disposition']['parameters']['filename'];
892         } elseif (is_array($part['parameters']) && array_key_exists('name', $part['parameters'])) {
893             $filename = $part['parameters']['name'];
894         } else {
895             $filename = 'Part ' . $part['partId'];
896         }
897         
898         return $filename;
899     }
900     
901     /**
902      * delete messages from cache by folder
903      * 
904      * @param $_folder
905      */
906     public function deleteByFolder(Felamimail_Model_Folder $_folder)
907     {
908         $this->_backend->deleteByFolderId($_folder);
909     }
910
911     /**
912      * update folder counts and returns list of affected folders
913      * 
914      * @param array $_folderCounter (folderId => unreadcounter)
915      * @return Tinebase_Record_RecordSet of affected folders
916      * @throws Felamimail_Exception
917      */
918     protected function _updateFolderCounts($_folderCounter)
919     {
920         foreach ($_folderCounter as $folderId => $counter) {
921             $folder = Felamimail_Controller_Folder::getInstance()->get($folderId);
922             
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'],
929                 );
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']
935                 );
936             } else {
937                 throw new Felamimail_Exception('Wrong folder counter given: ' . print_r($_folderCounter, TRUE));
938             }
939             
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'
944                 );
945                 $updatedCounters = Felamimail_Controller_Cache_Folder::getInstance()->getCacheFolderCounter($folder);
946             }
947             
948             Felamimail_Controller_Folder::getInstance()->updateFolderCounter($folder, $updatedCounters);
949         }
950         
951         return Felamimail_Controller_Folder::getInstance()->getMultiple(array_keys($_folderCounter));
952     }
953     
954     /**
955      * get punycode converter
956      * 
957      * @return NULL|idna_convert
958      */
959     public function getPunycodeConverter()
960     {
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();
964         }
965         
966         return $this->_punycodeConverter;
967     }
968
969     /**
970      * get resource part id
971      * 
972      * @param string $cid
973      * @param string $messageId
974      * @return array
975      * @throws Tinebase_Exception_NotFound
976      * 
977      * @todo add param string $folderId?
978      */
979     public function getResourcePartStructure($cid, $messageId)
980     {
981         $message = $this->get($messageId);
982         $this->_checkMessageAccount($message);
983         
984         $attachments = $this->getAttachments($messageId);
985         
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);
990                 return $attachment;
991             }
992         }
993         
994         throw new Tinebase_Exception_NotFound('Resource not found');
995     }
996 }