alecpl
2010-06-19 d7a5dfa26abe21aa9216fe862225baa2b5caca3e
commit | author | age
8fa58e 1 <?php
T 2
3 /*
4  +-----------------------------------------------------------------------+
5  | program/include/rcube_message.php                                     |
6  |                                                                       |
7  | This file is part of the RoundCube Webmail client                     |
64e3e8 8  | Copyright (C) 2008-2010, RoundCube Dev. - Switzerland                 |
8fa58e 9  | Licensed under the GNU GPL                                            |
T 10  |                                                                       |
11  | PURPOSE:                                                              |
12  |   Logical representation of a mail message with all its data          |
13  |   and related functions                                               |
14  +-----------------------------------------------------------------------+
15  | Author: Thomas Bruederli <roundcube@gmail.com>                        |
16  +-----------------------------------------------------------------------+
17
638fb8 18  $Id$
8fa58e 19
T 20 */
21
22
23 /**
45f56c 24  * Logical representation of a mail message with all its data
T 25  * and related functions
8fa58e 26  *
T 27  * @package    Mail
28  * @author     Thomas Bruederli <roundcube@gmail.com>
29  */
30 class rcube_message
31 {
d311d8 32     private $app;
A 33     private $imap;
34     private $opt = array();
35     private $inline_parts = array();
36     private $parse_alternative = false;
8fa58e 37   
d311d8 38     public $uid = null;
A 39     public $headers;
40     public $structure;
41     public $parts = array();
42     public $mime_parts = array();
43     public $attachments = array();
44     public $subject = '';
45     public $sender = null;
46     public $is_safe = false;
8fa58e 47   
193fb4 48
d311d8 49     /**
A 50      * __construct
51      *
52      * Provide a uid, and parse message structure.
53      *
54      * @param string $uid The message UID.
55      *
56      * @uses rcmail::get_instance()
57      * @uses rcube_imap::decode_mime_string()
58      * @uses self::set_safe()
59      *
60      * @see self::$app, self::$imap, self::$opt, self::$structure
61      */
62     function __construct($uid)
8fa58e 63     {
d311d8 64         $this->app = rcmail::get_instance();
A 65         $this->imap = $this->app->imap;
64e3e8 66
d311d8 67         $this->uid = $uid;
A 68         $this->headers = $this->imap->get_headers($uid, NULL, true, true);
64e3e8 69
A 70         if (!$this->headers)
71             return;
8fa58e 72
d311d8 73         $this->subject = rcube_imap::decode_mime_string(
A 74             $this->headers->subject, $this->headers->charset);
75         list(, $this->sender) = each($this->imap->decode_address_list($this->headers->from));
64e3e8 76
d311d8 77         $this->set_safe((intval($_GET['_safe']) || $_SESSION['safe_messages'][$uid]));
A 78         $this->opt = array(
79             'safe' => $this->is_safe,
80             'prefer_html' => $this->app->config->get('prefer_html'),
81             'get_url' => rcmail_url('get', array(
82                 '_mbox' => $this->imap->get_mailbox_name(), '_uid' => $uid))
83         );
8fa58e 84
d311d8 85         if ($this->structure = $this->imap->get_structure($uid, $this->headers->body_structure)) {
A 86             $this->get_mime_numbers($this->structure);
87             $this->parse_structure($this->structure);
aa16b4 88         }
d311d8 89         else {
A 90             $this->body = $this->imap->get_body($uid);
91         }
92
93         // notify plugins and let them analyze this structured message object
94         $this->app->plugins->exec_hook('message_load', array('object' => $this));
95     }
96   
97   
98     /**
99      * Return a (decoded) message header
100      *
101      * @param string Header name
102      * @param bool   Don't mime-decode the value
103      * @return string Header value
104      */
105     public function get_header($name, $raw = false)
106     {
107         $value = $this->headers->$name;
108         return $raw ? $value : $this->imap->decode_header($value);
109     }
110
111   
112     /**
113      * Set is_safe var and session data
114      *
115      * @param bool enable/disable
116      */
117     public function set_safe($safe = true)
118     {
119         $this->is_safe = $safe;
120         $_SESSION['safe_messages'][$this->uid] = $this->is_safe;
121     }
122
123
124     /**
125      * Compose a valid URL for getting a message part
126      *
127      * @param string Part MIME-ID
128      * @return string URL or false if part does not exist
129      */
130     public function get_part_url($mime_id)
131     {
132         if ($this->mime_parts[$mime_id])
133             return $this->opt['get_url'] . '&_part=' . $mime_id;
aa16b4 134         else
d311d8 135             return false;
8fa58e 136     }
T 137
d311d8 138
A 139     /**
140      * Get content of a specific part of this message
141      *
142      * @param string Part MIME-ID
143      * @param resource File pointer to save the message part
144      * @return string Part content
145      */
146     public function get_part_content($mime_id, $fp=NULL)
147     {
148         if ($part = $this->mime_parts[$mime_id]) {
149             // stored in message structure (winmail/inline-uuencode)
150             if ($part->encoding == 'stream') {
151                 if ($fp) {
152                     fwrite($fp, $part->body);
153                 }
154                 return $fp ? true : $part->body;
155             }
156             // get from IMAP
157             return $this->imap->get_message_part($this->uid, $mime_id, $part, NULL, $fp);
158         } else
159             return null;
8fa58e 160     }
T 161
162
d311d8 163     /**
A 164      * Determine if the message contains a HTML part
165      *
166      * @return bool True if a HTML is available, False if not
167      */
168     function has_html_part()
169     {
170         // check all message parts
171         foreach ($this->parts as $pid => $part) {
172             $mimetype = strtolower($part->ctype_primary . '/' . $part->ctype_secondary);
173             if ($mimetype == 'text/html')
174                 return true;
175         }
176
177         return false;
178     }
179
180
181     /**
182      * Return the first HTML part of this message
183      *
184      * @return string HTML message part content
185      */
186     function first_html_part()
187     {
188         // check all message parts
189         foreach ($this->mime_parts as $mime_id => $part) {
190             $mimetype = strtolower($part->ctype_primary . '/' . $part->ctype_secondary);
191             if ($mimetype == 'text/html') {
192                 return $this->imap->get_message_part($this->uid, $mime_id, $part);
193             }
194         }
195     }
196
197
198     /**
199      * Return the first text part of this message
200      *
201      * @return string Plain text message/part content
202      */
203     function first_text_part()
204     {
205         // no message structure, return complete body
206         if (empty($this->parts))
207             return $this->body;
208
209         $out = null;
210
211         // check all message parts
212         foreach ($this->mime_parts as $mime_id => $part) {
213             $mimetype = $part->ctype_primary . '/' . $part->ctype_secondary;
214
215             if ($mimetype == 'text/plain') {
216                 $out = $this->imap->get_message_part($this->uid, $mime_id, $part);
217         
218                 // re-format format=flowed content
219                 if ($part->ctype_secondary == 'plain' && $part->ctype_parameters['format'] == 'flowed')
220                     $out = self::unfold_flowed($out);
221                 break;
222             }
223             else if ($mimetype == 'text/html') {
224                 $html_part = $this->imap->get_message_part($this->uid, $mime_id, $part);
225
226                 // remove special chars encoding
227                 $trans = array_flip(get_html_translation_table(HTML_ENTITIES));
228                 $html_part = strtr($html_part, $trans);
229
230                 // create instance of html2text class
231                 $txt = new html2text($html_part);
232                 $out = $txt->get_text();
233                 break;
234             }
235         }
236
237         return $out;
238     }
239
240
241     /**
242      * Raad the message structure returend by the IMAP server
243      * and build flat lists of content parts and attachments
244      *
245      * @param object rcube_message_part Message structure node
246      * @param bool  True when called recursively
247      */
248     private function parse_structure($structure, $recursive = false)
249     {
250         $message_ctype_primary = $structure->ctype_primary;
251         $message_ctype_secondary = $structure->ctype_secondary;
252         $mimetype = $structure->mimetype;
253
254         // real content-type of message/rfc822 part
255         if ($mimetype == 'message/rfc822') {
256             if ($structure->real_mimetype) {
257                 $mimetype = $structure->real_mimetype;
258                 list($message_ctype_primary, $message_ctype_secondary) = explode('/', $mimetype);
259             }
260         }
261
262         // show message headers
263         if ($recursive && is_array($structure->headers) && isset($structure->headers['subject'])) {
264             $c = new stdClass;
265             $c->type = 'headers';
266             $c->headers = &$structure->headers;
267             $this->parts[] = $c;
268         }
269
270         // print body if message doesn't have multiple parts
271         if ($message_ctype_primary == 'text' && !$recursive) {
272             $structure->type = 'content';
273             $this->parts[] = &$structure;
274             
275             // Parse simple (plain text) message body
276             if ($message_ctype_secondary == 'plain')
277                 foreach ((array)$this->uu_decode($structure) as $uupart) {
278                     $this->mime_parts[$uupart->mime_id] = $uupart;
279                     $this->attachments[] = $uupart;
280                 }
281
282             // @TODO: plugin hook?
283         }
284         // the same for pgp signed messages
285         else if ($mimetype == 'application/pgp' && !$recursive) {
286             $structure->type = 'content';
287             $this->parts[] = &$structure;
288         }
289         // message contains alternative parts
290         else if ($mimetype == 'multipart/alternative' && is_array($structure->parts)) {
291             // get html/plaintext parts
292             $plain_part = $html_part = $print_part = $related_part = null;
293
294             foreach ($structure->parts as $p => $sub_part) {
295                 $sub_mimetype = $sub_part->mimetype;
296         
297                 // check if sub part is
298                 if ($sub_mimetype == 'text/plain')
299                     $plain_part = $p;
300                 else if ($sub_mimetype == 'text/html')
301                     $html_part = $p;
302                 else if ($sub_mimetype == 'text/enriched')
303                     $enriched_part = $p;
304                 else if (in_array($sub_mimetype, array('multipart/related', 'multipart/mixed', 'multipart/alternative')))
305                     $related_part = $p;
306             }
307
308             // parse related part (alternative part could be in here)
309             if ($related_part !== null && !$this->parse_alternative) {
310                 $this->parse_alternative = true;
311                 $this->parse_structure($structure->parts[$related_part], true);
312                 $this->parse_alternative = false;
313         
314                 // if plain part was found, we should unset it if html is preferred
315                 if ($this->opt['prefer_html'] && count($this->parts))
316                     $plain_part = null;
317             }
318
319             // choose html/plain part to print
320             if ($html_part !== null && $this->opt['prefer_html']) {
321                 $print_part = &$structure->parts[$html_part];
322             }
323             else if ($enriched_part !== null) {
324                 $print_part = &$structure->parts[$enriched_part];
325             }
326             else if ($plain_part !== null) {
327                 $print_part = &$structure->parts[$plain_part];
328             }
329
330             // add the right message body
331             if (is_object($print_part)) {
332                 $print_part->type = 'content';
333                 $this->parts[] = $print_part;
334             }
335             // show plaintext warning
336             else if ($html_part !== null && empty($this->parts)) {
337                 $c = new stdClass;
338                 $c->type            = 'content';
339                 $c->ctype_primary   = 'text';
340                 $c->ctype_secondary = 'plain';
341                 $c->body            = rcube_label('htmlmessage');
342
343                 $this->parts[] = $c;
344             }
345
346             // add html part as attachment
347             if ($html_part !== null && $structure->parts[$html_part] !== $print_part) {
348                 $html_part = &$structure->parts[$html_part];
349                 $html_part->filename = rcube_label('htmlmessage');
350                 $html_part->mimetype = 'text/html';
351
352                 $this->attachments[] = $html_part;
353             }
354         }
355         // this is an ecrypted message -> create a plaintext body with the according message
356         else if ($mimetype == 'multipart/encrypted') {
357             $p = new stdClass;
358             $p->type            = 'content';
359             $p->ctype_primary   = 'text';
360             $p->ctype_secondary = 'plain';
361             $p->body            = rcube_label('encryptedmessage');
362             $p->size            = strlen($p->body);
8fa58e 363       
d311d8 364             // maybe some plugins are able to decode this encrypted message part
A 365             $data = $this->app->plugins->exec_hook('message_part_encrypted',
366                 array('object' => $this, 'struct' => $structure, 'part' => $p));
8fa58e 367
d311d8 368             if (is_array($data['parts'])) {
A 369                 $this->parts = array_merge($this->parts, $data['parts']);
370             }
371             else if ($data['part']) {
372                 $this->parts[] = $p;
373             }
374         }
375         // message contains multiple parts
376         else if (is_array($structure->parts) && !empty($structure->parts)) {
377             // iterate over parts
378             for ($i=0; $i < count($structure->parts); $i++) {
379                 $mail_part      = &$structure->parts[$i];
380                 $primary_type   = $mail_part->ctype_primary;
381                 $secondary_type = $mail_part->ctype_secondary;
8fa58e 382
d311d8 383                 // real content-type of message/rfc822
A 384                 if ($mail_part->real_mimetype) {
385                     $part_orig_mimetype = $mail_part->mimetype;
386                     $part_mimetype = $mail_part->real_mimetype;
387                     list($primary_type, $secondary_type) = explode('/', $part_mimetype);
388                 }
389                 else
390                     $part_mimetype = $mail_part->mimetype;
6b6f2e 391
d311d8 392                 // multipart/alternative
A 393                 if ($primary_type == 'multipart') {
394                     $this->parse_structure($mail_part, true);
395
396                     // list message/rfc822 as attachment as well (mostly .eml)
397                     if ($part_orig_mimetype == 'message/rfc822' && !empty($mail_part->filename))
398                         $this->attachments[] = $mail_part;
399                 }
400                 // part text/[plain|html] OR message/delivery-status
401                 else if ((($part_mimetype == 'text/plain' || $part_mimetype == 'text/html') && $mail_part->disposition != 'attachment') ||
402                     $part_mimetype == 'message/delivery-status' || $part_mimetype == 'message/disposition-notification'
403                 ) {
404                     // add text part if it matches the prefs
405                     if (!$this->parse_alternative ||
406                         ($secondary_type == 'html' && $this->opt['prefer_html']) ||
407                         ($secondary_type == 'plain' && !$this->opt['prefer_html'])
408                     ) {
409                         $mail_part->type = 'content';
410                         $this->parts[] = $mail_part;
411                     }
412           
413                     // list as attachment as well
414                     if (!empty($mail_part->filename))
415                         $this->attachments[] = $mail_part;
416                 }
417                 // part message/*
418                 else if ($primary_type=='message') {
419                     $this->parse_structure($mail_part, true);
420
421                     // list as attachment as well (mostly .eml)
422                     if (!empty($mail_part->filename))
423                         $this->attachments[] = $mail_part;
424                 }
425                 // ignore "virtual" protocol parts
426                 else if ($primary_type == 'protocol') {
427                     continue;
428                 }
429                 // part is Microsoft Outlook TNEF (winmail.dat)
430                 else if ($part_mimetype == 'application/ms-tnef') {
431                     foreach ((array)$this->tnef_decode($mail_part) as $tpart) {
432                         $this->mime_parts[$tpart->mime_id] = $tpart;
433                         $this->attachments[] = $tpart;
434                     }
435                 }
436                 // part is a file/attachment
437                 else if (preg_match('/^(inline|attach)/', $mail_part->disposition) ||
438                     $mail_part->headers['content-id'] || (empty($mail_part->disposition) && $mail_part->filename)
439                 ) {
440                     // skip apple resource forks
441                     if ($message_ctype_secondary == 'appledouble' && $secondary_type == 'applefile')
442                         continue;
443
444                     // part belongs to a related message and is linked
445                     if ($mimetype == 'multipart/related'
446                         && ($mail_part->headers['content-id'] || $mail_part->headers['content-location'])) {
447                         if ($mail_part->headers['content-id'])
448                             $mail_part->content_id = preg_replace(array('/^</', '/>$/'), '', $mail_part->headers['content-id']);
449                         if ($mail_part->headers['content-location'])
450                             $mail_part->content_location = $mail_part->headers['content-base'] . $mail_part->headers['content-location'];
451
452                         $this->inline_parts[] = $mail_part;
453                     }
454                     // attachment encapsulated within message/rfc822 part needs further decoding (#1486743)
455                     else if ($part_orig_mimetype == 'message/rfc822') {
456                         $this->parse_structure($mail_part, true);
457                     }
458                     // is a regular attachment
459                     else if (preg_match('!^[a-z0-9-.+]+/[a-z0-9-.+]+$!i', $part_mimetype)) {
460                         if (!$mail_part->filename)
461                             $mail_part->filename = 'Part '.$mail_part->mime_id;
462                         $this->attachments[] = $mail_part;
463                     }
464                 }
465             }
466
467             // if this was a related part try to resolve references
468             if ($mimetype == 'multipart/related' && sizeof($this->inline_parts)) {
469                 $a_replaces = array();
470
471                 foreach ($this->inline_parts as $inline_object) {
472                     $part_url = $this->get_part_url($inline_object->mime_id);
473                     if ($inline_object->content_id)
474                         $a_replaces['cid:'.$inline_object->content_id] = $part_url;
475                     if ($inline_object->content_location)
476                         $a_replaces[$inline_object->content_location] = $part_url;
477                 }
478
479                 // add replace array to each content part
480                 // (will be applied later when part body is available)
481                 foreach ($this->parts as $i => $part) {
482                     if ($part->type == 'content')
483                         $this->parts[$i]->replaces = $a_replaces;
484                 }
485             }
486         }
487         // message is a single part non-text
488         else if ($structure->filename) {
489             $this->attachments[] = $structure;
490         }
6b6f2e 491     }
d311d8 492
A 493
494     /**
495      * Fill aflat array with references to all parts, indexed by part numbers
496      *
497      * @param object rcube_message_part Message body structure
498      */
499     private function get_mime_numbers(&$part)
500     {
501         if (strlen($part->mime_id))
502             $this->mime_parts[$part->mime_id] = &$part;
f19d86 503
d311d8 504         if (is_array($part->parts))
A 505             for ($i=0; $i<count($part->parts); $i++)
506                 $this->get_mime_numbers($part->parts[$i]);
507     }
508
509
510     /**
511      * Decode a Microsoft Outlook TNEF part (winmail.dat)
512      *
513      * @param object rcube_message_part Message part to decode
514      */
515     function tnef_decode(&$part)
516     {
517         // @TODO: attachment may be huge, hadle it via file
518         if (!isset($part->body))
519             $part->body = $this->imap->get_message_part($this->uid, $part->mime_id, $part);
520
521         $parts = array();
f19d86 522         $tnef = new tnef_decoder;
A 523         $tnef_arr = $tnef->decompress($part->body);
d311d8 524
A 525         foreach ($tnef_arr as $pid => $winatt) {
526             $tpart = new rcube_message_part;
527
528             $tpart->filename        = trim($winatt['name']);
529             $tpart->encoding        = 'stream';
f19d86 530             $tpart->ctype_primary   = trim(strtolower($winatt['type']));
A 531             $tpart->ctype_secondary = trim(strtolower($winatt['subtype']));
d311d8 532             $tpart->mimetype        = $tpart->ctype_primary . '/' . $tpart->ctype_secondary;
A 533             $tpart->mime_id         = 'winmail.' . $part->mime_id . '.' . $pid;
534             $tpart->size            = $winatt['size'];
535             $tpart->body            = $winatt['stream'];
536
537             $parts[] = $tpart;
538             unset($tnef_arr[$pid]);
539         }
f19d86 540
d311d8 541         return $parts;
A 542     }
543
544
545     /**
546      * Parse message body for UUencoded attachments bodies
547      *
548      * @param object rcube_message_part Message part to decode
549      */
550     function uu_decode(&$part)
551     {
552         // @TODO: messages may be huge, hadle body via file
553         if (!isset($part->body))
554             $part->body = $this->imap->get_message_part($this->uid, $part->mime_id, $part);
555
556         $parts = array();
557         // FIXME: line length is max.65?
558         $uu_regexp = '/begin [0-7]{3,4} ([^\n]+)\n(([\x21-\x7E]{0,65}\n)+)`\nend/s';
559
560         if (preg_match_all($uu_regexp, $part->body, $matches, PREG_SET_ORDER)) {
561             // remove attachments bodies from the message body
562             $part->body = preg_replace($uu_regexp, '', $part->body);
563             // update message content-type
564             $part->ctype_primary   = 'multipart';
565             $part->ctype_secondary = 'mixed';
566             $part->mimetype        = $part->ctype_primary . '/' . $part->ctype_secondary;
567
568             // add attachments to the structure
569             foreach ($matches as $pid => $att) {
570                 $uupart = new rcube_message_part;
571
572                 $uupart->filename = trim($att[1]);
573                 $uupart->encoding = 'stream';
574                 $uupart->body     = convert_uudecode($att[2]);
575                 $uupart->size     = strlen($uupart->body);
576                 $uupart->mime_id  = 'uu.' . $part->mime_id . '.' . $pid;
577
578                 $ctype = rc_mime_content_type($uupart->body, $uupart->filename, 'application/octet-stream', true);
579                 $uupart->mimetype = $ctype;
580                 list($uupart->ctype_primary, $uupart->ctype_secondary) = explode('/', $ctype);
581
582                 $parts[] = $uupart;
583                 unset($matches[$pid]);
584             }
585         }
f19d86 586
d311d8 587         return $parts;
A 588     }
589
590
591     /**
592      * Interpret a format=flowed message body according to RFC 2646
593      *
594      * @param string  Raw body formatted as flowed text
595      * @return string Interpreted text with unwrapped lines and stuffed space removed
596      */
597     public static function unfold_flowed($text)
598     {
599         return preg_replace(
600             array('/-- (\r?\n)/',   '/^ /m',  '/(.) \r?\n/',  '/--%SIGEND%(\r?\n)/'),
601             array('--%SIGEND%\\1',  '',       '\\1 ',         '-- \\1'),
602             $text);
603     }
604
605
606     /**
607      * Wrap the given text to comply with RFC 2646
608      */
609     public static function format_flowed($text, $length = 72)
610     {
611         $out = '';
6b6f2e 612     
d311d8 613         foreach (preg_split('/\r?\n/', trim($text)) as $line) {
A 614             // don't wrap quoted lines (to avoid wrapping problems)
615             if ($line[0] != '>')
616                 $line = rc_wordwrap(rtrim($line), $length - 1, " \r\n");
617
618             $out .= $line . "\r\n";
619         }
620     
621         return $out;
622     }
6b6f2e 623
8fa58e 624 }