relaxes winmail.dat handling
[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                 'account_id'  => $_message->account_id,
225                 'messageuid'  => $_message->messageuid,
226                 'folder_id'   => $_message->folder_id,
227                 'received'    => $_message->received,
228                 'size'        => isset($structure['size']) ? $structure['size'] : 0,
229                 'partid'      => $_partId,
230                 'body'        => $body,
231                 'headers'     => $headers,
232                 'attachments' => $attachments
233             ));
234             
235             $message->parseHeaders($headers);
236             
237             $structure = isset($structure['messageStructure']) ? $structure['messageStructure'] : $structure;
238             $message->parseStructure($structure);
239         }
240         
241         return $message;
242     }
243     
244     /**
245      * send reading confirmation for message
246      * 
247      * @param string $messageId
248      */
249     public function sendReadingConfirmation($messageId)
250     {
251         $message = $this->get($messageId);
252         $this->_checkMessageAccount($message);
253         $message->sendReadingConfirmation();
254     }
255     
256     /**
257      * prepare message parts that could be interesting for other apps
258      * 
259      * @param Felamimail_Model_Message $_message
260      */
261     public function prepareAndProcessParts(Felamimail_Model_Message $_message)
262     {
263         $preparedParts = new Tinebase_Record_RecordSet('Felamimail_Model_PreparedMessagePart');
264         
265         foreach ($this->_supportedForeignContentTypes as $application => $contentType) {
266             if (! Tinebase_Application::getInstance()->isInstalled($application) || ! Tinebase_Core::getUser()->hasRight($application, Tinebase_Acl_Rights::RUN)) {
267                 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ 
268                     . ' ' . $application . ' not installed or access denied.');
269                 continue;
270             }
271
272             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
273                 . ' Looking for ' . $application . '[' . $contentType . '] content ...');
274             
275             $parts = $_message->getBodyParts(NULL, $contentType);
276             foreach ($parts as $partId => $partData) {
277                 if ($partData['contentType'] !== $contentType) {
278                     continue;
279                 }
280                 
281                 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ 
282                     . ' ' . $application . '[' . $contentType . '] content found.');
283                 
284                 $preparedPart = $this->_getForeignMessagePart($_message, $partId, $partData);
285                 if ($preparedPart) {
286                     $this->_processForeignMessagePart($application, $preparedPart);
287                     $preparedParts->addRecord(new Felamimail_Model_PreparedMessagePart(array(
288                         'id'             => $_message->getId() . '_' . $partId,
289                         'contentType'     => $contentType,
290                         'preparedData'   => $preparedPart,
291                     )));
292                 }
293             }
294         }
295         
296         $_message->preparedParts = $preparedParts;
297     }
298     
299     /**
300     * get foreign message parts
301     * 
302     * - calendar invitations
303     * - addressbook vcards
304     * - ...
305     *
306     * @param Felamimail_Model_Message $_message
307     * @param string $_partId
308     * @param array $_partData
309     * @return NULL|Tinebase_Record_Abstract
310     */
311     protected function _getForeignMessagePart(Felamimail_Model_Message $_message, $_partId, $_partData)
312     {
313         $part = $this->getMessagePart($_message, $_partId);
314         
315         $userAgent = (isset($_message->headers['user-agent'])) ? $_message->headers['user-agent'] : NULL;
316         $parameters = (isset($_partData['parameters'])) ? $_partData['parameters'] : array();
317         $decodedContent = $part->getDecodedContent();
318         
319         switch ($part->type) {
320             case Felamimail_Model_Message::CONTENT_TYPE_CALENDAR:
321                 if (! version_compare(PHP_VERSION, '5.3.0', '>=')) {
322                     if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . ' PHP 5.3+ is needed for vcalendar support.');
323                     return NULL;
324                 }
325                 
326                 $partData = new Calendar_Model_iMIP(array(
327                     'id'             => $_message->getId() . '_' . $_partId,
328                     'ics'            => $decodedContent,
329                     'method'         => (isset($parameters['method'])) ? $parameters['method'] : NULL,
330                     'originator'     => $_message->from_email,
331                     'userAgent'      => $userAgent,
332                 ));
333                 break;
334             default:
335                 if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . ' Could not create iMIP of content type ' . $part->type);
336                 $partData = NULL;
337         }
338         
339         return $partData;
340     }
341     
342     /**
343      * process foreign iMIP part
344      * 
345      * @param string $_application
346      * @param Tinebase_Record_Abstract $_iMIP
347      * @return mixed
348      * 
349      * @todo use iMIP factory?
350      */
351     protected function _processForeignMessagePart($_application, $_iMIP)
352     {
353         $iMIPFrontendClass = $_application . '_Frontend_iMIP';
354         if (! class_exists($iMIPFrontendClass)) {
355             if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . ' iMIP class not found in application ' . $_application);
356             return NULL;
357         }
358         
359         $iMIPFrontend = new $iMIPFrontendClass();
360         try {
361             $result = $iMIPFrontend->autoProcess($_iMIP);
362         } catch (Exception $e) {
363             if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Processing failed: ' . $e->getMessage());
364             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' ' . $e->getTraceAsString());
365             $result = NULL;
366         }
367         
368         return $result;
369     }
370
371     /**
372      * get iMIP by message and part id
373      * 
374      * @param string $_iMIPId
375      * @throws Tinebase_Exception_InvalidArgument
376      * @return Tinebase_Record_Abstract
377      */
378     public function getiMIP($_iMIPId)
379     {
380         if (strpos($_iMIPId, '_') === FALSE) {
381             throw new Tinebase_Exception_InvalidArgument('messageId_partId expecetd.');
382         }
383         
384         list($messageId, $partId) = explode('_', $_iMIPId);
385         
386         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
387             . ' Fetching ' . $messageId . '[' . $partId . '] part with iMIP data ...');
388         
389         $message = $this->get($messageId);
390         
391         $iMIPPartStructure = $message->getPartStructure($partId);
392         $iMIP = $this->_getForeignMessagePart($message, $partId, $iMIPPartStructure);
393         
394         return $iMIP;
395     }
396     
397     /**
398      * get message part
399      *
400      * @param string|Felamimail_Model_Message $_id
401      * @param string $_partId (the part id, can look like this: 1.3.2 -> returns the second part of third part of first part...)
402      * @param boolean $_onlyBodyOfRfc822 only fetch body of rfc822 messages (FALSE to get headers, too)
403      * @param array $_partStructure (is fetched if NULL/omitted)
404      * @return Zend_Mime_Part
405      */
406     public function getMessagePart($_id, $_partId = NULL, $_onlyBodyOfRfc822 = FALSE, $_partStructure = NULL)
407     {
408         if ($_id instanceof Felamimail_Model_Message) {
409             $message = $_id;
410         } else {
411             $message = $this->get($_id);
412         }
413         
414         // need to refetch part structure of RFC822 messages because message structure is used instead
415         $partContentType = ($_partId && isset($message->structure['parts'][$_partId])) ? $message->structure['parts'][$_partId]['contentType'] : NULL;
416         $partStructure  = ($_partStructure !== NULL && $partContentType !== Felamimail_Model_Message::CONTENT_TYPE_MESSAGE_RFC822) ? $_partStructure : $message->getPartStructure($_partId, FALSE);
417         
418         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
419             . ' ' . print_r($partStructure, TRUE));
420         
421         $rawContent = $this->_getPartContent($message, $_partId, $partStructure, $_onlyBodyOfRfc822);
422         
423         $part = $this->_createMimePart($rawContent, $partStructure);
424         
425         return $part;
426     }
427     
428     /**
429      * get part content (and update structure) from message part
430      * 
431      * @param Felamimail_Model_Message $_message
432      * @param string $_partId
433      * @param array $_partStructure
434      * @param boolean $_onlyBodyOfRfc822 only fetch body of rfc822 messages (FALSE to get headers, too)
435      * @return string
436      */
437     protected function _getPartContent(Felamimail_Model_Message $_message, $_partId, &$_partStructure, $_onlyBodyOfRfc822 = FALSE)
438     {
439         $imapBackend = $this->_getBackendAndSelectFolder($_message->folder_id);
440         
441         $rawContent = '';
442         
443         // special handling for rfc822 messages
444         if ($_partId !== NULL && $_partStructure['contentType'] === Felamimail_Model_Message::CONTENT_TYPE_MESSAGE_RFC822) {
445             if ($_onlyBodyOfRfc822) {
446                 $logmessage = 'Fetch message part (TEXT) ' . $_partId . ' of messageuid ' . $_message->messageuid;
447                 if ((isset($_partStructure['messageStructure']) || array_key_exists('messageStructure', $_partStructure))) {
448                     $_partStructure = $_partStructure['messageStructure'];
449                 }
450             } else {
451                 $logmessage = 'Fetch message part (HEADER + TEXT) ' . $_partId . ' of messageuid ' . $_message->messageuid;
452                 $rawContent .= $imapBackend->getRawContent($_message->messageuid, $_partId . '.HEADER', true);
453             }
454             
455             $section = $_partId . '.TEXT';
456         } else {
457             $logmessage = ($_partId !== NULL) 
458                 ? 'Fetch message part ' . $_partId . ' of messageuid ' . $_message->messageuid 
459                 : 'Fetch main of messageuid ' . $_message->messageuid;
460             
461             $section = $_partId;
462         }
463         
464         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . print_r($_partStructure, TRUE));
465         
466         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' ' . $logmessage);
467         
468         $rawContent .= $imapBackend->getRawContent($_message->messageuid, $section, TRUE);
469         
470         return $rawContent;
471     }
472     
473     /**
474      * create mime part from raw content and part structure
475      * 
476      * @param string $_rawContent
477      * @param array $_partStructure
478      * @return Zend_Mime_Part
479      */
480     protected function _createMimePart($_rawContent, $_partStructure)
481     {
482         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' Content: ' . $_rawContent);
483         
484         $stream = fopen("php://temp", 'r+');
485         fputs($stream, $_rawContent);
486         rewind($stream);
487         
488         unset($_rawContent);
489         
490         $part = new Zend_Mime_Part($stream);
491         $part->type        = $_partStructure['contentType'];
492         $part->encoding    = (isset($_partStructure['encoding']) || array_key_exists('encoding', $_partStructure)) ? $_partStructure['encoding'] : null;
493         $part->id          = (isset($_partStructure['id']) || array_key_exists('id', $_partStructure)) ? $_partStructure['id'] : null;
494         $part->description = (isset($_partStructure['description']) || array_key_exists('description', $_partStructure)) ? $_partStructure['description'] : null;
495         $part->charset     = (isset($_partStructure['parameters']['charset']) || array_key_exists('charset', $_partStructure['parameters'])) 
496             ? $_partStructure['parameters']['charset'] 
497             : Tinebase_Mail::DEFAULT_FALLBACK_CHARSET;
498         $part->boundary    = (isset($_partStructure['parameters']['boundary']) || array_key_exists('boundary', $_partStructure['parameters'])) ? $_partStructure['parameters']['boundary'] : null;
499         $part->location    = $_partStructure['location'];
500         $part->language    = $_partStructure['language'];
501         if (is_array($_partStructure['disposition'])) {
502             $part->disposition = $_partStructure['disposition']['type'];
503             if ((isset($_partStructure['disposition']['parameters']) || array_key_exists('parameters', $_partStructure['disposition']))) {
504                 $part->filename    = (isset($_partStructure['disposition']['parameters']['filename']) || array_key_exists('filename', $_partStructure['disposition']['parameters'])) ? $_partStructure['disposition']['parameters']['filename'] : null;
505             }
506         }
507         if (empty($part->filename) && (isset($_partStructure['parameters']) || array_key_exists('parameters', $_partStructure)) && (isset($_partStructure['parameters']['name']) || array_key_exists('name', $_partStructure['parameters']))) {
508             $part->filename = $_partStructure['parameters']['name'];
509         }
510         
511         return $part;
512     }
513     
514     /**
515      * get message body
516      * 
517      * @param string|Felamimail_Model_Message $_messageId
518      * @param string $_partId
519      * @param string $_contentType
520      * @param Felamimail_Model_Account $_account
521      * @return string
522      */
523     public function getMessageBody($_messageId, $_partId, $_contentType, $_account = NULL)
524     {
525         $message = ($_messageId instanceof Felamimail_Model_Message) ? $_messageId : $this->get($_messageId);
526
527         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
528             . ' Get Message body (part: ' . $_partId . ') of message id ' . $message->getId() . ' (content type ' . $_contentType . ')');
529         
530         $cacheBody = Felamimail_Config::getInstance()->get(Felamimail_Config::CACHE_EMAIL_BODY, TRUE);
531         if ($cacheBody) {
532             $cache = Tinebase_Core::getCache();
533             $cacheId = $this->_getMessageBodyCacheId($message, $_partId, $_contentType, $_account);
534             
535             if ($cache->test($cacheId)) {
536                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Getting Message from cache.');
537                 return $cache->load($cacheId);
538             }
539         }
540         
541         $messageBody = $this->_getAndDecodeMessageBody($message, $_partId, $_contentType, $_account);
542         
543         // activate garbage collection (@see 0008216: HTMLPurifier/TokenFactory.php : Allowed memory size exhausted)
544         $cycles = gc_collect_cycles();
545         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
546             . ' Current mem usage after gc_collect_cycles(' . $cycles . ' ): ' . memory_get_usage()/1024/1024);
547         
548         if ($cacheBody) {
549             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Put message body into Tinebase cache (for 24 hours).');
550             $cache->save($messageBody, $cacheId, array('getMessageBody'), 86400);
551         }
552         
553         return $messageBody;
554     }
555     
556     /**
557      * get message body cache id
558      * 
559      * @param string|Felamimail_Model_Message $_messageId
560      * @param string $_partId
561      * @param string $_contentType
562      * @param Felamimail_Model_Account $_account
563      * @return string
564      */
565     protected function _getMessageBodyCacheId($_message, $_partId, $_contentType, $_account)
566     {
567         $cacheId = 'getMessageBody_'
568             . $_message->getId()
569             . str_replace('.', '', $_partId)
570             . substr($_contentType, -4)
571             . (($_account !== NULL) ? 'acc' : '');
572         
573         return $cacheId;
574     }
575     
576     /**
577      * get and decode message body
578      * 
579      * @param Felamimail_Model_Message $_message
580      * @param string $_partId
581      * @param string $_contentType
582      * @param Felamimail_Model_Account $_account
583      * @return string
584      * 
585      * @todo multipart_related messages should deliver inline images
586      */
587     protected function _getAndDecodeMessageBody(Felamimail_Model_Message $_message, $_partId, $_contentType, $_account = NULL)
588     {
589         $structure = $_message->getPartStructure($_partId);
590         if (empty($structure)) {
591             if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
592                 . ' Empty structure, could not find body parts of message ' . $_message->subject);
593             return '';
594         }
595         
596         $bodyParts = $_message->getBodyParts($structure, $_contentType);
597         if (empty($bodyParts)) {
598             if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
599                 . ' Could not find body parts of message ' . $_message->subject);
600             return '';
601         }
602         
603         $messageBody = '';
604         
605         foreach ($bodyParts as $partId => $partStructure) {
606             $bodyPart = $this->getMessagePart($_message, $partId, TRUE, $partStructure);
607             
608             $body = Tinebase_Mail::getDecodedContent($bodyPart, $partStructure);
609             
610             if ($partStructure['contentType'] != Zend_Mime::TYPE_TEXT) {
611                 $bodyCharCountBefore = strlen($body);
612                 $body = $this->_purifyBodyContent($body, $_message->getId());
613                 $bodyCharCountAfter = strlen($body);
614                 
615                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
616                     . ' Purifying removed ' . ($bodyCharCountBefore - $bodyCharCountAfter) . ' / ' . $bodyCharCountBefore . ' characters.');
617                 if ($_message->text_partid && $bodyCharCountAfter < $bodyCharCountBefore / 10) {
618                     if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
619                         . ' Purify may have removed (more than 9/10) too many chars, using alternative text message part.');
620                     $result = $this->_getAndDecodeMessageBody($_message, $_message->text_partid , Zend_Mime::TYPE_TEXT, $_account);
621                     return Felamimail_Message::convertContentType(Zend_Mime::TYPE_TEXT, Zend_Mime::TYPE_HTML, $result);
622                 }
623             }
624             
625             if (! ($_account !== NULL && $_account->display_format === Felamimail_Model_Account::DISPLAY_CONTENT_TYPE && $bodyPart->type == Zend_Mime::TYPE_TEXT)) {
626                 $body = Felamimail_Message::convertContentType($partStructure['contentType'], $_contentType, $body);
627                 if ($bodyPart->type == Zend_Mime::TYPE_TEXT && $_contentType == Zend_Mime::TYPE_HTML) {
628                     $body = Felamimail_Message::replaceUris($body);
629                     $body = Felamimail_Message::replaceEmails($body);
630                 }
631             } else {
632                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
633                     . ' Do not convert ' . $bodyPart->type . ' part to ' . $_contentType);
634             }
635             
636             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
637                 . ' Adding part ' . $partId . ' to message body.');
638             
639             $messageBody .= Tinebase_Core::filterInputForDatabase($body);
640         }
641         
642         return $messageBody;
643     }
644     
645     /**
646      * use html purifier to remove 'bad' tags/attributes from html body
647      *
648      * @param string $_content
649      * @param string $messageId
650      * @return string
651      */
652     protected function _purifyBodyContent($_content, $messageId)
653     {
654         if (!defined('HTMLPURIFIER_PREFIX')) {
655             define('HTMLPURIFIER_PREFIX', realpath(dirname(__FILE__) . '/../../library/HTMLPurifier'));
656         }
657         
658         $config = Tinebase_Core::getConfig();
659         $path = ($config->caching && $config->caching->active && $config->caching->path) 
660             ? $config->caching->path : Tinebase_Core::getTempDir();
661
662         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
663             . ' Purifying html body. (cache path: ' . $path .')');
664         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
665             . ' Current mem usage before purify: ' . memory_get_usage()/1024/1024);
666         
667         // add custom schema for passing message id to URIScheme
668         $configSchema = HTMLPurifier_ConfigSchema::makeFromSerial();
669         $configSchema->add('Felamimail.messageId', NULL, 'string', TRUE);
670         $config = HTMLPurifier_Config::create(NULL, $configSchema);
671         $config->set('HTML.DefinitionID', 'purify message body contents');
672         $config->set('HTML.DefinitionRev', 1);
673         
674         // @see: http://htmlpurifier.org/live/configdoc/plain.html#Attr.EnableID
675         $config->set('Attr.EnableID', TRUE);
676         $config->set('Attr.IDPrefix', 'felamimail_inline_');
677         
678         // @see: http://htmlpurifier.org/live/configdoc/plain.html#HTML.TidyLevel
679         $config->set('HTML.TidyLevel', 'heavy');
680         
681         // some config values to consider
682         /*
683         $config->set('Attr.EnableID', true);
684         $config->set('Attr.ClassUseCDATA', true);
685         $config->set('CSS.AllowTricky', true);
686         */
687         $config->set('Cache.SerializerPath', $path);
688         $config->set('URI.AllowedSchemes', array(
689             'http' => true,
690             'https' => true,
691             'mailto' => true,
692             'data' => true,
693             'cid' => true
694         ));
695         $config->set('Felamimail.messageId', $messageId);
696         
697         $this->_transformBodyTags($config);
698         
699         // add uri filter
700         $uri = $config->getDefinition('URI');
701         $uri->addFilter(new Felamimail_HTMLPurifier_URIFilter_TransformURI(), $config);
702         
703         // add cid uri scheme
704         require_once(dirname(dirname(__FILE__)) . '/HTMLPurifier/URIScheme/cid.php');
705         
706         $purifier = new HTMLPurifier($config);
707         $content = $purifier->purify($_content);
708
709         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
710             . ' Current mem usage after purify: ' . memory_get_usage()/1024/1024);
711
712         return $content;
713     }
714     
715     /**
716      * transform some tags / attributes
717      * 
718      * @param HTMLPurifier_Config $config
719      */
720     protected function _transformBodyTags(HTMLPurifier_Config $config)
721     {
722         if ($def = $config->maybeGetRawHTMLDefinition()) {
723             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
724                 . ' Add target="_blank" to anchors');
725             $a = $def->addBlankElement('a');
726             $a->attr_transform_post[] = new Felamimail_HTMLPurifier_AttrTransform_AValidator();
727             
728             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
729                 . ' Add class="felamimail-body-blockquote" to blockquote tags that do not already have the class');
730             $bq = $def->addBlankElement('blockquote');
731             $bq->attr_transform_post[] = new Felamimail_HTMLPurifier_AttrTransform_BlockquoteValidator();
732         } else {
733              if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
734                 . ' Could not get HTMLDefinition, no transformation possible');
735         }
736     }
737     
738     /**
739      * get message headers
740      * 
741      * @param string|Felamimail_Model_Message $_messageId
742      * @param boolean $_readOnly
743      * @return array
744      * @throws Felamimail_Exception_IMAPMessageNotFound
745      */
746     public function getMessageHeaders($_messageId, $_partId = null, $_readOnly = false)
747     {
748         if (! $_messageId instanceof Felamimail_Model_Message) {
749             $message = $this->_backend->get($_messageId);
750         } else {
751             $message = $_messageId;
752         }
753         
754         $cache = Tinebase_Core::get('cache');
755         $cacheId = 'getMessageHeaders' . $message->getId() . str_replace('.', '', $_partId);
756         if ($cache->test($cacheId)) {
757             return $cache->load($cacheId);
758         }
759         
760         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
761             . ' Fetching headers for message uid ' .  $message->messageuid . ' (part:' . $_partId . ')');
762         
763         try {
764             $imapBackend = $this->_getBackendAndSelectFolder($message->folder_id);
765         } catch (Zend_Mail_Storage_Exception $zmse) {
766             if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' ' . $zmse->getMessage());
767             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' ' . $zmse->getTraceAsString());
768             throw new Felamimail_Exception_IMAPMessageNotFound('Folder not found');
769         }
770         
771         if ($imapBackend === null) {
772             throw new Felamimail_Exception('Failed to get imap backend');
773         }
774         
775         $section = ($_partId === null) ?  'HEADER' : $_partId . '.HEADER';
776         
777         try {
778             $rawHeaders = $imapBackend->getRawContent($message->messageuid, $section, $_readOnly);
779         } catch (Felamimail_Exception_IMAPMessageNotFound $feimnf) {
780             $this->_backend->delete($message->getId());
781             throw $feimnf;
782         }
783         
784         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ 
785             . ' Fetched Headers: ' . $rawHeaders);
786
787         $headers = array();
788         $body = null;
789         Zend_Mime_Decode::splitMessage($rawHeaders, $headers, $body);
790         
791         $cache->save($headers, $cacheId, array('getMessageHeaders'), 86400);
792         
793         return $headers;
794     }
795     
796     /**
797      * get imap backend and folder (and select folder)
798      *
799      * @param string                    $_folderId
800      * @param Felamimail_Backend_Folder &$_folder
801      * @param boolean                   $_select
802      * @param Felamimail_Backend_ImapProxy   $_imapBackend
803      * @throws Felamimail_Exception_IMAPServiceUnavailable
804      * @throws Felamimail_Exception_IMAPFolderNotFound
805      * @return Felamimail_Backend_ImapProxy
806      */
807     protected function _getBackendAndSelectFolder($_folderId = NULL, &$_folder = NULL, $_select = TRUE, Felamimail_Backend_ImapProxy $_imapBackend = NULL)
808     {
809         if ($_folder === NULL || empty($_folder)) {
810             $folderBackend  = new Felamimail_Backend_Folder();
811             $_folder = $folderBackend->get($_folderId);
812         }
813         
814         try {
815             $imapBackend = ($_imapBackend === NULL) ? Felamimail_Backend_ImapFactory::factory($_folder->account_id) : $_imapBackend;
816             if ($_select) {
817                 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ 
818                     . ' Select folder ' . $_folder->globalname);
819                 $imapBackend->selectFolder(Felamimail_Model_Folder::encodeFolderName($_folder->globalname));
820             }
821         } catch (Zend_Mail_Storage_Exception $zmse) {
822             // @todo remove the folder from cache if it could not be found on the IMAP server?
823             throw new Felamimail_Exception_IMAPFolderNotFound($zmse->getMessage());
824         } catch (Zend_Mail_Protocol_Exception $zmpe) {
825             throw new Felamimail_Exception_IMAPServiceUnavailable($zmpe->getMessage());
826         }
827         
828         return $imapBackend;
829     }
830     
831     /**
832      * get attachments of message
833      *
834      * @param  array  $_structure
835      * @return array
836      */
837     public function getAttachments($_messageId, $_partId = null)
838     {
839         if (! $_messageId instanceof Felamimail_Model_Message) {
840             $message = $this->_backend->get($_messageId);
841         } else {
842             $message = $_messageId;
843         }
844         
845         $structure = $message->getPartStructure($_partId);
846         
847         $attachments = array();
848         
849         if (! isset($structure['parts'])) {
850             return $attachments;
851         }
852         
853         foreach ($structure['parts'] as $part) {
854             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
855                 . ' ' . print_r($part, TRUE));
856             
857             if ($part['type'] == 'multipart') {
858                 $attachments = $attachments + $this->getAttachments($message, $part['partId']);
859             } else {
860                 $filename = $this->_getAttachmentFilename($part);
861                 
862                 if ($part['type'] == 'text' && 
863                     (! is_array($part['disposition']) || ($part['disposition']['type'] == Zend_Mime::DISPOSITION_INLINE && ! (isset($part['disposition']["parameters"]) || array_key_exists("parameters", $part['disposition']))))
864                 ) {
865                     if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
866                         . ' Skipping DISPOSITION_INLINE attachment with name ' . $filename);
867                     if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
868                         . ' part: ' . print_r($part, TRUE));
869                     continue;
870                 }
871                 
872                 $expanded = array();
873                 
874                 // if a winmail.dat exists, try to expand it
875                 if (preg_match('/^winmail[.]*\.dat/i', $filename) && Tinebase_Core::systemCommandExists('tnef')) {
876
877                     if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
878                         . ' Got winmail.dat attachment (contentType=' . $part['contentType'] . '). Trying to extract files ...');
879
880                     if ($part['contentType'] == 'application/ms-tnef' || $part['contentType'] == 'text/plain') {
881                         $expanded = $this->_expandWinMailDat($_messageId, $part['partId']);
882
883                         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
884                             . ' Extracted ' . count($expanded) . ' files from ' . $filename);
885
886                         if (!empty($expanded)) {
887                             $attachments = array_merge($attachments, $expanded);
888                         }
889                     }
890                 }
891                 
892                 // if its not a winmail.dat, or the winmail.dat couldn't be expanded 
893                 // properly because it has richtext embedded, return attachment as it is
894                 if (empty($expanded)) {
895                 
896                     $attachmentData = array(
897                         'content-type' => $part['contentType'], 
898                         'filename'     => $filename,
899                         'partId'       => $part['partId'],
900                         'size'         => $part['size'],
901                         'description'  => $part['description'],
902                         'cid'          => (! empty($part['id'])) ? $part['id'] : NULL,
903                     );
904                     
905                     if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
906                         . ' Got attachment with name ' . $filename);
907                     
908                     $attachments[] = $attachmentData;
909                 }
910             }
911         }
912         
913         return $attachments;
914     }
915     
916     /**
917      * extracts contents from the ugly .dat format
918      * 
919      * @param string $messageId
920      * @param string $partId
921      */
922     protected function _expandWinMailDat($messageId, $partId)
923     {
924         $files = $this->extractWinMailDat($messageId['id'], $partId);
925         $path = Tinebase_Core::getTempDir() . '/winmail/' . $messageId['id'] . '/';
926         
927         $attachmentData = array();
928         
929         $i = 0;
930         
931         foreach($files as $filename) {
932             $attachmentData[] = array(
933                 'content-type' => mime_content_type($path . $filename),
934                 'filename'     => $filename,
935                 'partId'       => 'winmail-' . $i,
936                 'size'         => filesize($path . $filename),
937                 'description'  => 'Extracted Content',
938                 'cid'          => 'winmail-' . Tinebase_Record_Abstract::generateUID(10),
939             );
940             
941             $i++;
942         }
943         
944         return $attachmentData;
945     }
946     
947     /**
948      * @param string $partId
949      * @param string $messageId
950      * 
951      * @return array
952      */
953     public function extractWinMailDat($messageId, $partId = NULL)
954     {
955         $path = Tinebase_Core::getTempDir() . '/winmail/';
956         
957         // create base path
958         if (! is_dir($path)) {
959             mkdir($path);
960         }
961         
962         // create path for this message id
963         if (! is_dir($path . $messageId)) {
964             mkdir($path . $messageId);
965             
966             $part = $this->getMessagePart($messageId, $partId);
967             
968             $path = $path . $messageId . '/';
969             $datFile =  $path . 'winmail.dat';
970             
971             $stream = $part->getDecodedStream();
972             $tmpFile = fopen($datFile, 'w');
973             stream_copy_to_stream($stream, $tmpFile);
974             fclose($tmpFile);
975             
976             // find out filenames
977             $files = array();
978             $fileString = explode(chr(10), Tinebase_Core::callSystemCommand('tnef -t ' . $datFile));
979             
980             foreach($fileString as $line) {
981                 $split = explode('|', $line);
982                 $clean = trim($split[0]);
983                 if (! empty($clean)) {
984                     $files[] = $clean;
985                 }
986             }
987             
988             // extract files
989             Tinebase_Core::callSystemCommand('tnef -C ' . $path . ' ' . $datFile);
990             
991         } else { // temp files still existing
992             $dir = new DirectoryIterator($path . $messageId);
993             $files = array();
994             
995             foreach($dir as $file) {
996                 if ($file->isFile() && $file->getFilename() != 'winmail.dat') {
997                     $files[] = $file->getFilename();
998                 }
999             }
1000         }
1001         
1002         asort($files);
1003         return $files;
1004     }
1005     
1006     
1007     /**
1008      * fetch attachment filename from part
1009      * 
1010      * @param array $part
1011      * @return string
1012      */
1013     protected function _getAttachmentFilename($part)
1014     {
1015         if (is_array($part['disposition']) && (isset($part['disposition']['parameters']) || array_key_exists('parameters', $part['disposition'])) 
1016             && (isset($part['disposition']['parameters']['filename']) || array_key_exists('filename', $part['disposition']['parameters']))) 
1017         {
1018             $filename = $part['disposition']['parameters']['filename'];
1019         } elseif (is_array($part['parameters']) && (isset($part['parameters']['name']) || array_key_exists('name', $part['parameters']))) {
1020             $filename = $part['parameters']['name'];
1021         } else {
1022             $filename = 'Part ' . $part['partId'];
1023             if (isset($part['contentType'])) {
1024                 $filename .= ' (' . $part['contentType'] . ')';
1025             }
1026         }
1027         
1028         return $filename;
1029     }
1030     
1031     /**
1032      * delete messages from cache by folder
1033      * 
1034      * @param $_folder
1035      */
1036     public function deleteByFolder(Felamimail_Model_Folder $_folder)
1037     {
1038         $this->_backend->deleteByFolderId($_folder);
1039     }
1040
1041     /**
1042      * update folder counts and returns list of affected folders
1043      * 
1044      * @param array $_folderCounter (folderId => unreadcounter)
1045      * @return Tinebase_Record_RecordSet of affected folders
1046      * @throws Felamimail_Exception
1047      */
1048     protected function _updateFolderCounts($_folderCounter)
1049     {
1050         foreach ($_folderCounter as $folderId => $counter) {
1051             $folder = Felamimail_Controller_Folder::getInstance()->get($folderId);
1052             
1053             // get error condition and update array by checking $counter keys
1054             if ((isset($counter['incrementUnreadCounter']) || array_key_exists('incrementUnreadCounter', $counter))) {
1055                 // this is only used in clearFlags() atm
1056                 $errorCondition = ($folder->cache_unreadcount + $counter['incrementUnreadCounter'] > $folder->cache_totalcount);
1057                 $updatedCounters = array(
1058                     'cache_unreadcount' => '+' . $counter['incrementUnreadCounter'],
1059                 );
1060             } else if ((isset($counter['decrementMessagesCounter']) || array_key_exists('decrementMessagesCounter', $counter)) && (isset($counter['decrementUnreadCounter']) || array_key_exists('decrementUnreadCounter', $counter))) {
1061                 $errorCondition = ($folder->cache_unreadcount < $counter['decrementUnreadCounter'] || $folder->cache_totalcount < $counter['decrementMessagesCounter']);
1062                 $updatedCounters = array(
1063                     'cache_totalcount'  => '-' . $counter['decrementMessagesCounter'],
1064                     'cache_unreadcount' => '-' . $counter['decrementUnreadCounter']
1065                 );
1066             } else {
1067                 throw new Felamimail_Exception('Wrong folder counter given: ' . print_r($_folderCounter, TRUE));
1068             }
1069             
1070             if ($errorCondition) {
1071                 // something went wrong => recalculate counter
1072                 if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . 
1073                     ' folder counters dont match => refresh counters'
1074                 );
1075                 $updatedCounters = Felamimail_Controller_Cache_Folder::getInstance()->getCacheFolderCounter($folder);
1076             }
1077             
1078             Felamimail_Controller_Folder::getInstance()->updateFolderCounter($folder, $updatedCounters);
1079         }
1080         
1081         return Felamimail_Controller_Folder::getInstance()->getMultiple(array_keys($_folderCounter));
1082     }
1083     
1084     /**
1085      * get punycode converter
1086      * 
1087      * @return NULL|idna_convert
1088      */
1089     public function getPunycodeConverter()
1090     {
1091         if ($this->_punycodeConverter === NULL) {
1092             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1093                 . ' Creating idna convert class for punycode conversion.');
1094             $this->_punycodeConverter = new idna_convert();
1095         }
1096         
1097         return $this->_punycodeConverter;
1098     }
1099
1100     /**
1101      * get resource part id
1102      * 
1103      * @param string $cid
1104      * @param string $messageId
1105      * @return array
1106      * @throws Tinebase_Exception_NotFound
1107      * 
1108      * @todo add param string $folderId?
1109      */
1110     public function getResourcePartStructure($cid, $messageId)
1111     {
1112         $message = $this->get($messageId);
1113         $this->_checkMessageAccount($message);
1114         
1115         $attachments = $this->getAttachments($messageId);
1116         
1117         foreach ($attachments as $attachment) {
1118             if ($attachment['cid'] === '<' . $cid . '>') {
1119                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1120                     . ' Found attachment ' . $attachment['partId'] . ' with cid ' . $cid);
1121                 return $attachment;
1122             }
1123         }
1124         
1125         throw new Tinebase_Exception_NotFound('Resource not found');
1126     }
1127 }