Merge branch '2015.11' into 2015.11-develop
[tine20] / tine20 / Felamimail / Model / Message.php
1 <?php
2 /**
3  * class to hold message cache data
4  * 
5  * @package     Felamimail
6  * @subpackage    Model
7  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
8  * @author      Lars Kneschke <l.kneschke@metaways.de>
9  * @copyright   Copyright (c) 2009-2012 Metaways Infosystems GmbH (http://www.metaways.de)
10  */
11
12 /**
13  * class to hold message cache data
14  * 
15  * @package     Felamimail
16  * @subpackage  Model
17  * @property    string  $folder_id      the folder id
18  * @property    string  $subject        the subject of the email
19  * @property    string  $from_email     the address of the sender (from)
20  * @property    string  $from_name      the name of the sender (from)
21  * @property    string  $sender         the sender of the email
22  * @property    string  $content_type   the content type of the message
23  * @property    string  $body_content_type   the content type of the message body
24  * @property    array   $to             the to receipients
25  * @property    array   $cc             the cc receipients
26  * @property    array   $bcc            the bcc receipients
27  * @property    array   $structure      the message structure
28  * @property    array   $attachments    the attachments
29  * @property    string  $messageuid     the message uid on the imap server
30  * @property    array   $preparedParts  prepared parts
31  * @property    integer $reading_conf   true if it must send a reading confirmation
32  */
33 class Felamimail_Model_Message extends Tinebase_Record_Abstract
34 {
35     /**
36      * message content type (rfc822)
37      *
38      */
39     const CONTENT_TYPE_MESSAGE_RFC822 = 'message/rfc822';
40
41     /**
42      * content type html
43      *
44      */
45     const CONTENT_TYPE_HTML = 'text/html';
46
47     /**
48      * content type plain text
49      *
50      */
51     const CONTENT_TYPE_PLAIN = 'text/plain';
52
53     /**
54      * content type multipart/alternative
55      */
56     const CONTENT_TYPE_MULTIPART = 'multipart/alternative';
57     
58     /**
59      * content type multipart/related
60      */
61     const CONTENT_TYPE_MULTIPART_RELATED = 'multipart/related';
62     
63     /**
64      * content type multipart/mixed
65      */
66     const CONTENT_TYPE_MULTIPART_MIXED = 'multipart/mixed';
67     
68     /**
69      * content type text/calendar
70      */
71     const CONTENT_TYPE_CALENDAR = 'text/calendar';
72     
73     /**
74      * content type text/vcard
75      */
76     const CONTENT_TYPE_VCARD = 'text/vcard';
77     
78     /**
79      * attachment filename regexp 
80      *
81      */
82     const ATTACHMENT_FILENAME_REGEXP = "/name=\"(.*)\"/";
83     
84     /**
85      * quote string ("> ")
86      * 
87      * @var string
88      */
89     const QUOTE = '&gt; ';
90     
91     /**
92      * key in $_validators/$_properties array for the field which 
93      * represents the identifier
94      * 
95      * @var string
96      */
97     protected $_identifier = 'id';
98     
99     /**
100      * application the record belongs to
101      *
102      * @var string
103      */
104     protected $_application = 'Felamimail';
105
106     /**
107      * list of zend validator
108      * 
109      * this validators get used when validating user generated content with Zend_Input_Filter
110      *
111      * @var array
112      */
113     protected $_validators = array(
114         'id'                    => array(Zend_Filter_Input::ALLOW_EMPTY => true),
115         'account_id'            => array(Zend_Filter_Input::ALLOW_EMPTY => true),
116         'original_id'           => array(Zend_Filter_Input::ALLOW_EMPTY => true),
117         'original_part_id'      => array(Zend_Filter_Input::ALLOW_EMPTY => true),
118         'messageuid'            => array(Zend_Filter_Input::ALLOW_EMPTY => false), 
119         'folder_id'             => array(Zend_Filter_Input::ALLOW_EMPTY => true), 
120         'subject'               => array(Zend_Filter_Input::ALLOW_EMPTY => true), 
121         'from_email'            => array(Zend_Filter_Input::ALLOW_EMPTY => true), 
122         'from_name'             => array(Zend_Filter_Input::ALLOW_EMPTY => true), 
123         'sender'                => array(Zend_Filter_Input::ALLOW_EMPTY => true), 
124         'to'                    => array(Zend_Filter_Input::ALLOW_EMPTY => true), 
125         'cc'                    => array(Zend_Filter_Input::ALLOW_EMPTY => true), 
126         'bcc'                   => array(Zend_Filter_Input::ALLOW_EMPTY => true),
127         'received'              => array(Zend_Filter_Input::ALLOW_EMPTY => true), 
128         'sent'                  => array(Zend_Filter_Input::ALLOW_EMPTY => true), 
129         'size'                  => array(Zend_Filter_Input::ALLOW_EMPTY => true), 
130         'flags'                 => array(Zend_Filter_Input::ALLOW_EMPTY => true),
131         'timestamp'             => array(Zend_Filter_Input::ALLOW_EMPTY => true),
132         'body'                  => array(Zend_Filter_Input::ALLOW_EMPTY => true),
133         // this is: refactor body and content type handling / or names
134         'body_content_type_of_body_property_of_this_record' => array(
135             Zend_Filter_Input::ALLOW_EMPTY => true,
136             Zend_Filter_Input::DEFAULT_VALUE => self::CONTENT_TYPE_PLAIN,
137             array('InArray', array(self::CONTENT_TYPE_HTML, self::CONTENT_TYPE_PLAIN)),
138         ),
139         'structure'             => array(Zend_Filter_Input::ALLOW_EMPTY => true),
140         'text_partid'           => array(Zend_Filter_Input::ALLOW_EMPTY => true),
141         'html_partid'           => array(Zend_Filter_Input::ALLOW_EMPTY => true),
142         'has_attachment'        => array(Zend_Filter_Input::ALLOW_EMPTY => true),
143         'headers'               => array(Zend_Filter_Input::ALLOW_EMPTY => true),
144         // this is: content_type_of_envelope
145         'content_type'          => array(Zend_Filter_Input::ALLOW_EMPTY => true),
146         // this is: body_content_type_from_message_structrue
147         'body_content_type'     => array(
148             Zend_Filter_Input::ALLOW_EMPTY => true,
149             Zend_Filter_Input::DEFAULT_VALUE => self::CONTENT_TYPE_PLAIN,
150             array('InArray', array(self::CONTENT_TYPE_HTML, self::CONTENT_TYPE_PLAIN)),
151         ),
152         'attachments'           => array(Zend_Filter_Input::ALLOW_EMPTY => true),
153     // save email as contact note
154         'note'                  => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => 0),
155     // Felamimail_Message object
156         'message'               => array(Zend_Filter_Input::ALLOW_EMPTY => true),
157     // prepared parts (iMIP invitations, contact vcards, ...)
158         'preparedParts'         => array(Zend_Filter_Input::ALLOW_EMPTY => true),
159         'reading_conf'          => array(Zend_Filter_Input::ALLOW_EMPTY => true,
160                                          Zend_Filter_Input::DEFAULT_VALUE => 0),
161     );
162     
163     /**
164      * name of fields containing datetime or or an array of datetime information
165      *
166      * @var array list of datetime fields
167      */
168     protected $_datetimeFields = array(
169         'timestamp',
170         'received',
171         'sent',
172     );
173     
174     /**
175      * gets record related properties
176      * 
177      * @param string _name of property
178      * @throws Tinebase_Exception_UnexpectedValue
179      * @return mixed value of property
180      */
181     public function __get($_name)
182     {
183         $result = parent::__get($_name);
184         
185         if ($_name === 'structure' && empty($result)) {
186             $result = $this->_fetchStructure();
187         }
188         
189         return $result;
190     }
191     
192     /**
193      * fetch structure from cache or imap server, parse it and store it into cache
194      * 
195      * @return array
196      */
197     protected function _fetchStructure()
198     {
199         $cacheId = $this->_getStructureCacheId();
200         $cache = Tinebase_Core::getCache();
201         if ($cache->test($cacheId)) {
202             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' Getting message structure from cache: ' . $cacheId);
203             $result = $cache->load($cacheId);
204         } else {
205             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' Getting message structure from IMAP server.');
206             
207             try {
208                 $summary = Felamimail_Controller_Cache_Message::getInstance()->getMessageSummary($this->messageuid, $this->account_id, $this->folder_id);
209                 $result = $summary['structure'];
210             } catch (Zend_Mail_Protocol_Exception $zmpe) {
211                 // imap server might have gone away
212                 Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ 
213                     . ' IMAP protocol error during summary fetching: ' . $zmpe->getMessage());
214                 $result = array();
215             }
216             $this->_setStructure($result);
217         }
218         
219         return $result;
220     }
221     
222     /**
223      * get cache id for structure
224      * 
225      * @return string
226      */
227     protected function _getStructureCacheId()
228     {
229         return 'messageStructure' . $this->folder_id . $this->messageuid;
230     }
231     
232     /**
233      * set structure and save into cache
234      * 
235      * @param array $structure
236      */
237     protected function _setStructure($structure)
238     {
239         if (! empty($structure['partId'])) {
240             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ 
241                 . ' Don\'t cache structure of subparts');
242             return;
243         }
244         
245         $cacheId = $this->_getStructureCacheId();
246         
247         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' Caching message structure: ' . $cacheId);
248         Tinebase_Core::getCache()->save($structure, $cacheId, array('messageStructure'), 86400); // 24 hours
249         
250         $this->structure = $structure;
251     }
252     
253     /**
254      * check if message has \SEEN flag
255      * 
256      * @return boolean
257      */
258     public function hasSeenFlag()
259     {
260         return (is_array($this->flags) && in_array(Zend_Mail_Storage::FLAG_SEEN, $this->flags));
261     }
262     
263     /**
264      * Send the reading confirmation in a message who has the correct header and is not seen yet
265      *
266      * @return void
267      */
268     public function sendReadingConfirmation()
269     {
270         if (! is_array($this->headers)) {
271             $this->headers = Felamimail_Controller_Message::getInstance()->getMessageHeaders($this->getId(), NULL, TRUE);
272         }
273         
274         if ((isset($this->headers['disposition-notification-to']) || array_key_exists('disposition-notification-to', $this->headers)) && $this->headers['disposition-notification-to']) {
275             $translate = Tinebase_Translation::getTranslation($this->_application);
276             $from = Felamimail_Controller_Account::getInstance()->get($this->account_id);
277             
278             $message = new Felamimail_Model_Message();
279             $message->account_id = $this->account_id;
280             
281             $punycodeConverter = Felamimail_Controller_Message::getInstance()->getPunycodeConverter();
282             $to = Felamimail_Message::convertAddresses($this->headers['disposition-notification-to'], $punycodeConverter);
283             if (empty($to)) {
284                 throw new Felamimail_Exception('disposition-notification-to header does not contain a valid email address');
285             }
286              
287             $message->content_type = Felamimail_Model_Message::CONTENT_TYPE_HTML;
288             $message->to           = $to[0]['email'];
289             $message->subject      = $translate->_('Reading Confirmation:') . ' '. $this->subject;
290             $message->body         = $translate->_('Your message:'). ' ' . $this->subject . "\n" .
291                                      $translate->_('Received on')  . ' ' . $this->received . "\n" .
292                                      $translate->_('Was read by:') . ' ' . $from->from .  ' <' . $from->email .'> ' .
293                                      $translate->_('on') . ' ' . (date('Y-m-d H:i:s'));
294             $message->body         = Tinebase_Mail::convertFromTextToHTML($message->body, 'felamimail-body-blockquote');
295             Felamimail_Controller_Message_Send::getInstance()->sendMessage($message);
296         }
297     }
298     
299     /**
300      * parse headers and set 'date', 'from', 'to', 'cc', 'bcc', 'subject', 'sender' fields
301      * 
302      * @param array $_headers
303      * @return void
304      */
305     public function parseHeaders(array $_headers)
306     {
307         // remove duplicate headers (which can't be set twice in real life)
308         foreach (array('date', 'from', 'subject', 'sender') as $field) {
309             if (isset($_headers[$field]) && is_array($_headers[$field])) {
310                 $_headers[$field] = $_headers[$field][0];
311             }
312         }
313         
314         // @see 0008644: error when sending mail with note (wrong charset)
315         $this->subject = (isset($_headers['subject'])) ? Tinebase_Core::filterInputForDatabase(Felamimail_Message::convertText($_headers['subject'])) : null;
316         
317         if ((isset($_headers['date']) || array_key_exists('date', $_headers))) {
318             $this->sent = Felamimail_Message::convertDate($_headers['date']);
319         } elseif ((isset($_headers['resent-date']) || array_key_exists('resent-date', $_headers))) {
320             $this->sent = Felamimail_Message::convertDate($_headers['resent-date']);
321         }
322         
323         $punycodeConverter = Felamimail_Controller_Message::getInstance()->getPunycodeConverter();
324         
325         foreach (array('to', 'cc', 'bcc', 'from', 'sender') as $field) {
326             if (isset($_headers[$field])) {
327                 if (is_array($_headers[$field])) {
328                     $value = array();
329                     foreach ($_headers[$field] as $headerValue) {
330                         $value = array_merge($value, Felamimail_Message::convertAddresses($headerValue, $punycodeConverter));
331                     }
332                     $this->$field = $value;
333                 } else {
334                     $value = Felamimail_Message::convertAddresses($_headers[$field], $punycodeConverter);
335                     switch ($field) {
336                         case 'from':
337                             $this->from_email = (isset($value[0]) && (isset($value[0]['email']) || array_key_exists('email', $value[0]))) ? $value[0]['email'] : '';
338                             $this->from_name = (isset($value[0]) && (isset($value[0]['name']) || array_key_exists('name', $value[0])) && ! empty($value[0]['name'])) ? $value[0]['name'] : $this->from_email;
339                             break;
340                             
341                         case 'sender':
342                             $this->sender = (isset($value[0]) && (isset($value[0]['email']) || array_key_exists('email', $value[0]))) ? '<' . $value[0]['email'] . '>' : '';
343                             if ((isset($value[0]) && (isset($value[0]['name']) || array_key_exists('name', $value[0])) && ! empty($value[0]['name']))) {
344                                 $this->sender = '"' . $value[0]['name'] . '" ' . $this->sender;
345                             }
346                             break;
347                             
348                         default:
349                             $this->$field = $value;
350                     }
351                 }
352             }
353         }
354     }
355     
356     /**
357      * parse message structure to get content types
358      * 
359      * @param array $_structure
360      * @return void
361      */
362     public function parseStructure($_structure = NULL)
363     {
364         if ($_structure !== NULL) {
365             $this->_setStructure($_structure);
366         }
367         
368         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
369             . ' Parsing structure: ' . print_r($this->structure, TRUE));
370         
371         $this->content_type  = isset($this->structure['contentType']) ? $this->structure['contentType'] : Zend_Mime::TYPE_TEXT;
372         $this->_setBodyContentType();
373     }
374     
375     /**
376      * parse parts to set body content type
377      */
378     protected function _setBodyContentType()
379     {
380         if ((isset($this->structure['parts']) || array_key_exists('parts', $this->structure))) {
381             $bodyContentTypes = $this->_getBodyContentTypes($this->structure['parts']);
382             $this->body_content_type = (in_array(self::CONTENT_TYPE_HTML, $bodyContentTypes)) ? self::CONTENT_TYPE_HTML : self::CONTENT_TYPE_PLAIN;
383         } else {
384             $this->body_content_type = (in_array($this->content_type, array(self::CONTENT_TYPE_HTML, self::CONTENT_TYPE_PLAIN))) ? $this->content_type : self::CONTENT_TYPE_PLAIN;
385         }
386         
387         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' Set body content type to ' . $this->body_content_type);
388     }
389     
390     /**
391      * get all content types of mail body
392      * 
393      * @param array $_parts
394      * @return array
395      */
396     protected function _getBodyContentTypes($_parts)
397     {
398         $_bodyContentTypes = array();
399         foreach ($_parts as $part) {
400             if (is_array($part) && (isset($part['contentType']) || array_key_exists('contentType', $part))) {
401                 if (in_array($part['contentType'], array(self::CONTENT_TYPE_HTML, self::CONTENT_TYPE_PLAIN)) && ! $this->_partIsAttachment($part)) {
402                     $_bodyContentTypes[] = $part['contentType'];
403                 } else if (($part['contentType'] == self::CONTENT_TYPE_MULTIPART || $part['contentType'] == self::CONTENT_TYPE_MULTIPART_RELATED) 
404                     && (isset($part['parts']) || array_key_exists('parts', $part)))
405                 {
406                     $_bodyContentTypes = array_merge($_bodyContentTypes, $this->_getBodyContentTypes($part['parts']));
407                 }
408             }
409         }
410         
411         return $_bodyContentTypes;
412     }
413     
414     /**
415      * get message part structure
416      * 
417      * @param  string  $_partId                 the part id to search for
418      * @param  boolean $_useMessageStructure    if you want to get only the messageStructure part
419      * @return array
420      */
421     public function getPartStructure($_partId, $_useMessageStructure = TRUE)
422     {
423         $result = NULL;
424         
425         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ 
426             . ' Getting structure for part ' . $_partId . ' / complete structure: ' . print_r($this->structure, TRUE));
427         
428         if ($_partId == null) {
429             // maybe we want no part at all => just return the whole structure
430             $result = $this->structure;
431         } else if ($this->structure['partId'] == $_partId) {
432             // maybe we want the first part => just return the whole structure
433             $result = $this->structure;
434         } else {
435             // iterate structure to find the correct part
436             $iterator = new RecursiveIteratorIterator(
437                 new RecursiveArrayIterator($this->structure),
438                 RecursiveIteratorIterator::SELF_FIRST
439             );
440             
441             foreach ($iterator as $key => $value) {
442                 if ($key == $_partId && is_array($value) && (isset($value['partId']) || array_key_exists('partId', $value))) {
443                     $result = ($_useMessageStructure && is_array($value) && (isset($value['messageStructure']) || array_key_exists('messageStructure', $value))) ? $value['messageStructure'] : $value;
444                 }
445             }
446         }
447         
448         if ($result === NULL) {
449             Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' ' . "Structure for partId $_partId not found!");
450         }
451         
452         return $result;
453     }
454     
455     /**
456      * get body parts
457      * 
458      * @param array $_structure
459      * @param string $_preferedMimeType
460      * @return array
461      */
462     public function getBodyParts($_structure = NULL, $_preferedMimeType = Zend_Mime::TYPE_HTML)
463     {
464         $bodyParts = array();
465         $structure = ($_structure !== NULL) ? $_structure : $this->structure;
466
467         if (! is_array($structure)) {
468             Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' Structure should be an array (' . $structure . ')');
469             return $bodyParts;
470         }
471         
472         if ((isset($structure['parts']) || array_key_exists('parts', $structure))) {
473             $bodyParts = $bodyParts + $this->_parseMultipart($structure, $_preferedMimeType);
474         } else {
475             $bodyParts = $bodyParts + $this->_parseSinglePart($structure, in_array($_preferedMimeType, array(Zend_Mime::TYPE_HTML, Zend_Mime::TYPE_TEXT)));
476         }
477         
478         return $bodyParts;
479     }
480     
481     /**
482      * parse single part message
483      * 
484      * @param array $_structure
485      * @return array
486      */
487     protected function _parseSinglePart(array $_structure, $_onlyGetNonAttachmentParts = TRUE)
488     {
489         $result = array();
490         
491         if (! (isset($_structure['type']) || array_key_exists('type', $_structure)) || $_structure['type'] != 'text') {
492             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' Structure has no type key or type != text: ' . print_r($_structure, TRUE));
493             return $result;
494         }
495         
496         if ($_onlyGetNonAttachmentParts && $this->_partIsAttachment($_structure)) {
497             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Part is attachment: ' . print_r($_structure['disposition'], TRUE));
498             return $result;
499         }
500
501         $partId = !empty($_structure['partId']) ? $_structure['partId'] : 1;
502         
503         $result[$partId] = $_structure;
504
505         return $result;
506     }
507     
508     /**
509      * checks if part is attachment
510      * 
511      * @param array $_structure
512      * @return boolean
513      */
514     protected function _partIsAttachment(array $_structure)
515     {
516         return (
517             isset($_structure['disposition']['type']) && 
518                 ($_structure['disposition']['type'] == Zend_Mime::DISPOSITION_ATTACHMENT ||
519                 // treat as attachment if structure contains parameters 
520                 ($_structure['disposition']['type'] == Zend_Mime::DISPOSITION_INLINE && (isset($_structure['disposition']["parameters"]) || array_key_exists("parameters", $_structure['disposition']))
521             )
522         ));
523     }
524     
525     /**
526      * parse multipart message
527      * 
528      * @param array $_structure
529      * @param string $_preferedMimeType
530      * @return array
531      */
532     protected function _parseMultipart(array $_structure, $_preferedMimeType = Zend_Mime::TYPE_HTML)
533     {
534         $result = array();
535         
536         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
537             . ' Structure: ' . print_r($_structure, TRUE));
538         
539         if ($_structure['subType'] == 'alternative' || $_structure['subType'] == 'related') {
540             foreach ($_structure['parts'] as $part) {
541                 $foundParts[$part['contentType']] = $part['partId'];
542             }
543             
544             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
545                 . ' Found parts: ' . print_r($foundParts, TRUE));
546             
547             if ((isset($foundParts[$_preferedMimeType]) || array_key_exists($_preferedMimeType, $foundParts))) {
548                 // found our desired body part
549                 $result[$foundParts[$_preferedMimeType]] = $_structure['parts'][$foundParts[$_preferedMimeType]];
550             }
551             
552             $multipartTypes = array(self::CONTENT_TYPE_MULTIPART, self::CONTENT_TYPE_MULTIPART_RELATED, self::CONTENT_TYPE_MULTIPART_MIXED);
553             foreach ($multipartTypes as $multipartType) {
554                 if ((isset($foundParts[$multipartType]) || array_key_exists($multipartType, $foundParts))) {
555                     // dig deeper into the structure to find the desired part(s)
556                     $partStructure = $_structure['parts'][$foundParts[$multipartType]];
557                     $result = $this->getBodyParts($partStructure, $_preferedMimeType);
558                 }
559             }
560             
561             $alternativeType = ($_preferedMimeType == Zend_Mime::TYPE_HTML) 
562                 ? Zend_Mime::TYPE_TEXT 
563                 : (($_preferedMimeType == Zend_Mime::TYPE_TEXT) ? Zend_Mime::TYPE_HTML : '');
564             if (empty($result) && (isset($foundParts[$alternativeType]) || array_key_exists($alternativeType, $foundParts))) {
565                 // found the alternative body part
566                 $result[$foundParts[$alternativeType]] = $_structure['parts'][$foundParts[$alternativeType]];
567             }
568         } else {
569             foreach ($_structure['parts'] as $part) {
570                 $result = $result + $this->getBodyParts($part, $_preferedMimeType);
571             }
572         }
573         
574         return $result;
575     }
576     
577     /**
578      * parse structure to get text_partid and html_partid
579      * 
580      * @deprecated should be replaced by getBodyParts
581      * @see 0007742: refactoring: replace parseBodyParts with getBodyParts
582      */
583     public function parseBodyParts()
584     {
585         $bodyParts = $this->_getBodyPartIds($this->structure);
586         if (isset($bodyParts['text'])) {
587             $this->text_partid = $bodyParts['text'];
588         }
589         if (isset($bodyParts['html'])) {
590             $this->html_partid = $bodyParts['html'];
591         }
592     }
593     
594     /**
595      * get body part ids
596      * 
597      * @param array $_structure
598      * @return array
599      */
600     protected function _getBodyPartIds(array $_structure)
601     {
602         $result = array();
603         
604         if ($_structure['type'] == 'text') {
605             $result = array_merge($result, $this->_getTextPartId($_structure));
606         } elseif($_structure['type'] == 'multipart') {
607             $result = array_merge($result, $this->_getMultipartIds($_structure));
608         }
609         
610         return $result;
611     }
612
613     /**
614      * get multipart ids
615      * 
616      * @param array $_structure
617      * @return array
618      */
619     protected function _getMultipartIds(array $_structure)
620     {
621         $result = array();
622         
623         if ($_structure['subType'] == 'alternative' || $_structure['subType'] == 'mixed' || 
624             $_structure['subType'] == 'signed' || $_structure['subType'] == 'related') {
625             foreach ($_structure['parts'] as $part) {
626                 $result = array_merge($result, $this->_getBodyPartIds($part));
627             }
628         } else {
629             // ignore other types for now
630             #var_dump($_structure);
631             #throw new Exception('unsupported multipart');
632         }
633         
634         return $result;
635     }
636     
637     /**
638      * get text part id
639      * 
640      * @param array $_structure
641      * @return array
642      */
643     protected function _getTextPartId(array $_structure)
644     {
645         $result = array();
646
647         if ($this->_partIsAttachment($_structure)) {
648             return $result;
649         }
650         
651         if ($_structure['subType'] == 'plain') {
652             $result['text'] = !empty($_structure['partId']) ? $_structure['partId'] : 1;
653         } elseif($_structure['subType'] == 'html') {
654             $result['html'] = !empty($_structure['partId']) ? $_structure['partId'] : 1;
655         }
656         
657         return $result;
658     }
659     
660     /**
661      * fills a record from json data
662      *
663      * @param array $recordData
664      * 
665      * @todo    get/detect delimiter from row? could be ';' or ','
666      * @todo    add recipient names
667      */
668     protected function _setFromJson(array &$recordData)
669     {
670         // explode email addresses if multiple
671         $recipientType = array('to', 'cc', 'bcc');
672         $delimiter = ';';
673         foreach ($recipientType as $field) {
674             if (!empty($recordData[$field])) {
675                 $recipients = array();
676                 if (! is_array($recordData[$field])) {
677                     throw new Tinebase_Exception_Record_Validation($field . ' property should be an array');
678                 }
679                 foreach ($recordData[$field] as $addresses) {
680                     if (substr_count($addresses, '@') > 1) {
681                         $recipients = array_merge($recipients, explode($delimiter, $addresses));
682                     } else {
683                         // single recipient
684                         $recipients[] = $addresses;
685                     }
686                 }
687                 
688                 foreach ($recipients as $key => &$recipient) {
689                     // extract email address if name and address given
690                     if (preg_match('/(.*)<(.*)>/', $recipient, $matches) > 0) {
691                         $recipient = $matches[2];
692                     }
693                     if (empty($recipient)) {
694                         unset($recipients[$key]);
695                     }
696                 }
697
698                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' ' . print_r($recipients, true));
699                 
700                 $recordData[$field] = array_unique($recipients);
701             }
702         }
703     }
704     
705     /**
706      * get body as plain text
707      * 
708      * @return string
709      */
710     public function getPlainTextBody()
711     {
712         $result = self::convertHTMLToPlainTextWithQuotes($this->body);
713         
714         return $result;
715     }
716     
717     /**
718      * convert html to plain text with replaced blockquotes, stripped tags and replaced <br>s
719      * -> use DOM extension
720      * 
721      * @param string $_html
722      * @param string $_eol
723      * @return string
724      */
725     public static function convertHTMLToPlainTextWithQuotes($_html, $_eol = "\n")
726     {
727         $html = $_html;
728         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' Original body string: ' . $_html);
729         
730         $dom = new DOMDocument('1.0', 'UTF-8');
731         
732         // body tag might be missing
733         if (strpos($html, '<body>') === FALSE) {
734             $html = '<body>' . $_html . '</body>';
735         }
736         // need to set meta tag to make sure that the encoding is done right (@see https://bugs.php.net/bug.php?id=32547)
737         if (strpos($html, '<html>') === FALSE) {
738             $html = '<head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/></head>' . $html;
739         }
740         // use a hack to make sure html is loaded as utf-8 (@see http://php.net/manual/en/domdocument.loadhtml.php#95251)
741         $dom->loadHTML('<?xml encoding="UTF-8">' . $html);
742         
743         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' HTML (DOMDocument): ' . $dom->saveHTML());
744         
745         $bodyElements = $dom->getElementsByTagName('body');
746         if ($bodyElements->length > 0) {
747             $firstBodyNode = $bodyElements->item(0);
748             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' Before quoting: ' . $firstBodyNode->nodeValue);
749             $result = self::addQuotesAndStripTags($firstBodyNode, 0, $_eol);
750             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' After quoting: ' . $result);
751             $result = html_entity_decode($result, ENT_COMPAT, 'UTF-8');
752             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' Entities decoded: ' . $result);
753         }
754         
755         return $result;
756     }
757     
758     /**
759      * convert blockquotes to quotes ("> ") and strip tags
760      * 
761      * this function uses tidy or DOM to recursivly walk the dom tree of the html mail
762      * @see http://php.net/manual/de/tidy.root.php
763      * @see http://php.net/manual/en/book.dom.php
764      * 
765      * @param tidyNode|DOMNode $_node
766      * @param integer $_quoteIndent
767      * @param string $_eol
768      * @return string
769      * 
770      * @todo we can transform more tags here, i.e. the <strong>BOLDTEXT</strong> tag could be replaced with *BOLDTEXT*
771      * @todo think about removing the tidy code
772      * @todo reduce complexity
773      */
774     public static function addQuotesAndStripTags($_node, $_quoteIndent = 0, $_eol = "\n")
775     {
776         $result = '';
777         
778         $hasChildren = ($_node instanceof DOMNode) ? $_node->hasChildNodes() : $_node->hasChildren();
779         $nameProperty = ($_node instanceof DOMNode) ? 'nodeName' : 'name';
780         $valueProperty = ($_node instanceof DOMNode) ? 'nodeValue' : 'value';
781
782         $divNewline = FALSE;
783         
784         if ($hasChildren) {
785             $lastChild = NULL;
786             $children = ($_node instanceof DOMNode) ? $_node->childNodes : $_node->child;
787             
788             if ($_node->{$nameProperty} == 'div') {
789                 $divNewline = TRUE;
790             }
791             
792             foreach ($children as $child) {
793                 
794                 $isTextLeaf = ($child instanceof DOMNode) ? $child->{$nameProperty} == '#text' : ! $child->{$nameProperty};
795                 if ($isTextLeaf) {
796                     // leaf -> add quotes and append to content string
797                     if ($_quoteIndent > 0) {
798                         $result .= str_repeat(self::QUOTE, $_quoteIndent) . $child->{$valueProperty};
799                     } else {
800                         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . 
801                             "value: " . $child->{$valueProperty} . " / name: " . $_node->{$nameProperty} . "\n");
802                         if ($divNewline) {
803                             $result .=  $_eol . str_repeat(self::QUOTE, $_quoteIndent);
804                             $divNewline = FALSE;
805                         }
806                         $result .= $child->{$valueProperty};
807                     }
808                     
809                 } else if ($child->{$nameProperty} == 'blockquote') {
810                     //  opening blockquote
811                     $_quoteIndent++;
812                     
813                 } else if ($child->{$nameProperty} == 'br') {
814                     if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' .
815                         "value: " . $child->{$valueProperty} . " / name: " . $_node->{$nameProperty} . "\n");
816                     // reset quoted state on newline
817                     if ($lastChild !== NULL && $lastChild->{$nameProperty} == 'br') {
818                         // add quotes to repeating newlines
819                         $result .= str_repeat(self::QUOTE, $_quoteIndent);
820                     }
821                     $result .= $_eol;
822                     $divNewline = FALSE;
823                 }
824                 
825                 $result .= self::addQuotesAndStripTags($child, $_quoteIndent, $_eol);
826                 
827                 if ($child->{$nameProperty} == 'blockquote') {
828                     // closing blockquote
829                     $_quoteIndent--;
830                     // add newline after last closing blockquote
831                     if ($_quoteIndent == 0) {
832                         $result .= $_eol;
833                     }
834                 }
835                 
836                 $lastChild = $child;
837             }
838             
839             // add newline if closing div
840             if ($divNewline) {
841                 $result .=  $_eol . str_repeat(self::QUOTE, $_quoteIndent);
842             }
843         }
844         
845         return $result;
846     }
847 }