source: subversion/branches/devel-framework/roundcubemail/program/include/rcube_message.php @ 5885

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