source: github/program/include/rcube_message.php @ 8b92d2b

HEADcourier-fixdev-browser-capabilitiespdorelease-0.8
Last change on this file since 8b92d2b was 8b92d2b, checked in by thomascube <thomas@…>, 16 months ago

Add lib for server side mime parsing (to be used by non-imap storage backends or as fallback if imap server doesn't provide a proper structure)

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