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