1<?php
2/**
3 * Copyright 2002-2017 Horde LLC (http://www.horde.org/)
4 *
5 * See the enclosed file COPYING for license information (GPL). If you
6 * did not receive this file, see http://www.horde.org/licenses/gpl.
7 *
8 * @category  Horde
9 * @copyright 2002-2017 Horde LLC
10 * @license   http://www.horde.org/licenses/gpl GPL
11 * @package   IMP
12 */
13
14/**
15 * The IMP_Contents:: class contains all functions related to handling the
16 * content and output of mail messages in IMP.
17 *
18 * @author    Michael Slusarz <slusarz@horde.org>
19 * @category  Horde
20 * @copyright 2002-2017 Horde LLC
21 * @license   http://www.horde.org/licenses/gpl GPL
22 * @package   IMP
23 */
24class IMP_Contents
25{
26    /* Mask entries for getSummary(). */
27    const SUMMARY_BYTES = 1;
28    const SUMMARY_SIZE = 2;
29    const SUMMARY_ICON = 4;
30    const SUMMARY_ICON_RAW = 16384;
31    const SUMMARY_DESCRIP = 8;
32    const SUMMARY_DESCRIP_LINK = 16;
33    const SUMMARY_DOWNLOAD = 32;
34    const SUMMARY_DOWNLOAD_ZIP = 64;
35    const SUMMARY_IMAGE_SAVE = 128;
36    const SUMMARY_PRINT = 256;
37    const SUMMARY_PRINT_STUB = 512;
38    const SUMMARY_STRIP = 1024;
39
40    /* Rendering mask entries. */
41    const RENDER_FULL = 1;
42    const RENDER_INLINE = 2;
43    const RENDER_INLINE_DISP_NO = 4;
44    const RENDER_INFO = 8;
45    const RENDER_INLINE_AUTO = 16;
46    const RENDER_RAW = 32;
47    const RENDER_RAW_FALLBACK = 64;
48
49    /* Header return type for getHeader(). */
50    const HEADER_OB = 1;
51    const HEADER_TEXT = 2;
52    const HEADER_STREAM = 3;
53
54    /**
55     * Have we scanned for embedded parts?
56     *
57     * @var boolean
58     */
59    protected $_build = false;
60
61    /**
62     * The list of MIME IDs that consist of embedded data.
63     *
64     * @var array
65     */
66    protected $_embedded = array();
67
68    /**
69     * Message header.
70     *
71     * @var mixed
72     */
73    protected $_header;
74
75    /**
76     * The index of the current message.
77     *
78     * @var IMP_Indices_Mailbox
79     */
80    protected $_indices;
81
82    /**
83     * The Horde_Mime_Part object for the message.
84     *
85     * @var Horde_Mime_Part
86     */
87    protected $_message;
88
89    /**
90     * Cached data for the MIME Viewer objects.
91     *
92     * @var object
93     */
94    protected $_viewcache;
95
96    /**
97     * Constructor.
98     *
99     * @param mixed $in  An IMP_Indices_Mailbox or Horde_Mime_Part object.
100     *
101     * @throws IMP_Exception
102     */
103    public function __construct($in)
104    {
105        if ($in instanceof Horde_Mime_Part) {
106            $this->_message = $in;
107        } else {
108            $this->_indices = $in;
109
110            /* Get the Horde_Mime_Part object for the given UID. */
111            $query = new Horde_Imap_Client_Fetch_Query();
112            $query->structure();
113
114            if (!($ret = $this->_fetchData($query))) {
115                $e = new IMP_Exception(_("Error displaying message: message does not exist on server."));
116                $e->setLogLevel('NOTICE');
117                throw $e;
118            }
119
120            $this->_message = $ret->getStructure();
121        }
122    }
123
124    /**
125     * String representation of object.
126     *
127     * @return string  The indices string.
128     */
129    public function __toString()
130    {
131        return strval($this->getIndicesOb());
132    }
133
134    /**
135     * Returns the IMAP UID for the current message.
136     *
137     * @return integer  The message UID.
138     */
139    public function getUid()
140    {
141        list(,$uid) = $this->_indices->getSingle();
142        return $uid;
143    }
144
145    /**
146     * Returns the IMAP mailbox for the current message.
147     *
148     * @return IMP_Mailbox  The message mailbox.
149     */
150    public function getMailbox()
151    {
152        list($mbox,) = $this->_indices->getSingle();
153        return $mbox;
154    }
155
156    /**
157     * Return an IMP_Indices object for the current message.
158     *
159     * @return IMP_Indices  An indices object.
160     */
161    public function getIndicesOb()
162    {
163        return $this->_indices;
164    }
165
166    /**
167     * Returns the entire body of the message.
168     *
169     * @param array $options  Additional options:
170     *   - stream: (boolean) If true, return a stream.
171     *             DEFAULT: No
172     *
173     * @return mixed  The text of the part, or a stream resource if 'stream'
174     *                is true.
175     */
176    public function getBody($options = array())
177    {
178        if (!$this->_indices) {
179            return $this->_message->toString(array(
180                'headers' => true,
181                'stream' => !empty($options['stream'])
182            ));
183        }
184
185        $query = new Horde_Imap_Client_Fetch_Query();
186        $query->bodytext(array(
187            'peek' => true
188        ));
189
190        return ($res = $this->_fetchData($query))
191            ? $res->getBodyText(0, !empty($options['stream']))
192            : '';
193    }
194
195    /**
196     * Gets the raw text for one section of the message.
197     *
198     * @param integer $id     The ID of the MIME part.
199     * @param array $options  Additional options:
200     *   - decode: (boolean) Attempt to decode the bodypart on the remote
201     *             server.
202     *             DEFAULT: No
203     *   - length: (integer) If set, only download this many bytes of the
204     *             bodypart from the server.
205     *             DEFAULT: All data is retrieved.
206     *   - mimeheaders: (boolean) Include the MIME headers also?
207     *                  DEFAULT: No
208     *   - stream: (boolean) If true, return a stream.
209     *             DEFAULT: No
210     *
211     * @return object  Object with the following properties:
212     * <pre>
213     *   - data: (mixed) The text of the part or a stream resource if 'stream'
214     *           option is true.
215     *   - decode: (string) If 'decode' option is true, and bodypart decoded
216     *             on server, the content-type of the decoded data.
217     * </pre>
218     */
219    public function getBodyPart($id, $options = array())
220    {
221        $ret = new stdClass;
222        $ret->data = '';
223        $ret->decode = null;
224
225        if (empty($id)) {
226            return $ret;
227        }
228
229        if (!$this->_indices || $this->isEmbedded($id)) {
230            if (empty($options['mimeheaders']) ||
231                in_array($id, $this->_embedded)) {
232                $ob = $this->getMIMEPart($id, array('nocontents' => true));
233
234                if (empty($options['stream'])) {
235                    if (!is_null($ob)) {
236                        $ret->data = $ob->getContents();
237                    }
238                } else {
239                    $ret->data = is_null($ob)
240                        ? fopen('php://temp', 'r+')
241                        : $ob->getContents(array('stream' => true));
242                }
243
244                return $ret;
245            }
246
247            $base_id = $id;
248            while (!in_array($base_id, $this->_embedded, true)) {
249                $base_id = Horde_Mime::mimeIdArithmetic($base_id, 'up');
250                if (is_null($base_id)) {
251                    return $ret;
252                }
253            }
254
255            $part = $this->getMIMEPart($base_id, array('nocontents' => true));
256            $txt = $part->addMimeHeaders()->toString() .
257                "\n" .
258                $part->getContents();
259
260            try {
261                $body = Horde_Mime_Part::getRawPartText($txt, 'header', '1') .
262                    "\n\n" .
263                    Horde_Mime_Part::getRawPartText($txt, 'body', '1');
264            } catch (Horde_Mime_Exception $e) {
265                $body = '';
266            }
267
268            if (empty($options['stream'])) {
269                $ret->data = $body;
270                return $ret;
271            }
272
273            $ret->data = fopen('php://temp', 'r+');
274            if (strlen($body)) {
275                fwrite($ret->data, $body);
276                fseek($ret->data, 0);
277            }
278            return $ret;
279        }
280
281        $query = new Horde_Imap_Client_Fetch_Query();
282        if (substr($id, -2) === '.0') {
283            $rfc822 = true;
284            $id = substr($id, 0, -2);
285        } else {
286            $rfc822 = false;
287        }
288
289        if (!isset($options['length']) || !empty($options['length'])) {
290            $bodypart_params = array(
291                'decode' => !empty($options['decode']),
292                'peek' => true
293            );
294
295            if (isset($options['length'])) {
296                $bodypart_params['start'] = 0;
297                $bodypart_params['length'] = $options['length'];
298            }
299
300            if ($rfc822) {
301                $bodypart_params['id'] = $id;
302                $query->bodyText($bodypart_params);
303            } else {
304                $query->bodyPart($id, $bodypart_params);
305            }
306        }
307
308        if (!empty($options['mimeheaders'])) {
309            if ($rfc822) {
310                $query->headerText(array(
311                    'id' => $id,
312                    'peek' => true
313                ));
314            } else {
315                $query->mimeHeader($id, array(
316                    'peek' => true
317                ));
318            }
319        }
320
321        if ($res = $this->_fetchData($query)) {
322            try {
323                if (empty($options['mimeheaders'])) {
324                    $ret->decode = $res->getBodyPartDecode($id);
325                    $ret->data = $rfc822
326                        ? $res->getBodyText($id, !empty($options['stream']))
327                        : $res->getBodyPart($id, !empty($options['stream']));
328                    return $ret;
329                } elseif (empty($options['stream'])) {
330                    $ret->data = $rfc822
331                        ? ($res->getHeaderText($id) . $res->getBodyText($id))
332                        : ($res->getMimeHeader($id) . $res->getBodyPart($id));
333                    return $ret;
334                }
335
336                if ($rfc822) {
337                    $data = array(
338                        $res->getHeaderText($id, Horde_Imap_Client_Data_Fetch::HEADER_STREAM),
339                        $res->getBodyText($id, true)
340                    );
341                } else {
342                    $data = array(
343                        $res->getMimeHeader($id, Horde_Imap_Client_Data_Fetch::HEADER_STREAM),
344                        $res->getBodyPart($id, true)
345                    );
346                }
347
348                $ret->data = Horde_Stream_Wrapper_Combine::getStream($data);
349                return $ret;
350            } catch (Horde_Exception $e) {}
351        }
352
353        if (!empty($options['stream'])) {
354            $ret->data = fopen('php://temp', 'r+');
355        }
356
357        return $ret;
358    }
359
360    /**
361     * Returns the full message text.
362     *
363     * @param array $options  Additional options:
364     *   - stream: (boolean) If true, return a stream for bodytext.
365     *             DEFAULT: No
366     *
367     * @return mixed  The full message text or a stream resource if 'stream'
368     *                is true.
369     */
370    public function fullMessageText($options = array())
371    {
372        if (!$this->_indices) {
373            return $this->_message->toString();
374        }
375
376        $query = new Horde_Imap_Client_Fetch_Query();
377        $query->bodyText(array(
378            'peek' => true
379        ));
380
381        if ($res = $this->_fetchData($query)) {
382            try {
383                if (empty($options['stream'])) {
384                    return $this->getHeader(self::HEADER_TEXT) . $res->getBodyText(0);
385                }
386
387                return Horde_Stream_Wrapper_Combine::getStream(array(
388                    $this->getHeader(self::HEADER_STREAM),
389                    $res->getBodyText(0, true)
390                ));
391            } catch (Horde_Exception $e) {}
392        }
393
394        return empty($options['stream'])
395            ? ''
396            : fopen('php://temp', 'r+');
397    }
398
399    /**
400     * Returns base header information.
401     *
402     * @param integer $type  Return type (HEADER_* constant).
403     *
404     * @return mixed  Either a Horde_Mime_Headers object (HEADER_OB), header
405     *                text (HEADER_TEXT), or a stream resource (HEADER_STREAM).
406     */
407    public function getHeader($type = self::HEADER_OB)
408    {
409        return $this->_getHeader($type, false);
410    }
411
412    /**
413     * Returns base header information and marks the message as seen.
414     *
415     * @param integer $type  See getHeader().
416     *
417     * @return mixed  See getHeader().
418     */
419    public function getHeaderAndMarkAsSeen($type = self::HEADER_OB)
420    {
421        $mbox = $this->getMailbox();
422
423        if ($mbox->readonly) {
424            $seen = false;
425        } else {
426            $seen = true;
427
428            if (isset($this->_header)) {
429                try {
430                    $imp_imap = $mbox->imp_imap;
431                    $imp_imap->store($mbox, array(
432                        'add' => array(
433                            Horde_Imap_Client::FLAG_SEEN
434                        ),
435                        'ids' => $imp_imap->getIdsOb($this->getUid())
436                    ));
437                } catch (Exception $e) {}
438            }
439        }
440
441        return $this->_getHeader($type, $seen);
442    }
443
444    /**
445     * Returns base header information.
446     *
447     * @param integer $type  See getHeader().
448     * @param boolean $seen  Mark message as seen?
449     *
450     * @return mixed  See getHeader().
451     */
452    protected function _getHeader($type, $seen)
453    {
454        if (!isset($this->_header)) {
455            if (!$this->_indices) {
456                $this->_header = $this->_message->addMimeHeaders();
457            } else {
458                $query = new Horde_Imap_Client_Fetch_Query();
459                $query->headerText(array(
460                    'peek' => !$seen
461                ));
462
463                $this->_header = ($res = $this->_fetchData($query))
464                    ? $res
465                    : new Horde_Imap_Client_Data_Fetch();
466            }
467        }
468
469        switch ($type) {
470        case self::HEADER_OB:
471            return $this->_indices
472                ? $this->_header->getHeaderText(0, Horde_Imap_Client_Data_Fetch::HEADER_PARSE)
473                : $this->_header;
474
475        case self::HEADER_TEXT:
476            return $this->_indices
477                ? $this->_header->getHeaderText()
478                : $this->_header->toString();
479
480        case self::HEADER_STREAM:
481            if ($this->_indices) {
482                return $this->_header->getHeaderText(0, Horde_Imap_Client_Data_Fetch::HEADER_STREAM);
483            }
484
485            $stream = new Horde_Support_StringStream($this->_header->toString());
486            $stream->fopen();
487            return $stream;
488        }
489    }
490
491    /**
492     * Returns the Horde_Mime_Part object.
493     *
494     * @return Horde_Mime_Part  A Horde_Mime_Part object.
495     */
496    public function getMIMEMessage()
497    {
498        return $this->_message;
499    }
500
501    /**
502     * Fetch a part of a MIME message.
503     *
504     * @param integer $id     The MIME index of the part requested.
505     * @param array $options  Additional options:
506     *   - length: (integer) If set, only download this many bytes of the
507     *             bodypart from the server.
508     *             DEFAULT: All data is retrieved.
509     *   - nocontents: (boolean) If true, don't add the contents to the part
510     *                 DEFAULT: Contents are added to the part
511     *
512     * @return Horde_Mime_Part  The raw MIME part asked for (reference).
513     */
514    public function getMIMEPart($id, $options = array())
515    {
516        $this->_buildMessage();
517
518        $part = $this->_message->getPart($id);
519
520        /* Ticket #9201: Treat 'ISO-8859-1' as 'windows-1252'. 1252 has some
521         * characters (e.g. euro sign, back quote) not in 8859-1. There
522         * shouldn't be any issue doing this since the additional code points
523         * in 1252 don't map to anything in 8859-1. */
524        if ($part &&
525            (strcasecmp($part->getCharset(), 'ISO-8859-1') === 0)) {
526            $part->setCharset('windows-1252');
527        }
528
529        /* Don't download contents of entire body if ID == 0 (indicating the
530         * body of the main multipart message).  I'm pretty sure we never
531         * want to download the body of that part here. */
532        if (!empty($id) &&
533            !is_null($part) &&
534            (substr($id, -2) != '.0') &&
535            empty($options['nocontents']) &&
536            $this->_indices &&
537            !$part->getContents(array('stream' => true))) {
538            $body = $this->getBodyPart($id, array(
539                'decode' => true,
540                'length' => empty($options['length']) ? null : $options['length'],
541                'stream' => true
542            ));
543            $part->setContents($body->data, array(
544                'encoding' => $body->decode,
545                'usestream' => true
546            ));
547        }
548
549        return $part;
550    }
551
552    /**
553     * Render a MIME Part.
554     *
555     * @param string $mime_id  The MIME ID to render.
556     * @param integer $mode    One of the RENDER_ constants.
557     * @param array $options   Additional options:
558     *   - autodetect: (boolean) Attempt to auto-detect MIME type?
559     *   - mime_part: (Horde_Mime_Part) The MIME part to render.
560     *   - type: (string) Use this MIME type instead of the MIME type
561     *           identified in the MIME part.
562     *
563     * @return array  See Horde_Mime_Viewer_Base::render(). The following
564     *                fields may also be present in addition to the fields
565     *                defined in Horde_Mime_Viewer_Base:
566     *   - attach: (boolean) Force display of this part as an attachment.
567     *   - js: (array) A list of javascript commands to run after the content
568     *         is displayed on screen.
569     *   - name: (string) Contains the MIME name information.
570     *   - wrap: (string) If present, indicates that this part, and all child
571     *           parts, will be wrapped in a DIV with the given class name.
572     */
573    public function renderMIMEPart($mime_id, $mode, array $options = array())
574    {
575        $this->_buildMessage();
576
577        $mime_part = empty($options['mime_part'])
578            ? $this->getMIMEPart($mime_id)
579            : $options['mime_part'];
580        if (!$mime_part) {
581            return array($mime_id => null);
582        }
583
584        if (!empty($options['autodetect']) &&
585            ($tempfile = Horde::getTempFile()) &&
586            ($fp = fopen($tempfile, 'w')) &&
587            !is_null($contents = $mime_part->getContents(array('stream' => true)))) {
588            rewind($contents);
589            while (!feof($contents)) {
590                fwrite($fp, fread($contents, 8192));
591            }
592            fclose($fp);
593
594            $options['type'] = Horde_Mime_Magic::analyzeFile($tempfile, empty($GLOBALS['conf']['mime']['magic_db']) ? null : $GLOBALS['conf']['mime']['magic_db']);
595        }
596
597        $type = empty($options['type'])
598            ? null
599            : $options['type'];
600
601        $viewer = $GLOBALS['injector']->getInstance('IMP_Factory_MimeViewer')->create($mime_part, array('contents' => $this, 'type' => $type));
602
603        switch ($mode) {
604        case self::RENDER_INLINE:
605        case self::RENDER_INLINE_AUTO:
606        case self::RENDER_INLINE_DISP_NO:
607            $textmode = 'inline';
608            $limit = $viewer->getConfigParam('limit_inline_size');
609
610            if ($limit && ($mime_part->getBytes() > $limit)) {
611                $data = '';
612                $status = new IMP_Mime_Status(array(
613                    _("This message part cannot be viewed because it is too large."),
614                    sprintf(_("Click %s to download the data."), $this->linkView($mime_part, 'download_attach', _("HERE")))
615                ));
616                $status->icon('alerts/warning.png', _("Warning"));
617
618                if (method_exists($viewer, 'overLimitText')) {
619                    $data = $viewer->overLimitText();
620                    $status->addText(_("The initial portion of this text part is displayed below."));
621                }
622
623                return array(
624                    $mime_id => array(
625                        'data' => $data,
626                        'name' => '',
627                        'status' => $status,
628                        'type' => 'text/html; charset=' . 'UTF-8'
629                    )
630                );
631            }
632            break;
633
634        case self::RENDER_INFO:
635            $textmode = 'info';
636            break;
637
638        case self::RENDER_RAW:
639            $textmode = 'raw';
640            break;
641
642        case self::RENDER_RAW_FALLBACK:
643            $textmode = $viewer->canRender('raw')
644                ? 'raw'
645                : 'full';
646            break;
647
648        case self::RENDER_FULL:
649        default:
650            $textmode = 'full';
651            break;
652        }
653
654        $ret = $viewer->render($textmode);
655
656        if (empty($ret)) {
657            return ($mode == self::RENDER_INLINE_AUTO)
658                ? $this->renderMIMEPart($mime_id, self::RENDER_INFO, $options)
659                : array();
660        }
661
662        if (!empty($ret[$mime_id]) && !isset($ret[$mime_id]['name'])) {
663            $ret[$mime_id]['name'] = $mime_part->getName(true);
664        }
665
666        /* Don't show empty parts. */
667        if (($textmode == 'inline') &&
668            !is_null($ret[$mime_id]['data']) &&
669            !strlen($ret[$mime_id]['data']) &&
670            !isset($ret[$mime_id]['status'])) {
671            $ret[$mime_id] = null;
672        }
673
674        return $ret;
675    }
676
677    /**
678     * Finds the main "body" text part (if any) in a message.
679     * "Body" data is the first text part in the base MIME part.
680     *
681     * @param string $subtype  Specifically search for this subtype.
682     *
683     * @return string  The MIME ID of the main body part.
684     */
685    public function findBody($subtype = null)
686    {
687        $this->_buildMessage();
688        return $this->_message->findBody($subtype);
689    }
690
691    /**
692     * Generate the preview text.
693     *
694     * @return array  Array with the following keys:
695     *   - cut: (boolean) Was the preview text cut?
696     *   - text: (string) The preview text.
697     */
698    public function generatePreview()
699    {
700        // For preview generation, don't go through overhead of scanning for
701        // embedded parts. Necessary evil, or else very large parts (e.g
702        // 5 MB+ text parts) will take ages to scan.
703        $oldbuild = $this->_build;
704        $this->_build = true;
705        $mimeid = $this->findBody();
706
707        if (is_null($mimeid)) {
708            $this->_build = $oldbuild;
709            return array('cut' => false, 'text' => '');
710        }
711
712        $maxlen = empty($GLOBALS['conf']['msgcache']['preview_size'])
713            ? $GLOBALS['prefs']->getValue('preview_maxlen')
714            : $GLOBALS['conf']['msgcache']['preview_size'];
715
716        // Retrieve 3x the size of $maxlen of bodytext data. This should
717        // account for any content-encoding & HTML tags.
718        $pmime = $this->getMIMEPart($mimeid, array('length' => $maxlen * 3));
719
720        $ptext = Horde_String::convertCharset($pmime->getContents(), $pmime->getCharset(), 'UTF-8');
721
722        if ($pmime->getType() == 'text/html') {
723            $ptext = $GLOBALS['injector']->getInstance('Horde_Core_Factory_TextFilter')->filter($ptext, 'Html2text');
724        }
725
726        $this->_build = $oldbuild;
727
728        if (Horde_String::length($ptext) > $maxlen) {
729            return array(
730                'cut' => true,
731                'text' => Horde_String::truncate($ptext, $maxlen)
732            );
733        }
734
735        return array(
736            'cut' => false,
737            'text' => $ptext
738        );
739    }
740
741    /**
742     * Get summary info for a MIME ID.
743     *
744     * @param string $id     The MIME ID.
745     * @param integer $mask  A bitmask indicating what information to return:
746     * <pre>
747     * Always output:
748     *   'type' = MIME type
749     *
750     * IMP_Contents::SUMMARY_BYTES
751     *   Output: parts = 'bytes'
752     *
753     * IMP_Contents::SUMMARY_SIZE
754     *   Output: parts = 'size'
755     *
756     * IMP_Contents::SUMMARY_ICON
757     * IMP_Contents::SUMMARY_ICON_RAW
758     *   Output: parts = 'icon'
759     *
760     * IMP_Contents::SUMMARY_DESCRIP
761     *   Output: parts = 'description_raw'
762     *
763     * IMP_Contents::SUMMARY_DESCRIP_LINK
764     *   Output: parts = 'description'
765     *
766     * IMP_Contents::SUMMARY_DOWNLOAD
767     *   Output: parts = 'download', 'download_url'
768     *
769     * IMP_Contents::SUMMARY_DOWNLOAD_ZIP
770     *   Output: parts = 'download_zip'
771     *
772     * IMP_Contents::SUMMARY_IMAGE_SAVE
773     *   Output: parts = 'img_save'
774     *
775     * IMP_Contents::SUMMARY_PRINT
776     * IMP_Contents::SUMMARY_PRINT_STUB
777     *   Output: parts = 'print'
778     *
779     * IMP_Contents::SUMMARY_STRIP
780     *   Output: parts = 'strip'
781     * </pre>
782     *
783     * @return array  An array with the requested information.
784     */
785    public function getSummary($id, $mask = 0)
786    {
787        $autodetect_link = false;
788        $download_zip = (($mask & self::SUMMARY_DOWNLOAD_ZIP) && Horde_Util::extensionExists('zlib'));
789        $param_array = array();
790
791        $this->_buildMessage();
792
793        $part = array(
794            'bytes' => null,
795            'download' => null,
796            'download_url' => null,
797            'download_zip' => null,
798            'id' => $id,
799            'img_save' => null,
800            'size' => null,
801            'strip' => null
802        );
803
804        $mime_part = $this->getMIMEPart($id, array('nocontents' => true));
805        if (empty($mime_part)) {
806            throw new IMP_Exception('MIME Part not found.');
807        }
808        $mime_type = $mime_part->getType();
809
810        /* If this is an attachment that has no specific MIME type info, see
811         * if we can guess a rendering type. */
812        if (in_array($mime_type, array('application/octet-stream', 'application/base64'))) {
813            $mime_type = Horde_Mime_Magic::filenameToMIME($mime_part->getName());
814            if ($mime_type == $mime_part->getType()) {
815                $autodetect_link = true;
816            } else {
817                $mime_part = clone $mime_part;
818                $mime_part->setType($mime_type);
819                $param_array['ctype'] = $mime_type;
820            }
821        }
822        $part['type'] = $mime_type;
823
824        /* Is this part an attachment? */
825        $is_atc = $this->isAttachment($mime_type);
826
827        /* Get bytes/size information. */
828        if (($mask & self::SUMMARY_BYTES) ||
829            $download_zip ||
830            ($mask & self::SUMMARY_SIZE)) {
831            $part['bytes'] = $size = $mime_part->getBytes();
832            $part['size'] = ($size > 1048576)
833                ? sprintf(_("%s MB"), IMP::numberFormat($size / 1048576, 1))
834                : sprintf(_("%s KB"), max(round($size / 1024), 1));
835        }
836
837        /* Get part's icon. */
838        if (($mask & self::SUMMARY_ICON) ||
839            ($mask & self::SUMMARY_ICON_RAW)) {
840            $part['icon'] = $GLOBALS['injector']->getInstance('IMP_Factory_MimeViewer')->getIcon($mime_type);
841            if ($mask & self::SUMMARY_ICON) {
842                $part['icon'] = Horde_Themes_Image::tag($part['icon'], array(
843                    'attr' => array(
844                        'title' => $mime_type
845                    )
846                ));
847            }
848        } else {
849            $part['icon'] = null;
850        }
851
852        /* Get part's description. */
853        $description = $this->getPartName($mime_part, true);
854
855        if ($mask & self::SUMMARY_DESCRIP_LINK) {
856            if (($can_d = $this->canDisplay($mime_part, self::RENDER_FULL)) ||
857                $autodetect_link) {
858                $part['description'] = $this->linkViewJS($mime_part, 'view_attach', htmlspecialchars($description), array('jstext' => sprintf(_("View %s"), $description), 'params' => array_filter(array_merge($param_array, array(
859                    'autodetect' => !$can_d
860                )))));
861            } else {
862                $part['description'] = htmlspecialchars($description);
863            }
864        }
865        if ($mask & self::SUMMARY_DESCRIP) {
866            $part['description_raw'] = $description;
867        }
868
869        /* Download column. */
870        if (($mask & self::SUMMARY_DOWNLOAD) &&
871            $is_atc &&
872            (is_null($part['bytes']) || $part['bytes'])) {
873            $part['download'] = $this->linkView($mime_part, 'download_attach', '', array('class' => 'iconImg downloadAtc', 'jstext' => _("Download")));
874            $part['download_url'] = $this->urlView($mime_part, 'download_attach');
875        }
876
877        /* Display the compressed download link only if size is greater
878         * than 200 KB. */
879        if ($is_atc &&
880            $download_zip &&
881            ($part['bytes'] > 204800)) {
882            $viewer = $GLOBALS['injector']->getInstance('IMP_Factory_MimeViewer')->create($mime_part, array('contents' => $this, 'type' => $mime_type));
883            if (!$viewer->getMetadata('compressed')) {
884                $part['download_zip'] = $this->linkView($mime_part, 'download_attach', null, array('class' => 'iconImg downloadZipAtc', 'jstext' => sprintf(_("Download %s in .zip Format"), $description), 'params' => array('zip' => 1)));
885            }
886        }
887
888        /* Display the image save link if the required registry calls are
889         * present. */
890        if (($mask & self::SUMMARY_IMAGE_SAVE) &&
891            $GLOBALS['registry']->hasMethod('images/selectGalleries') &&
892            ($mime_part->getPrimaryType() == 'image')) {
893            $part['img_save'] = Horde::link('#', _("Save Image in Gallery"), 'iconImg saveImgAtc', null, Horde::popupJs(IMP_Basic_Saveimage::url(), array('params' => array('muid' => strval($this->getIndicesOb()), 'id' => $id), 'height' => 200, 'width' => 450, 'urlencode' => true)) . 'return false;') . '</a>';
894        }
895
896        /* Add print link? */
897        if ((($mask & self::SUMMARY_PRINT) ||
898             ($mask & self::SUMMARY_PRINT_STUB)) &&
899            $this->canDisplay($id, self::RENDER_FULL)) {
900            $part['print'] = ($mask & self::SUMMARY_PRINT)
901                ? $this->linkViewJS($mime_part, 'print_attach', '', array('css' => 'iconImg printAtc', 'jstext' => _("Print"), 'onload' => 'IMP_JS.printWindow', 'params' => $param_array))
902                : Horde::link('#', _("Print"), 'iconImg printAtc', null, null, null, null, array('mimeid' => $id)) . '</a>';
903        }
904
905        /* Strip Attachment? Allow stripping of base parts other than the
906         * base multipart and the base text (body) part. */
907        if (($mask & self::SUMMARY_STRIP) &&
908            ($id != 0) &&
909            (intval($id) != 1) &&
910            (strpos($id, '.') === false)) {
911            $part['strip'] = Horde::link(
912                Horde::selfUrlParams()->add(array(
913                    'actionID' => 'strip_attachment',
914                    'imapid' => $id,
915                    'muid' => strval($this->getIndicesOb()),
916                    'token' => $GLOBALS['session']->getToken()
917                )),
918                _("Strip Attachment"),
919                'iconImg deleteImg stripAtc',
920                null,
921                null,
922                null,
923                null,
924                array('mimeid' => $id)
925            ) . '</a>';
926        }
927
928        return $part;
929    }
930
931    /**
932     * Return the URL to the download/view page.
933     *
934     * @param Horde_Mime_Part $mime_part  The MIME part to view.
935     * @param integer $actionID           The actionID to perform.
936     * @param array $options              Additional options:
937     *   - params: (array) A list of any additional parameters that need to be
938     *             passed to the download/view page (key => name).
939     *
940     * @return Horde_Url  The URL to the download/view page.
941     */
942    public function urlView($mime_part = null, $actionID = 'view_attach',
943                            array $options = array())
944    {
945        $params = $this->_urlViewParams($mime_part, $actionID, isset($options['params']) ? $options['params'] : array());
946
947        return (strpos($actionID, 'download_') === 0)
948            ? IMP_Contents_View::downloadUrl($mime_part->getName(true), $params)
949            : Horde::url('view.php', true)->add($params);
950    }
951
952    /**
953     * Generates the necessary URL parameters for the download/view page.
954     *
955     * @param Horde_Mime_Part $mime_part  The MIME part to view.
956     * @param integer $actionID           The actionID to perform.
957     * @param array $params               Additional parameters to pass.
958     *
959     * @return array  The array of parameters.
960     */
961    protected function _urlViewParams($mime_part, $actionID, $params)
962    {
963        /* Add the necessary local parameters. */
964        $params = array_merge($params, array(
965            'actionID' => $actionID,
966            'id' => isset($params['id']) ? $params['id'] : $mime_part->getMIMEId()
967        ));
968
969        if ($this->_indices) {
970            $params['muid'] = strval($this->getIndicesOb());
971        }
972
973        return IMP_Contents_View::addToken($params);
974    }
975
976    /**
977     * Generate a link to the download/view page.
978     *
979     * @param Horde_Mime_Part $mime_part  The MIME part to view.
980     * @param integer $actionID           The actionID value.
981     * @param string $text                The ESCAPED (!) link text.
982     * @param array $options              Additional parameters:
983     *   - class: (string) The CSS class to use.
984     *   - jstext: (string) The JS text to use.
985     *   - params: (array) A list of any additional parameters that need to be
986     *             passed to the download/view page.
987     *
988     * @return string  A HTML href link to the download/view page.
989     */
990    public function linkView($mime_part, $actionID, $text, $options = array())
991    {
992        $options = array_merge(array(
993            'class' => null,
994            'jstext' => $text,
995            'params' => array()
996        ), $options);
997
998        return Horde::link(
999            $this->urlView($mime_part, $actionID, $options),
1000            $options['jstext'],
1001            $options['class'],
1002            ($actionID == 'download_attach') ? null : strval(new Horde_Support_Randomid())
1003        ) . $text . '</a>';
1004    }
1005
1006    /**
1007     * Generate a javascript link to the download/view page.
1008     *
1009     * @param Horde_Mime_Part $mime_part  The MIME part to view.
1010     * @param string $actionID            The actionID to perform.
1011     * @param string $text                The ESCAPED (!) link text.
1012     * @param array $options              Additional options:
1013     *   - css: (string) The CSS class to use.
1014     *   - jstext: (string) The javascript link text.
1015     *   - onload: (string) A JS function to run when popup window is
1016     *             fully loaded.
1017     *   - params: (array) A list of any additional parameters that need to be
1018     *             passed to download/view page. (key = name)
1019     *   - widget: (boolean) If true use Horde::widget() to generate,
1020     *             Horde::link() otherwise.
1021     *
1022     * @return string  A HTML href link to the download/view page.
1023     */
1024    public function linkViewJS($mime_part, $actionID, $text,
1025                               $options = array())
1026    {
1027        if (empty($options['params'])) {
1028            $options['params'] = array();
1029        }
1030
1031        if (empty($options['jstext'])) {
1032            $options['jstext'] = sprintf(_("View %s"), $mime_part->getDescription(true));
1033        }
1034
1035        $url = Horde::popupJs(Horde::url('view.php'), array(
1036            'menu' => true,
1037            'onload' => empty($options['onload']) ? 'IMP_JS.resizePopup' : $options['onload'],
1038            'params' => $this->_urlViewParams($mime_part, $actionID, isset($options['params']) ? $options['params'] : array()),
1039            'urlencode' => true
1040        ));
1041
1042        return empty($options['widget'])
1043            ? Horde::link('#', $options['jstext'], empty($options['css']) ? null : $options['css'], null, $url) . $text . '</a>'
1044            : Horde::widget(array('url' => '#', 'class' => empty($options['css']) ? null : $options['css'], 'onclick' => $url, 'title' => $text));
1045    }
1046
1047    /**
1048     * Determines if a MIME type is an attachment.
1049     * For IMP's purposes, an attachment is any MIME part that can be
1050     * downloaded by itself (i.e. all the data needed to view the part is
1051     * contained within the download data).
1052     *
1053     * @param string $mime_type  The MIME type.
1054     *
1055     * @return boolean  True if an attachment.
1056     */
1057    public function isAttachment($mime_type)
1058    {
1059        switch ($mime_type) {
1060        case 'application/ms-tnef':
1061            return false;
1062        }
1063
1064        list($ptype,) = explode('/', $mime_type, 2);
1065
1066        switch ($ptype) {
1067        case 'message':
1068            return in_array($mime_type, array('message/rfc822', 'message/disposition-notification'));
1069
1070        case 'multipart':
1071            return false;
1072
1073        default:
1074            return true;
1075        }
1076    }
1077
1078    /**
1079     * Builds the "virtual" Horde_Mime_Part object by checking for embedded
1080     * parts.
1081     *
1082     * @param array $parts  The parts list to process.
1083     */
1084    protected function _buildMessage($parts = null)
1085    {
1086        global $injector;
1087
1088        if (is_null($parts)) {
1089            if ($this->_build) {
1090                return;
1091            }
1092            $this->_build = true;
1093            $parts = array_keys($this->_message->contentTypeMap());
1094            $first_id = reset($parts);
1095        } else {
1096            $first_id = null;
1097        }
1098
1099        $last_id = null;
1100        $to_process = array();
1101
1102        $mv_factory = $injector->getInstance('IMP_Factory_MimeViewer');
1103
1104        foreach ($parts as $id) {
1105            if (!is_null($last_id) &&
1106                (strpos($id, $last_id) === 0)) {
1107                continue;
1108            }
1109
1110            $last_id = null;
1111
1112            $mime_part = $this->getMIMEPart($id, array('nocontents' => true));
1113            $viewer = $mv_factory->create($mime_part, array('contents' => $this));
1114            if ($viewer->embeddedMimeParts()) {
1115                $mime_part = $this->getMIMEPart($id);
1116                $viewer->setMIMEPart($mime_part);
1117                $new_part = $viewer->getEmbeddedMimeParts();
1118                if (!is_null($new_part)) {
1119                    $mime_part->addPart($new_part);
1120                    $mime_part->buildMimeIds($id);
1121                    $this->_embedded[] = $new_part->getMimeId();
1122                    $to_process = array_merge($to_process, array_keys($new_part->contentTypeMap()));
1123                    $last_id = $id;
1124                }
1125            }
1126        }
1127
1128        if (!empty($to_process)) {
1129            $this->_buildMessage($to_process);
1130        }
1131    }
1132
1133    /**
1134     * Can this MIME part be displayed in the given mode?
1135     *
1136     * @param mixed $part    The MIME part or a MIME ID string.
1137     * @param integer $mask  One of the RENDER_ constants.
1138     * @param string $type   The type to use (overrides the MIME ID if $id is
1139     *                       a MIME part).
1140     *
1141     * @return integer  The RENDER_ constant of the allowable display.
1142     */
1143    public function canDisplay($part, $mask, $type = null)
1144    {
1145        if (!is_object($part)) {
1146            $part = $this->getMIMEPart($part, array('nocontents' => true));
1147        }
1148        if (!$part) {
1149            return 0;
1150        }
1151        $viewer = $GLOBALS['injector']->getInstance('IMP_Factory_MimeViewer')->create($part, array('contents' => $this, 'type' => $type));
1152
1153        if ($mask & self::RENDER_INLINE_AUTO) {
1154            $mask |= self::RENDER_INLINE | self::RENDER_INFO;
1155        }
1156
1157        if (($mask & self::RENDER_RAW) && $viewer->canRender('raw')) {
1158            return self::RENDER_RAW;
1159        }
1160
1161        if (($mask & self::RENDER_FULL) && $viewer->canRender('full')) {
1162            return self::RENDER_FULL;
1163        }
1164
1165        if ($mask & self::RENDER_INLINE) {
1166            if ($viewer->canRender('inline')) {
1167                return self::RENDER_INLINE;
1168            }
1169        } elseif (($mask & self::RENDER_INLINE_DISP_NO) &&
1170                  $viewer->canRender('inline')) {
1171            return self::RENDER_INLINE_DISP_NO;
1172        }
1173
1174        if (($mask & self::RENDER_INFO) && $viewer->canRender('info')) {
1175            return self::RENDER_INFO;
1176        }
1177
1178        return 0;
1179    }
1180
1181    /**
1182     * Returns the Content-Type map for the entire message, regenerating
1183     * embedded parts if needed.
1184     *
1185     * @return array  See Horde_Mime_Part::contentTypeMap().
1186     */
1187    public function getContentTypeMap()
1188    {
1189        $this->_buildMessage();
1190        return $this->_message->contentTypeMap();
1191    }
1192
1193    /**
1194     * Returns the MIME part tree of the message.
1195     *
1196     * @param string $renderer  Either the tree renderer driver or a full
1197     *                          class name to use.
1198     *
1199     * @return Horde_Tree_Renderer_Base  A tree instance representing the MIME parts.
1200     * @throws Horde_Tree_Exception
1201     */
1202    public function getTree($renderer = 'Horde_Core_Tree_Renderer_Html')
1203    {
1204        $tree = $GLOBALS['injector']->getInstance('Horde_Core_Factory_Tree')->create('mime-' . $this->getUid(), $renderer, array(
1205            'nosession' => true
1206        ));
1207        $this->_addTreeNodes($tree, $this->_message);
1208        return $tree;
1209    }
1210
1211    /**
1212     * Adds MIME parts to the tree instance.
1213     *
1214     * @param Horde_Tree_Renderer_Base tree   A tree instance.
1215     * @param Horde_Mime_Part $part           The MIME part to add to the
1216     *                                        tree, including its sub-parts.
1217     * @param string $parent                  The parent part's MIME id.
1218     */
1219    protected function _addTreeNodes($tree, $part, $parent = null)
1220    {
1221        $mimeid = $part->getMimeId();
1222
1223        $summary_mask = self::SUMMARY_ICON_RAW | self::SUMMARY_DESCRIP_LINK | self::SUMMARY_SIZE | self::SUMMARY_DOWNLOAD;
1224        if ($GLOBALS['prefs']->getValue('strip_attachments')) {
1225            $summary_mask += self::SUMMARY_STRIP;
1226        }
1227
1228        $summary = $this->getSummary($mimeid, $summary_mask);
1229
1230        $tree->addNode(array(
1231            'id' => $mimeid,
1232            'parent' => $parent,
1233            'label' => sprintf(
1234                '%s (%s) %s %s',
1235                $summary['description'],
1236                $summary['size'],
1237                $summary['download'],
1238                $summary['strip']
1239            ),
1240            'params' => array(
1241                'class' => 'partsTreeDiv',
1242                'icon' => $summary['icon']
1243            )
1244        ));
1245
1246        foreach ($part->getParts() as $part) {
1247            $this->_addTreeNodes($tree, $part, $mimeid);
1248        }
1249    }
1250
1251    /**
1252     * Get download all list.
1253     *
1254     * @return array  An array of downloadable parts.
1255     */
1256    public function downloadAllList()
1257    {
1258        $ret = array();
1259
1260        foreach ($this->getContentTypeMap() as $key => $val) {
1261            if ($this->isAttachment($val)) {
1262                $ret[] = $key;
1263            }
1264        }
1265
1266        return $ret;
1267    }
1268
1269    /**
1270     * Injects body contents into the base Horde_Mime_part object.
1271     *
1272     * @param array $ignore  A list of MIME IDs to ignore.
1273     *
1274     * @return Horde_Mime_Part  The part with body contents set.
1275     */
1276    public function buildMessageContents($ignore = array())
1277    {
1278        $message = $this->_message;
1279        $curr_ignore = null;
1280
1281        foreach ($message->contentTypeMap() as $key => $val) {
1282            if (is_null($curr_ignore) && in_array($key, $ignore)) {
1283                $curr_ignore = $key . '.';
1284            } elseif (is_null($curr_ignore) ||
1285                      (strpos($key, $curr_ignore) === false)) {
1286                $curr_ignore = null;
1287                if (($key != 0) &&
1288                    ($val != 'message/rfc822') &&
1289                    (strpos($val, 'multipart/') === false)) {
1290                    $part = $this->getMIMEPart($key);
1291                    $message->alterPart($key, $part);
1292                }
1293            }
1294        }
1295
1296        return $message;
1297    }
1298
1299    /**
1300     * Determines if a given MIME part ID is a part of embedded data.
1301     *
1302     * @param string $mime_id  The MIME ID.
1303     *
1304     * @return boolean  True if the MIME ID is part of embedded data.
1305     */
1306    public function isEmbedded($mime_id)
1307    {
1308        foreach ($this->_embedded as $val) {
1309            if (($mime_id == $val) || Horde_Mime::isChild($val, $mime_id)) {
1310                return true;
1311            }
1312        }
1313
1314        return false;
1315    }
1316
1317    /**
1318     * Find a MIME type in parent parts.
1319     *
1320     * @param string $id    The MIME ID to begin the search at.
1321     * @param string $type  The MIME type to search for.
1322     *
1323     * @return mixed  Either the requested MIME part, or null if not found.
1324     */
1325    public function findMimeType($id, $type)
1326    {
1327        $id = Horde_Mime::mimeIdArithmetic($id, 'up');
1328
1329        while (!is_null($id)) {
1330            if (($part = $this->getMIMEPart($id, array('nocontents' => true))) &&
1331                ($part->getType() == $type)) {
1332                return $part;
1333            }
1334            $id = Horde_Mime::mimeIdArithmetic($id, 'up');
1335        }
1336
1337        return null;
1338    }
1339
1340    /**
1341     * Return the descriptive part label, making sure it is not empty.
1342     *
1343     * @param Horde_Mime_Part $part  The MIME Part object.
1344     * @param boolean $use_descrip   Use description? If false, uses name.
1345     *
1346     * @return string  The part label (non-empty).
1347     */
1348    public function getPartName(Horde_Mime_Part $part, $use_descrip = false)
1349    {
1350        $name = $use_descrip
1351            ? $part->getDescription(true)
1352            : $part->getName(true);
1353
1354        if ($name) {
1355            return $name;
1356        }
1357
1358        switch ($ptype = $part->getPrimaryType()) {
1359        case 'multipart':
1360            if (($part->getSubType() == 'related') &&
1361                ($view_id = $part->getMetaData('viewable_part')) &&
1362                ($viewable = $this->getMIMEPart($view_id, array('nocontents' => true)))) {
1363                return $this->getPartName($viewable, $use_descrip);
1364            }
1365            /* Fall-through. */
1366
1367        case 'application':
1368        case 'model':
1369            $ptype = $part->getSubType();
1370            break;
1371        }
1372
1373        switch ($ptype) {
1374        case 'audio':
1375            return _("Audio");
1376
1377        case 'image':
1378            return _("Image");
1379
1380        case 'message':
1381        case '':
1382        case Horde_Mime_Part::UNKNOWN:
1383            return _("Message");
1384
1385        case 'multipart':
1386            return _("Multipart");
1387
1388        case 'text':
1389            return _("Text");
1390
1391        case 'video':
1392            return _("Video");
1393
1394        default:
1395            // Attempt to translate this type, if possible. Odds are that
1396            // it won't appear in the dictionary though.
1397            return _(Horde_String::ucfirst($ptype));
1398        }
1399    }
1400
1401    /**
1402     * Generate inline message display.
1403     *
1404     * @param array $options  Options:
1405     *   - display_mask: (integer) The mask of display view type to render
1406     *                   inline (DEFAULT: RENDER_INLINE_AUTO).
1407     *   - mask: (integer) The mask needed for a getSummary() call.
1408     *   - no_inline_all: (boolean) If true, only display first inline part.
1409     *                    Subsequent inline parts will be treated as
1410     *                    attachments.
1411     *   - part_info_display: (array) The list of summary fields to display.
1412     *   - show_parts: (string) The value of the 'parts_display' pref.
1413     *
1414     * @return array  An array with the following keys:
1415     *   - atc_parts: (array) The list of attachment MIME IDs.
1416     *   - display_ids: (array) The list of display MIME IDs.
1417     *   - js_onload: (array) A list of javascript code to run onload.
1418     *   - msgtext: (string) The rendered HTML code.
1419     *   - one_part: (boolean) If true, the message only consists of one part.
1420     */
1421    public function getInlineOutput(array $options = array())
1422    {
1423        global $prefs, $registry;
1424
1425        $atc_parts = $display_ids = $msgtext = $js_onload = $wrap_ids = array();
1426        $parts_list = $this->getContentTypeMap();
1427        $text_out = '';
1428        $view = $registry->getView();
1429
1430        $contents_mask = isset($options['mask'])
1431            ? $options['mask']
1432            : 0;
1433        $display_mask = isset($options['display_mask'])
1434            ? $options['display_mask']
1435            : self::RENDER_INLINE_AUTO;
1436        $no_inline_all = !empty($options['no_inline_all']);
1437        $part_info_display = isset($options['part_info_display'])
1438            ? $options['part_info_display']
1439            : array();
1440        $show_parts = isset($options['show_parts'])
1441            ? $options['show_parts']
1442            : $prefs->getValue('parts_display');
1443
1444        foreach ($parts_list as $mime_id => $mime_type) {
1445            if (isset($display_ids[$mime_id]) ||
1446                isset($atc_parts[$mime_id])) {
1447                continue;
1448            }
1449
1450            if (!($render_mode = $this->canDisplay($mime_id, $display_mask))) {
1451                if ($this->isAttachment($mime_type)) {
1452                    if ($show_parts == 'atc') {
1453                        $atc_parts[$mime_id] = 1;
1454                    }
1455
1456                    if ($contents_mask) {
1457                        $msgtext[$mime_id] = array(
1458                            'text' => $this->_formatSummary($mime_id, $contents_mask, $part_info_display, true)
1459                        );
1460                    }
1461                }
1462                continue;
1463            }
1464
1465            $render_part = $this->renderMIMEPart($mime_id, $render_mode);
1466            if (($show_parts == 'atc') &&
1467                $this->isAttachment($mime_type) &&
1468                (empty($render_part) ||
1469                 !($render_mode & self::RENDER_INLINE))) {
1470                $atc_parts[$mime_id] = 1;
1471            }
1472
1473            if (empty($render_part)) {
1474                if ($contents_mask &&
1475                    $this->isAttachment($mime_type)) {
1476                    $msgtext[$mime_id] = array(
1477                        'text' => $this->_formatSummary($mime_id, $contents_mask, $part_info_display, true)
1478                    );
1479                }
1480                continue;
1481            }
1482
1483            reset($render_part);
1484            while (list($id, $info) = each($render_part)) {
1485                $display_ids[$id] = 1;
1486
1487                if (empty($info)) {
1488                    continue;
1489                }
1490
1491                if ($no_inline_all === 1) {
1492                    $atc_parts[$id] = 1;
1493                    continue;
1494                }
1495
1496                $part_text = ($contents_mask && empty($info['nosummary']))
1497                    ? $this->_formatSummary($id, $contents_mask, $part_info_display, !empty($info['attach']))
1498                    : '';
1499
1500                if (empty($info['attach'])) {
1501                    if (isset($info['status'])) {
1502                        if (!is_array($info['status'])) {
1503                            $info['status'] = array($info['status']);
1504                        }
1505
1506                        foreach ($info['status'] as $val) {
1507                            if (in_array($view, $val->views)) {
1508                                $part_text .= strval($val);
1509                            }
1510                        }
1511                    }
1512
1513                    $part_text .= '<div class="mimePartData">' . $info['data'] . '</div>';
1514                } elseif ($show_parts == 'atc') {
1515                    $atc_parts[$id] = 1;
1516                }
1517
1518                $msgtext[$id] = array(
1519                    'text' => $part_text,
1520                    'wrap' => empty($info['wrap']) ? null : $info['wrap']
1521                );
1522
1523                if (isset($info['js'])) {
1524                    $js_onload = array_merge($js_onload, $info['js']);
1525                }
1526
1527                if ($no_inline_all) {
1528                    $no_inline_all = 1;
1529                }
1530            }
1531        }
1532
1533        if (!empty($msgtext)) {
1534            uksort($msgtext, 'strnatcmp');
1535        }
1536
1537        reset($msgtext);
1538        while (list($id, $part) = each($msgtext)) {
1539            while (!empty($wrap_ids) &&
1540                   !Horde_Mime::isChild(end($wrap_ids), $id)) {
1541                array_pop($wrap_ids);
1542                $text_out .= '</div>';
1543            }
1544
1545            if (!empty($part['wrap'])) {
1546                $text_out .= '<div class="' . $part['wrap'] . '">';
1547                $wrap_ids[] = $id;
1548            }
1549
1550            $text_out .= '<div class="mimePartBase">' . $part['text'] . '</div>';
1551        }
1552
1553        $text_out .= str_repeat('</div>', count($wrap_ids));
1554
1555        if (!strlen($text_out)) {
1556            $text_out = strval(new IMP_Mime_Status(_("There are no parts that can be shown inline.")));
1557        }
1558
1559        $atc_parts = ($show_parts == 'all')
1560            ? array_keys($parts_list)
1561            : array_keys($atc_parts);
1562
1563        return array(
1564            'atc_parts' => $atc_parts,
1565            'display_ids' => array_keys($display_ids),
1566            'js_onload' => $js_onload,
1567            'msgtext' => $text_out,
1568            'one_part' => (count($parts_list) == 1)
1569        );
1570    }
1571
1572    /**
1573     * Prints out a MIME summary (in HTML).
1574     *
1575     * @param string $id      The MIME ID.
1576     * @param integer $mask   A bitmask indicating what summary information to
1577     *                        return.
1578     * @param array $display  The fields to display (in this order).
1579     * @param boolean $atc    Is this an attachment?
1580     *
1581     * @return string  The formatted summary string.
1582     */
1583    protected function _formatSummary($id, $mask, $display, $atc = false)
1584    {
1585        $summary = $this->getSummary($id, $mask);
1586        $tmp_summary = array();
1587
1588        foreach ($display as $val) {
1589            if (isset($summary[$val])) {
1590                switch ($val) {
1591                case 'description':
1592                    $summary[$val] = '<span class="mimePartInfoDescrip">' . $summary[$val] . '</span>';
1593                    break;
1594
1595                case 'size':
1596                    $summary[$val] = '<span class="mimePartInfoSize">(' . $summary[$val] . ')</span>';
1597                    break;
1598                }
1599                $tmp_summary[] = $summary[$val];
1600            }
1601        }
1602
1603        return '<div class="mimePartInfo' .
1604            ($atc ? ' mimePartInfoAtc' : '') .
1605            '"><div>' .
1606            implode(' ', $tmp_summary) .
1607            '</div></div>';
1608    }
1609
1610    /**
1611     * Get FETCH data from IMAP server for this message.
1612     *
1613     * @param Horde_Imap_Client_Fetch_Query $query  Search query.
1614     *
1615     * @return Horde_Imap_Client_Data_Fetch  Fetch data for the message.
1616     */
1617    protected function _fetchData(Horde_Imap_Client_Fetch_Query $query)
1618    {
1619        try {
1620            $mbox = $this->getMailbox();
1621            $imp_imap = $mbox->imp_imap;
1622            return $imp_imap->fetch($mbox, $query, array(
1623                'ids' => $imp_imap->getIdsOb($this->getUid())
1624            ))->first();
1625        } catch (Horde_Imap_Client_Exception $e) {
1626            return new Horde_Imap_Client_Data_Fetch();
1627        }
1628    }
1629
1630    /**
1631     * Return the view cache object for this message.
1632     *
1633     * @return object  View object.
1634     */
1635    public function getViewCache()
1636    {
1637        if (!isset($this->_viewcache)) {
1638            $this->_viewcache = new stdClass;
1639        }
1640
1641        return $this->_viewcache;
1642    }
1643
1644}
1645