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_Compose:: class represents an outgoing mail message.
16 *
17 * @author    Michael Slusarz <slusarz@horde.org>
18 * @category  Horde
19 * @copyright 2002-2017 Horde LLC
20 * @license   http://www.horde.org/licenses/gpl GPL
21 * @package   IMP
22 */
23class IMP_Compose implements ArrayAccess, Countable, IteratorAggregate
24{
25    /* The virtual path to save drafts. */
26    const VFS_DRAFTS_PATH = '.horde/imp/drafts';
27
28    /* Compose types. */
29    const COMPOSE = 0;
30    const REPLY = 1;
31    const REPLY_ALL = 2;
32    const REPLY_AUTO = 3;
33    const REPLY_LIST = 4;
34    const REPLY_SENDER = 5;
35    const FORWARD = 6;
36    const FORWARD_ATTACH = 7;
37    const FORWARD_AUTO = 8;
38    const FORWARD_BODY = 9;
39    const FORWARD_BOTH = 10;
40    const REDIRECT = 11;
41    const EDITASNEW = 12;
42    const TEMPLATE = 13;
43
44    /* Related part attribute name. */
45    const RELATED_ATTR = 'imp_related_attr';
46
47    /* The blockquote tag to use to indicate quoted text in HTML data. */
48    const HTML_BLOCKQUOTE = '<blockquote type="cite" style="border-left:2px solid blue;margin-left:2px;padding-left:12px;">';
49
50    /**
51     * Attachment ID counter.
52     *
53     * @var integer
54     */
55    public $atcId = 0;
56
57    /**
58     * Mark as changed for purposes of storing in the session.
59     * Either empty, 'changed', or 'deleted'.
60     *
61     * @var string
62     */
63    public $changed = '';
64
65    /**
66     * The charset to use for sending.
67     *
68     * @var string
69     */
70    public $charset;
71
72    /**
73     * Attachment data.
74     *
75     * @var array
76     */
77    protected $_atc = array();
78
79    /**
80     * The cache ID used to store object in session.
81     *
82     * @var string
83     */
84    protected $_cacheid;
85
86    /**
87     * Various metadata for this message.
88     *
89     * @var array
90     */
91    protected $_metadata = array();
92
93    /**
94     * The reply type.
95     *
96     * @var integer
97     */
98    protected $_replytype = self::COMPOSE;
99
100    /**
101     * Constructor.
102     *
103     * @param string $cacheid  The cache ID string.
104     */
105    public function __construct($cacheid)
106    {
107        $this->_cacheid = $cacheid;
108        $this->charset = $GLOBALS['registry']->getEmailCharset();
109    }
110
111    /**
112     * Tasks to do upon unserialize().
113     */
114    public function __wakeup()
115    {
116        $this->changed = '';
117    }
118
119    /**
120     * Destroys an IMP_Compose instance.
121     *
122     * @param string $action  The action performed to cause the end of this
123     *                        instance.  Either 'cancel', 'discard',
124     *                        'save_draft', or 'send'.
125     */
126    public function destroy($action)
127    {
128        switch ($action) {
129        case 'discard':
130        case 'send':
131            /* Delete the draft. */
132            $GLOBALS['injector']->getInstance('IMP_Message')->delete(
133                new IMP_Indices($this->getMetadata('draft_uid')),
134                array('nuke' => true)
135            );
136            break;
137
138        case 'save_draft':
139            /* Don't delete any drafts. */
140            $this->changed = 'deleted';
141            return;
142
143        case 'cancel':
144            if ($this->getMetadata('draft_auto')) {
145                $this->destroy('discard');
146                return;
147            }
148            // Fall-through
149
150        default:
151            // No-op
152            break;
153        }
154
155        $this->deleteAllAttachments();
156
157        $this->changed = 'deleted';
158    }
159
160    /**
161     * Gets metadata about the current object.
162     *
163     * @param string $name  The metadata name.
164     *
165     * @return mixed  The metadata value or null if it doesn't exist.
166     */
167    public function getMetadata($name)
168    {
169        return isset($this->_metadata[$name])
170            ? $this->_metadata[$name]
171            : null;
172    }
173
174    /**
175     * Sets metadata for the current object.
176     *
177     * @param string $name  The metadata name.
178     * @param mixed $value  The metadata value.
179     */
180    protected function _setMetadata($name, $value)
181    {
182        if (is_null($value)) {
183            unset($this->_metadata[$name]);
184        } else {
185            $this->_metadata[$name] = $value;
186        }
187        $this->changed = 'changed';
188    }
189
190    /**
191     * Saves a draft message.
192     *
193     * @param array $headers  List of message headers (UTF-8).
194     * @param mixed $message  Either the message text (string) or a
195     *                        Horde_Mime_Part object that contains the text
196     *                        to send.
197     * @param array $opts     An array of options w/the following keys:
198     * <pre>
199     *   - autosave: (boolean) Is this an auto-saved draft?
200     *   - html: (boolean) Is this an HTML message?
201     *   - priority: (string) The message priority ('high', 'normal', 'low').
202     *   - readreceipt: (boolean) Add return receipt headers?
203     * </pre>
204     *
205     * @return string  Notification text on success (not HTML encoded).
206     *
207     * @throws IMP_Compose_Exception
208     */
209    public function saveDraft($headers, $message, array $opts = array())
210    {
211        $body = $this->_saveDraftMsg($headers, $message, $opts);
212        $ret = $this->_saveDraftServer($body);
213        $this->_setMetadata('draft_auto', !empty($opts['autosave']));
214        return $ret;
215    }
216
217    /**
218     * Prepare the draft message.
219     *
220     * @param array $headers  List of message headers.
221     * @param mixed $message  Either the message text (string) or a
222     *                        Horde_Mime_Part object that contains the text
223     *                        to send.
224     * @param array $opts     An array of options w/the following keys:
225     *   - html: (boolean) Is this an HTML message?
226     *   - priority: (string) The message priority ('high', 'normal', 'low').
227     *   - readreceipt: (boolean) Add return receipt headers?
228     *   - verify_email: (boolean) Verify e-mail messages? Default: no.
229     *
230     * @return string  The body text.
231     *
232     * @throws IMP_Compose_Exception
233     */
234    protected function _saveDraftMsg($headers, $message, $opts)
235    {
236        $has_session = (bool)$GLOBALS['registry']->getAuth();
237
238        /* Set up the base message now. */
239        $base = $this->_createMimeMessage(new Horde_Mail_Rfc822_List(), $message, array(
240            'html' => !empty($opts['html']),
241            'noattach' => !$has_session,
242            'nofinal' => true
243        ));
244        $base->isBasePart(true);
245
246        $recip_list = $this->recipientList($headers);
247        if (!empty($opts['verify_email'])) {
248            foreach ($recip_list['list'] as $val) {
249                try {
250                    IMP::parseAddressList($val->writeAddress(true), array(
251                        'validate' => true
252                    ));
253                } catch (Horde_Mail_Exception $e) {
254                    throw new IMP_Compose_Exception(sprintf(
255                        _("Saving the message failed because it contains an invalid e-mail address: %s."),
256                        strval($val),
257                        $e->getMessage()
258                    ), $e->getCode());
259                }
260            }
261        }
262        $headers = array_merge($headers, $recip_list['header']);
263
264        /* Initalize a header object for the draft. */
265        $draft_headers = $this->_prepareHeaders($headers, array_merge($opts, array('bcc' => true)));
266
267        /* Add information necessary to log replies/forwards when finally
268         * sent. */
269        $imp_imap = $GLOBALS['injector']->getInstance('IMP_Factory_Imap')->create();
270        if ($this->_replytype) {
271            try {
272                $indices = $this->getMetadata('indices');
273
274                $imap_url = new Horde_Imap_Client_Url();
275                $imap_url->hostspec = $imp_imap->getParam('hostspec');
276                $imap_url->protocol = $imp_imap->isImap() ? 'imap' : 'pop';
277                $imap_url->username = $imp_imap->getParam('username');
278
279                $urls = array();
280                foreach ($indices as $val) {
281                    $imap_url->mailbox = $val->mbox;
282                    $imap_url->uidvalidity = $val->mbox->uidvalid;
283                    foreach ($val->uids as $val2) {
284                        $imap_url->uid = $val2;
285                        $urls[] = '<' . strval($imap_url) . '>';
286                    }
287                }
288
289                switch ($this->replyType(true)) {
290                case self::FORWARD:
291                    $draft_headers->addHeader('X-IMP-Draft-Forward', implode(', ', $urls));
292                    break;
293
294                case self::REPLY:
295                    $draft_headers->addHeader('X-IMP-Draft-Reply', implode(', ', $urls));
296                    $draft_headers->addHeader('X-IMP-Draft-Reply-Type', $this->_replytype);
297                    break;
298                }
299            } catch (Horde_Exception $e) {}
300        } else {
301            $draft_headers->addHeader('X-IMP-Draft', 'Yes');
302        }
303
304        return $base->toString(array(
305            'defserver' => $has_session ? $imp_imap->config->maildomain : null,
306            'headers' => $draft_headers
307        ));
308    }
309
310    /**
311     * Save a draft message on the IMAP server.
312     *
313     * @param string $data  The text of the draft message.
314     *
315     * @return string  Status string (not HTML escaped).
316     *
317     * @throws IMP_Compose_Exception
318     */
319    protected function _saveDraftServer($data)
320    {
321        if (!$drafts_mbox = IMP_Mailbox::getPref(IMP_Mailbox::MBOX_DRAFTS)) {
322            throw new IMP_Compose_Exception(_("Saving the draft failed. No drafts mailbox specified."));
323        }
324
325        /* Check for access to drafts mailbox. */
326        if (!$drafts_mbox->create()) {
327            throw new IMP_Compose_Exception(_("Saving the draft failed. Could not create a drafts mailbox."));
328        }
329
330        $append_flags = array(
331            Horde_Imap_Client::FLAG_DRAFT,
332            /* RFC 3503 [3.4] - MUST set MDNSent flag on draft message. */
333            Horde_Imap_Client::FLAG_MDNSENT
334        );
335        if (!$GLOBALS['prefs']->getValue('unseen_drafts')) {
336            $append_flags[] = Horde_Imap_Client::FLAG_SEEN;
337        }
338
339        $old_uid = $this->getMetadata('draft_uid');
340
341        /* Add the message to the mailbox. */
342        try {
343            $ids = $drafts_mbox->imp_imap->append($drafts_mbox, array(array('data' => $data, 'flags' => $append_flags)));
344
345            if ($old_uid) {
346                $GLOBALS['injector']->getInstance('IMP_Message')->delete($old_uid, array('nuke' => true));
347            }
348
349            $this->_setMetadata('draft_uid', $drafts_mbox->getIndicesOb($ids));
350            return sprintf(_("The draft has been saved to the \"%s\" mailbox."), $drafts_mbox->display);
351        } catch (IMP_Imap_Exception $e) {
352            return _("The draft was not successfully saved.");
353        }
354    }
355
356    /**
357     * Edits a message as new.
358     *
359     * @see resumeDraft().
360     *
361     * @param IMP_Indices $indices  An indices object.
362     * @param array $opts           Additional options:
363     *   - format: (string) Force to this format.
364     *             DEFAULT: Auto-determine.
365     *
366     * @return mixed  See resumeDraft().
367     *
368     * @throws IMP_Compose_Exception
369     */
370    public function editAsNew($indices, array $opts = array())
371    {
372        $ret = $this->_resumeDraft($indices, self::EDITASNEW, $opts);
373        $ret['type'] = self::EDITASNEW;
374        return $ret;
375    }
376
377    /**
378     * Edit an existing template message. Saving this template later
379     * (using saveTemplate()) will cause the original message to be deleted.
380     *
381     * @param IMP_Indices $indices  An indices object.
382     *
383     * @return mixed  See resumeDraft().
384     *
385     * @throws IMP_Compose_Exception
386     */
387    public function editTemplate($indices)
388    {
389        $res = $this->useTemplate($indices);
390        $this->_setMetadata('template_uid_edit', $indices);
391        return $res;
392    }
393
394    /**
395     * Resumes a previously saved draft message.
396     *
397     * @param IMP_Indices $indices  An indices object.
398     * @param array $opts           Additional options:
399     *   - format: (string) Force to this format.
400     *             DEFAULT: Auto-determine.
401     *
402     * @return mixed  An array with the following keys:
403     *   - addr: (array) Address lists (to, cc, bcc; Horde_Mail_Rfc822_List
404     *           objects).
405     *   - body: (string) The text of the body part.
406     *   - format: (string) The format of the body message ('html', 'text').
407     *   - identity: (mixed) See IMP_Prefs_Identity#getMatchingIdentity().
408     *   - priority: (string) The message priority.
409     *   - readreceipt: (boolean) Add return receipt headers?
410     *   - subject: (string) Formatted subject.
411     *   - type: (integer) - The compose type.
412     *
413     * @throws IMP_Compose_Exception
414     */
415    public function resumeDraft($indices, array $opts = array())
416    {
417        $res = $this->_resumeDraft($indices, null, $opts);
418        $this->_setMetadata('draft_uid', $indices);
419        return $res;
420    }
421
422    /**
423     * Uses a template to create a message.
424     *
425     * @see resumeDraft().
426     *
427     * @param IMP_Indices $indices  An indices object.
428     * @param array $opts           Additional options:
429     *   - format: (string) Force to this format.
430     *             DEFAULT: Auto-determine.
431     *
432     * @return mixed  See resumeDraft().
433     *
434     * @throws IMP_Compose_Exception
435     */
436    public function useTemplate($indices, array $opts = array())
437    {
438        $ret = $this->_resumeDraft($indices, self::TEMPLATE, $opts);
439        $ret['type'] = self::TEMPLATE;
440        return $ret;
441    }
442
443    /**
444     * Resumes a previously saved draft message.
445     *
446     * @param IMP_Indices $indices  See resumeDraft().
447     * @param integer $type         Compose type.
448     * @param array $opts           Additional options:
449     *   - format: (string) Force to this format.
450     *             DEFAULT: Auto-determine.
451     *
452     * @return mixed  See resumeDraft().
453     *
454     * @throws IMP_Compose_Exception
455     */
456    protected function _resumeDraft($indices, $type, $opts)
457    {
458        global $injector, $notification, $prefs;
459
460        $contents_factory = $injector->getInstance('IMP_Factory_Contents');
461
462        try {
463            $contents = $contents_factory->create($indices);
464        } catch (IMP_Exception $e) {
465            throw new IMP_Compose_Exception($e);
466        }
467
468        $headers = $contents->getHeader();
469        $imp_draft = false;
470
471        if ($draft_url = $headers->getValue('x-imp-draft-reply')) {
472            if (is_null($type) &&
473                !($type = $headers->getValue('x-imp-draft-reply-type'))) {
474                $type = self::REPLY;
475            }
476            $imp_draft = self::REPLY;
477        } elseif ($draft_url = $headers->getValue('x-imp-draft-forward')) {
478            $imp_draft = self::FORWARD;
479            if (is_null($type)) {
480                $type = self::FORWARD;
481            }
482        } elseif ($headers->getValue('x-imp-draft')) {
483            $imp_draft = self::COMPOSE;
484        }
485
486        if (!empty($opts['format'])) {
487            $compose_html = ($opts['format'] == 'html');
488        } elseif ($prefs->getValue('compose_html')) {
489            $compose_html = true;
490        } else {
491            switch ($type) {
492            case self::EDITASNEW:
493            case self::FORWARD:
494            case self::FORWARD_BODY:
495            case self::FORWARD_BOTH:
496                $compose_html = $prefs->getValue('forward_format');
497                break;
498
499            case self::REPLY:
500            case self::REPLY_ALL:
501            case self::REPLY_LIST:
502            case self::REPLY_SENDER:
503                $compose_html = $prefs->getValue('reply_format');
504                break;
505
506            case self::TEMPLATE:
507                $compose_html = true;
508                break;
509
510            default:
511                /* If this is an draft saved by IMP, we know 100% for sure
512                 * that if an HTML part exists, the user was composing in
513                 * HTML. */
514                $compose_html = ($imp_draft !== false);
515                break;
516            }
517        }
518
519        $msg_text = $this->_getMessageText($contents, array(
520            'html' => $compose_html,
521            'imp_msg' => $imp_draft,
522            'toflowed' => false
523        ));
524
525        if (empty($msg_text)) {
526            $body = '';
527            $format = 'text';
528            $text_id = 0;
529        } else {
530            /* Use charset at time of initial composition if this is an IMP
531             * draft. */
532            if ($imp_draft !== false) {
533                $this->charset = $msg_text['charset'];
534            }
535            $body = $msg_text['text'];
536            $format = $msg_text['mode'];
537            $text_id = $msg_text['id'];
538        }
539
540        $mime_message = $contents->getMIMEMessage();
541
542        /* Add attachments. */
543        $parts = array();
544        if (($mime_message->getPrimaryType() == 'multipart') &&
545            ($mime_message->getType() != 'multipart/alternative')) {
546            for ($i = 1; ; ++$i) {
547                if (intval($text_id) == $i) {
548                    continue;
549                }
550
551                if ($part = $contents->getMIMEPart($i)) {
552                    $parts[] = $part;
553                } else {
554                    break;
555                }
556            }
557        } elseif ($mime_message->getDisposition() == 'attachment') {
558            $parts[] = $contents->getMimePart('1');
559        }
560
561        foreach ($parts as $val) {
562            try {
563                $this->addAttachmentFromPart($val);
564            } catch (IMP_Compose_Exception $e) {
565                $notification->push($e, 'horde.warning');
566            }
567        }
568
569        $alist = new Horde_Mail_Rfc822_List();
570        $addr = array(
571            'to' => clone $alist,
572            'cc' => clone $alist,
573            'bcc' => clone $alist
574        );
575
576        if ($type != self::EDITASNEW) {
577            foreach (array('to', 'cc', 'bcc') as $val) {
578                if ($tmp = $headers->getOb($val)) {
579                    $addr[$val] = $tmp;
580                }
581            }
582
583            if ($val = $headers->getValue('references')) {
584                $ref_ob = new Horde_Mail_Rfc822_Identification($val);
585                $this->_setMetadata('references', $ref_ob->ids);
586
587                if ($val = $headers->getValue('in-reply-to')) {
588                    $this->_setMetadata('in_reply_to', $val);
589                }
590            }
591
592            if ($draft_url) {
593                $imp_imap = $injector->getInstance('IMP_Factory_Imap')->create();
594                $indices = new IMP_Indices();
595
596                foreach (explode(',', $draft_url) as $val) {
597                    $imap_url = new Horde_Imap_Client_Url(rtrim(ltrim($val, '<'), '>'));
598
599                    try {
600                        if (($imap_url->protocol == ($imp_imap->isImap() ? 'imap' : 'pop')) &&
601                            ($imap_url->username == $imp_imap->getParam('username')) &&
602                            // Ignore hostspec and port, since these can change
603                            // even though the server is the same. UIDVALIDITY
604                            // should catch any true server/backend changes.
605                            (IMP_Mailbox::get($imap_url->mailbox)->uidvalid == $imap_url->uidvalidity) &&
606                            $contents_factory->create(new IMP_Indices($imap_url->mailbox, $imap_url->uid))) {
607                            $indices->add($imap_url->mailbox, $imap_url->uid);
608                        }
609                    } catch (Exception $e) {}
610                }
611
612                if (count($indices)) {
613                    $this->_setMetadata('indices', $indices);
614                    $this->_replytype = $type;
615                }
616            }
617        }
618
619        $mdn = new Horde_Mime_Mdn($headers);
620        $readreceipt = (bool)$mdn->getMdnReturnAddr();
621
622        $this->changed = 'changed';
623
624        return array(
625            'addr' => $addr,
626            'body' => $body,
627            'format' => $format,
628            'identity' => $this->_getMatchingIdentity($headers, array('from')),
629            'priority' => $injector->getInstance('IMP_Mime_Headers')->getPriority($headers),
630            'readreceipt' => $readreceipt,
631            'subject' => $headers->getValue('subject'),
632            'type' => $type
633        );
634    }
635
636    /**
637     * Save a template message on the IMAP server.
638     *
639     * @param array $headers  List of message headers (UTF-8).
640     * @param mixed $message  Either the message text (string) or a
641     *                        Horde_Mime_Part object that contains the text
642     *                        to save.
643     * @param array $opts     An array of options w/the following keys:
644     *   - html: (boolean) Is this an HTML message?
645     *   - priority: (string) The message priority ('high', 'normal', 'low').
646     *   - readreceipt: (boolean) Add return receipt headers?
647     *
648     * @return string  Notification text on success.
649     *
650     * @throws IMP_Compose_Exception
651     */
652    public function saveTemplate($headers, $message, array $opts = array())
653    {
654        if (!$mbox = IMP_Mailbox::getPref(IMP_Mailbox::MBOX_TEMPLATES)) {
655            throw new IMP_Compose_Exception(_("Saving the template failed: no template mailbox exists."));
656        }
657
658        /* Check for access to mailbox. */
659        if (!$mbox->create()) {
660            throw new IMP_Compose_Exception(_("Saving the template failed: could not create the templates mailbox."));
661        }
662
663        $append_flags = array(
664            // Don't mark as draft, since other MUAs could potentially
665            // delete it.
666            Horde_Imap_Client::FLAG_SEEN
667        );
668
669        $old_uid = $this->getMetadata('template_uid_edit');
670
671        /* Add the message to the mailbox. */
672        try {
673            $mbox->imp_imap->append($mbox, array(array(
674                'data' => $this->_saveDraftMsg($headers, $message, $opts),
675                'flags' => $append_flags,
676                'verify_email' => true
677            )));
678
679            if ($old_uid) {
680                $GLOBALS['injector']->getInstance('IMP_Message')->delete($old_uid, array('nuke' => true));
681            }
682        } catch (IMP_Imap_Exception $e) {
683            return _("The template was not successfully saved.");
684        }
685
686        return _("The template has been saved.");
687    }
688
689    /**
690     * Does this message have any drafts associated with it?
691     *
692     * @return boolean  True if draft messages exist.
693     */
694    public function hasDrafts()
695    {
696        return (bool)$this->getMetadata('draft_uid');
697    }
698
699    /**
700     * Builds and sends a MIME message.
701     *
702     * @param string $body                  The message body.
703     * @param array $header                 List of message headers.
704     * @param IMP_Prefs_Identity $identity  The Identity object for the sender
705     *                                      of this message.
706     * @param array $opts                   An array of options w/the
707     *                                      following keys:
708     *  - encrypt: (integer) A flag whether to encrypt or sign the message.
709     *            One of:
710     *    - IMP_Crypt_Pgp::ENCRYPT</li>
711     *    - IMP_Crypt_Pgp::SIGNENC</li>
712     *    - IMP_Crypt_Smime::ENCRYPT</li>
713     *    - IMP_Crypt_Smime::SIGNENC</li>
714     *  - html: (boolean) Whether this is an HTML message.
715     *          DEFAULT: false
716     *  - pgp_attach_pubkey: (boolean) Attach the user's PGP public key to the
717     *                       message?
718     *  - priority: (string) The message priority ('high', 'normal', 'low').
719     *  - save_sent: (boolean) Save sent mail?
720     *  - sent_mail: (IMP_Mailbox) The sent-mail mailbox (UTF-8).
721     *  - strip_attachments: (bool) Strip attachments from the message?
722     *  - signature: (string) The message signature.
723     *  - readreceipt: (boolean) Add return receipt headers?
724     *  - useragent: (string) The User-Agent string to use.
725     *  - vcard_attach: (string) Attach the user's vCard (value is name to
726     *                  display as vcard filename).
727     *
728     * @throws Horde_Exception
729     * @throws IMP_Compose_Exception
730     * @throws IMP_Compose_Exception_Address
731     * @throws IMP_Exception
732     */
733    public function buildAndSendMessage(
734        $body, $header, IMP_Prefs_Identity $identity, array $opts = array()
735    )
736    {
737        global $conf, $injector, $notification, $prefs, $registry, $session;
738
739        /* We need at least one recipient & RFC 2822 requires that no 8-bit
740         * characters can be in the address fields. */
741        $recip = $this->recipientList($header);
742        if (!count($recip['list'])) {
743            if ($recip['has_input']) {
744                throw new IMP_Compose_Exception(_("Invalid e-mail address."));
745            }
746            throw new IMP_Compose_Exception(_("Need at least one message recipient."));
747        }
748        $header = array_merge($header, $recip['header']);
749
750        /* Check for correct identity usage. */
751        if (!$this->getMetadata('identity_check') &&
752            (count($recip['list']) === 1)) {
753            $identity_search = $identity->getMatchingIdentity($recip['list'], false);
754            if (!is_null($identity_search) &&
755                ($identity->getDefault() != $identity_search)) {
756                $this->_setMetadata('identity_check', true);
757
758                $e = new IMP_Compose_Exception(_("Recipient address does not match the currently selected identity."));
759                $e->tied_identity = $identity_search;
760                throw $e;
761            }
762        }
763
764        /* Check body size of message. */
765        $imp_imap = $injector->getInstance('IMP_Factory_Imap')->create();
766        if (!$imp_imap->accessCompose(IMP_Imap::ACCESS_COMPOSE_BODYSIZE, strlen($body))) {
767            Horde::permissionDeniedError('imp', 'max_bodysize');
768            throw new IMP_Compose_Exception(sprintf(
769                _("Your message body has exceeded the limit by body size by %d characters."),
770                (strlen($body) - $imp_imap->max_compose_bodysize)
771            ));
772        }
773
774        $from = new Horde_Mail_Rfc822_Address($header['from']);
775        if (is_null($from->host)) {
776            $from->host = $imp_imap->config->maildomain;
777        }
778
779        /* Prepare the array of messages to send out.  May be more
780         * than one if we are encrypting for multiple recipients or
781         * are storing an encrypted message locally. */
782        $encrypt = empty($opts['encrypt']) ? 0 : $opts['encrypt'];
783        $send_msgs = array();
784        $msg_options = array(
785            'encrypt' => $encrypt,
786            'html' => !empty($opts['html']),
787            'identity' => $identity,
788            'pgp_attach_pubkey' => (!empty($opts['pgp_attach_pubkey']) && $prefs->getValue('use_pgp') && $prefs->getValue('pgp_public_key')),
789            'signature' => is_null($opts['signature']) ? $identity : $opts['signature'],
790            'vcard_attach' => ((!empty($opts['vcard_attach']) && $registry->hasMethod('contacts/ownVCard')) ? ((strlen($opts['vcard_attach']) ? $opts['vcard_attach'] : 'vcard') . '.vcf') : null)
791        );
792
793        /* Must encrypt & send the message one recipient at a time. */
794        if ($prefs->getValue('use_smime') &&
795            in_array($encrypt, array(IMP_Crypt_Smime::ENCRYPT, IMP_Crypt_Smime::SIGNENC))) {
796            foreach ($recip['list'] as $val) {
797                $list_ob = new Horde_Mail_Rfc822_List($val);
798                $send_msgs[] = array(
799                    'base' => $this->_createMimeMessage($list_ob, $body, $msg_options),
800                    'recipients' => $list_ob
801                );
802            }
803
804            /* Must target the encryption for the sender before saving message
805             * in sent-mail. */
806            $save_msg = $this->_createMimeMessage(IMP::parseAddressList($header['from']), $body, $msg_options);
807        } else {
808            /* Can send in clear-text all at once, or PGP can encrypt
809             * multiple addresses in the same message. */
810            $msg_options['from'] = $from;
811            $save_msg = $this->_createMimeMessage($recip['list'], $body, $msg_options);
812            $send_msgs[] = array(
813                'base' => $save_msg,
814                'recipients' => $recip['list']
815            );
816        }
817
818        /* Initalize a header object for the outgoing message. */
819        $headers = $this->_prepareHeaders($header, $opts);
820
821        /* Add a Received header for the hop from browser to server. */
822        $headers->addReceivedHeader(array(
823            'dns' => $injector->getInstance('Net_DNS2_Resolver'),
824            'server' => $conf['server']['name']
825        ));
826
827        /* Add Reply-To header. */
828        if (!empty($header['replyto']) &&
829            ($header['replyto'] != $from->bare_address)) {
830            $headers->addHeader('Reply-to', $header['replyto']);
831        }
832
833        /* Add the 'User-Agent' header. */
834        if (empty($opts['useragent'])) {
835            $headers->setUserAgent('Internet Messaging Program (IMP) ' . $registry->getVersion());
836        } else {
837            $headers->setUserAgent($opts['useragent']);
838        }
839        $headers->addUserAgentHeader();
840
841        /* Add preferred reply language(s). */
842        if ($lang = @unserialize($prefs->getValue('reply_lang'))) {
843            $headers->addHeader('Accept-Language', implode(',', $lang));
844        }
845
846        /* Send the messages out now. */
847        $sentmail = $injector->getInstance('IMP_Sentmail');
848
849        foreach ($send_msgs as $val) {
850            switch (intval($this->replyType(true))) {
851            case self::REPLY:
852                $senttype = IMP_Sentmail::REPLY;
853                break;
854
855            case self::FORWARD:
856                $senttype = IMP_Sentmail::FORWARD;
857                break;
858
859            case self::REDIRECT:
860                $senttype = IMP_Sentmail::REDIRECT;
861                break;
862
863            default:
864                $senttype = IMP_Sentmail::NEWMSG;
865                break;
866            }
867            $headers_copy = clone $headers;
868            try {
869                $this->_prepSendMessageAssert($val['recipients'], $headers_copy, $val['base']);
870                $this->sendMessage($val['recipients'], $headers_copy, $val['base']);
871
872                /* Store history information. */
873                $msg_id = new Horde_Mail_Rfc822_Identification(
874                    $headers_copy->getValue('message-id')
875                );
876                $sentmail->log(
877                    $senttype,
878                    reset($msg_id->ids),
879                    $val['recipients'],
880                    true
881                );
882            } catch (IMP_Compose_Exception_Address $e) {
883                throw $e;
884            } catch (IMP_Compose_Exception $e) {
885                /* Unsuccessful send. */
886                if ($e->log()) {
887                    $msg_id = new Horde_Mail_Rfc822_Identification(
888                        $headers_copy->getValue('message-id')
889                    );
890                    $sentmail->log(
891                        $senttype,
892                        reset($msg_id->ids),
893                        $val['recipients'],
894                        false
895                    );
896                }
897                throw new IMP_Compose_Exception(sprintf(_("There was an error sending your message: %s"), $e->getMessage()));
898            }
899        }
900
901        $recipients = strval($recip['list']);
902
903        if ($this->_replytype) {
904            /* Log the reply. */
905            if ($indices = $this->getMetadata('indices')) {
906                switch ($this->_replytype) {
907                case self::FORWARD:
908                case self::FORWARD_ATTACH:
909                case self::FORWARD_BODY:
910                case self::FORWARD_BOTH:
911                    $log = new IMP_Maillog_Log_Forward($recipients);
912                    break;
913
914                case self::REPLY:
915                case self::REPLY_SENDER:
916                    $log = new IMP_Maillog_Log_Reply();
917                    break;
918
919                case IMP_Compose::REPLY_ALL:
920                    $log = new IMP_Maillog_Log_Replyall();
921                    break;
922
923                case IMP_Compose::REPLY_LIST:
924                    $log = new IMP_Maillog_Log_Replylist();
925                    break;
926                }
927
928                $log_msgs = array();
929                foreach ($indices as $val) {
930                    foreach ($val->uids as $val2) {
931                        $log_msgs[] = new IMP_Maillog_Message(
932                            new IMP_Indices($val->mbox, $val2)
933                        );
934                    }
935                }
936
937                $injector->getInstance('IMP_Maillog')->log($log_msgs, $log);
938            }
939
940            $imp_message = $injector->getInstance('IMP_Message');
941            $reply_uid = new IMP_Indices($this);
942
943            switch ($this->replyType(true)) {
944            case self::FORWARD:
945                /* Set the Forwarded flag, if possible, in the mailbox.
946                 * See RFC 5550 [5.9] */
947                $imp_message->flag(array(
948                    'add' => array(Horde_Imap_Client::FLAG_FORWARDED)
949                ), $reply_uid);
950                break;
951
952            case self::REPLY:
953                /* Make sure to set the IMAP reply flag and unset any
954                 * 'flagged' flag. */
955                $imp_message->flag(array(
956                    'add' => array(Horde_Imap_Client::FLAG_ANSWERED),
957                    'remove' => array(Horde_Imap_Client::FLAG_FLAGGED)
958                ), $reply_uid);
959                break;
960            }
961        }
962
963        Horde::log(
964            sprintf(
965                "Message sent to %s from %s (%s)",
966                $recipients,
967                $registry->getAuth(),
968                $session->get('horde', 'auth/remoteAddr')
969            ),
970            'INFO'
971        );
972
973        /* Should we save this message in the sent mail mailbox? */
974        if (!empty($opts['sent_mail']) &&
975            ((!$prefs->isLocked('save_sent_mail') &&
976              !empty($opts['save_sent'])) ||
977             ($prefs->isLocked('save_sent_mail') &&
978              $prefs->getValue('save_sent_mail')))) {
979            /* Keep Bcc: headers on saved messages. */
980            if ((is_array($header['bcc']) || $header['bcc'] instanceof Countable) &&
981                count($header['bcc'])) {
982                $headers->addHeader('Bcc', $header['bcc']);
983            }
984
985            /* Strip attachments if requested. */
986            if (!empty($opts['strip_attachments'])) {
987                $save_msg->buildMimeIds();
988
989                /* Don't strip any part if this is a text message with both
990                 * plaintext and HTML representation, or a signed or encrypted
991                 * message. */
992                if ($save_msg->getType() != 'multipart/alternative' &&
993                    $save_msg->getType() != 'multipart/encrypted' &&
994                    $save_msg->getType() != 'multipart/signed') {
995                    for ($i = 2; ; ++$i) {
996                        if (!($oldPart = $save_msg->getPart($i))) {
997                            break;
998                        }
999
1000                        $replace_part = new Horde_Mime_Part();
1001                        $replace_part->setType('text/plain');
1002                        $replace_part->setCharset($this->charset);
1003                        $replace_part->setLanguage($GLOBALS['language']);
1004                        $replace_part->setContents('[' . _("Attachment stripped: Original attachment type") . ': "' . $oldPart->getType() . '", ' . _("name") . ': "' . $oldPart->getName(true) . '"]');
1005                        $save_msg->alterPart($i, $replace_part);
1006                    }
1007                }
1008            }
1009
1010            /* Generate the message string. */
1011            $fcc = $save_msg->toString(array(
1012                'defserver' => $imp_imap->config->maildomain,
1013                'headers' => $headers,
1014                'stream' => true
1015            ));
1016
1017            /* Make sure sent mailbox is created. */
1018            $sent_mail = IMP_Mailbox::get($opts['sent_mail']);
1019            $sent_mail->create();
1020
1021            $flags = array(
1022                Horde_Imap_Client::FLAG_SEEN,
1023                /* RFC 3503 [3.3] - MUST set MDNSent flag on sent message. */
1024                Horde_Imap_Client::FLAG_MDNSENT
1025            );
1026
1027            try {
1028                $imp_imap->append($sent_mail, array(array('data' => $fcc, 'flags' => $flags)));
1029            } catch (IMP_Imap_Exception $e) {
1030                $notification->push(sprintf(_("Message sent successfully, but not saved to %s."), $sent_mail->display));
1031            }
1032        }
1033
1034        /* Delete the attachment data. */
1035        $this->deleteAllAttachments();
1036
1037        /* Save recipients to address book? */
1038        $this->_saveRecipients($recip['list']);
1039
1040        /* Call post-sent hook. */
1041        try {
1042            $injector->getInstance('Horde_Core_Hooks')->callHook(
1043                'post_sent',
1044                'imp',
1045                array($save_msg['msg'], $headers)
1046            );
1047        } catch (Horde_Exception_HookNotSet $e) {}
1048    }
1049
1050    /**
1051     * Prepare header object with basic header fields and converts headers
1052     * to the current compose charset.
1053     *
1054     * @param array $headers  Array with 'from', 'to', 'cc', 'bcc', and
1055     *                        'subject' values.
1056     * @param array $opts     An array of options w/the following keys:
1057     *   - bcc: (boolean) Add BCC header to output.
1058     *   - priority: (string) The message priority ('high', 'normal', 'low').
1059     *
1060     * @return Horde_Mime_Headers  Headers object with the appropriate headers
1061     *                             set.
1062     */
1063    protected function _prepareHeaders($headers, array $opts = array())
1064    {
1065        $ob = new Horde_Mime_Headers();
1066
1067        $ob->addHeader('Date', date('r'));
1068        $ob->addMessageIdHeader();
1069
1070        if (isset($headers['from']) && strlen($headers['from'])) {
1071            $ob->addHeader('From', $headers['from']);
1072        }
1073
1074        if (isset($headers['to']) &&
1075            (is_object($headers['to']) || strlen($headers['to']))) {
1076            $ob->addHeader('To', $headers['to']);
1077        }
1078
1079        if (isset($headers['cc']) &&
1080            (is_object($headers['cc']) || strlen($headers['cc']))) {
1081            $ob->addHeader('Cc', $headers['cc']);
1082        }
1083
1084        if (!empty($opts['bcc']) &&
1085            isset($headers['bcc']) &&
1086            (is_object($headers['bcc']) || strlen($headers['bcc']))) {
1087            $ob->addHeader('Bcc', $headers['bcc']);
1088        }
1089
1090        if (isset($headers['subject']) && strlen($headers['subject'])) {
1091            $ob->addHeader('Subject', $headers['subject']);
1092        }
1093
1094        if ($this->replyType(true) == self::REPLY) {
1095            if ($refs = $this->getMetadata('references')) {
1096                $ob->addHeader('References', implode(' ', $refs));
1097            }
1098            if ($this->getMetadata('in_reply_to')) {
1099                $ob->addHeader('In-Reply-To', $this->getMetadata('in_reply_to'));
1100            }
1101        }
1102
1103        /* Add priority header, if requested. */
1104        if (!empty($opts['priority'])) {
1105            switch ($opts['priority']) {
1106            case 'high':
1107                $ob->addHeader('Importance', 'High');
1108                $ob->addHeader('X-Priority', '1 (Highest)');
1109                break;
1110
1111            case 'low':
1112                $ob->addHeader('Importance', 'Low');
1113                $ob->addHeader('X-Priority', '5 (Lowest)');
1114                break;
1115            }
1116        }
1117
1118        /* Add Return Receipt Headers. */
1119        if (!empty($opts['readreceipt'])) {
1120            $from = $ob->getOb('from');
1121            $from = $from[0];
1122            if (is_null($from->host)) {
1123                $from->host = $GLOBALS['injector']->getInstance('IMP_Factory_Imap')->create()->config->maildomain;
1124            }
1125
1126            $mdn = new Horde_Mime_Mdn($ob);
1127            $mdn->addMdnRequestHeaders($from);
1128        }
1129
1130        return $ob;
1131    }
1132
1133    /**
1134     * Sends a message.
1135     *
1136     * @param Horde_Mail_Rfc822_List $email  The e-mail list to send to.
1137     * @param Horde_Mime_Headers $headers    The object holding this message's
1138     *                                       headers.
1139     * @param Horde_Mime_Part $message       The object that contains the text
1140     *                                       to send.
1141     *
1142     * @throws IMP_Compose_Exception
1143     */
1144    public function sendMessage(Horde_Mail_Rfc822_List $email,
1145                                Horde_Mime_Headers $headers,
1146                                Horde_Mime_Part $message)
1147    {
1148        $email = $this->_prepSendMessage($email, $message);
1149
1150        $opts = array();
1151        if ($this->getMetadata('encrypt_sign')) {
1152            /* Signing requires that the body not be altered in transport. */
1153            $opts['encode'] = Horde_Mime_Part::ENCODE_7BIT;
1154        }
1155
1156        try {
1157            $message->send($email, $headers, $GLOBALS['injector']->getInstance('IMP_Mail'), $opts);
1158        } catch (Horde_Mime_Exception $e) {
1159            throw new IMP_Compose_Exception($e);
1160        }
1161    }
1162
1163    /**
1164     * Sanity checking/MIME formatting before sending a message.
1165     *
1166     * @param Horde_Mail_Rfc822_List $email  The e-mail list to send to.
1167     * @param Horde_Mime_Part $message       The object that contains the text
1168     *                                       to send.
1169     *
1170     * @return string  The encoded $email list.
1171     *
1172     * @throws IMP_Compose_Exception
1173     */
1174    protected function _prepSendMessage(Horde_Mail_Rfc822_List $email,
1175                                        $message = null)
1176    {
1177        /* Properly encode the addresses we're sending to. Always try
1178         * charset of original message as we know that the user can handle
1179         * that charset. */
1180        try {
1181            return $this->_prepSendMessageEncode($email, is_null($message) ? 'UTF-8' : $message->getHeaderCharset());
1182        } catch (IMP_Compose_Exception $e) {
1183            if (is_null($message)) {
1184                throw $e;
1185            }
1186        }
1187
1188        /* Fallback to UTF-8 (if replying, original message might be in
1189         * US-ASCII, for example, but To/Subject/Etc. may contain 8-bit
1190         * characters. */
1191        $message->setHeaderCharset('UTF-8');
1192        return $this->_prepSendMessageEncode($email, 'UTF-8');
1193    }
1194
1195    /**
1196     * Additonal checks to do if this is a user-generated compose message.
1197     *
1198     * @param Horde_Mail_Rfc822_List $email  The e-mail list to send to.
1199     * @param Horde_Mime_Headers $headers    The object holding this message's
1200     *                                       headers.
1201     * @param Horde_Mime_Part $message       The object that contains the text
1202     *                                       to send.
1203     *
1204     * @throws IMP_Compose_Exception
1205     */
1206    protected function _prepSendMessageAssert(Horde_Mail_Rfc822_List $email,
1207                                              Horde_Mime_Headers $headers = null,
1208                                              Horde_Mime_Part $message = null)
1209    {
1210        global $injector;
1211
1212        $email_count = count($email);
1213        $imp_imap = $injector->getInstance('IMP_Factory_Imap')->create();
1214
1215        if (!$imp_imap->accessCompose(IMP_Imap::ACCESS_COMPOSE_TIMELIMIT, $email_count)) {
1216            Horde::permissionDeniedError('imp', 'max_timelimit');
1217            throw new IMP_Compose_Exception(sprintf(
1218                ngettext(
1219                    "You are not allowed to send messages to more than %d recipient within %d hours.",
1220                    "You are not allowed to send messages to more than %d recipients within %d hours.",
1221                    $imp_imap->max_compose_timelimit
1222                ),
1223                $imp_imap->max_compose_timelimit,
1224                $injector->getInstance('IMP_Sentmail')->limit_period
1225            ));
1226        }
1227
1228        /* Count recipients if necessary. We need to split email groups
1229         * because the group members count as separate recipients. */
1230        if (!$imp_imap->accessCompose(IMP_Imap::ACCESS_COMPOSE_RECIPIENTS, $email_count)) {
1231            Horde::permissionDeniedError('imp', 'max_recipients');
1232            throw new IMP_Compose_Exception(sprintf(
1233                ngettext(
1234                    "You are not allowed to send messages to more than %d recipient.",
1235                    "You are not allowed to send messages to more than %d recipients.",
1236                    $imp_imap->max_compose_recipients
1237                ),
1238                $imp_imap->max_compose_recipients
1239            ));
1240        }
1241
1242        /* Pass to hook to allow alteration of message details. */
1243        if (!is_null($message)) {
1244            try {
1245                $injector->getInstance('Horde_Core_Hooks')->callHook(
1246                    'pre_sent',
1247                    'imp',
1248                    array($message, $headers, $this)
1249                );
1250            } catch (Horde_Exception_HookNotSet $e) {}
1251        }
1252    }
1253
1254    /**
1255     * Encode address and do sanity checking on encoded address.
1256     *
1257     * @param Horde_Mail_Rfc822_List $email  The e-mail list to send to.
1258     * @param string $charset                The charset to encode to.
1259     *
1260     * @return string  The encoded $email list.
1261     *
1262     * @throws IMP_Compose_Exception_Address
1263     */
1264    protected function _prepSendMessageEncode(Horde_Mail_Rfc822_List $email,
1265                                              $charset)
1266    {
1267        global $injector;
1268
1269        $exception = new IMP_Compose_Exception_Address();
1270        $hook = true;
1271        $out = array();
1272
1273        foreach ($email as $val) {
1274            /* $email contains address objects that already have the default
1275             * maildomain appended. Need to encode personal part and encode
1276             * IDN domain names. */
1277            try {
1278                $tmp = $val->writeAddress(array(
1279                    'encode' => $charset,
1280                    'idn' => true
1281                ));
1282
1283                /* We have written address, but it still may not be valid.
1284                 * So double-check. */
1285                $alist = IMP::parseAddressList($tmp, array(
1286                    'validate' => true
1287                ));
1288
1289                $error = null;
1290
1291                if ($hook) {
1292                    try {
1293                        $error = $injector->getInstance('Horde_Core_Hooks')->callHook(
1294                            'compose_addr',
1295                            'imp',
1296                            array($alist[0])
1297                        );
1298                    } catch (Horde_Exception_HookNotSet $e) {
1299                        $hook = false;
1300                    }
1301                }
1302            } catch (Horde_Idna_Exception $e) {
1303                $error = array(
1304                    'msg' => sprintf(_("Invalid e-mail address (%s): %s"), $val, $e->getMessage())
1305                );
1306            } catch (Horde_Mail_Exception $e) {
1307                $error = array(
1308                    'msg' => sprintf(_("Invalid e-mail address (%s)."), $val)
1309                );
1310            }
1311
1312            if (is_array($error)) {
1313                switch (isset($error['level']) ? $error['level'] : $exception::BAD) {
1314                case $exception::WARN:
1315                case 'warn':
1316                    if (($warn = $this->getMetadata('warn_addr')) &&
1317                        in_array(strval($val), $warn)) {
1318                        $out[] = $tmp;
1319                        continue 2;
1320                    }
1321                    $warn[] = strval($val);
1322                    $this->_setMetadata('warn_addr', $warn);
1323                    $this->changed = 'changed';
1324                    $level = $exception::WARN;
1325                    break;
1326
1327                default:
1328                    $level = $exception::BAD;
1329                    break;
1330                }
1331
1332                $exception->addAddress($val, $error['msg'], $level);
1333            } else {
1334                $out[] = $tmp;
1335            }
1336        }
1337
1338        if (count($exception)) {
1339            throw $exception;
1340        }
1341
1342        return implode(', ', $out);
1343    }
1344
1345    /**
1346     * Save the recipients done in a sendMessage().
1347     *
1348     * @param Horde_Mail_Rfc822_List $recipients  The list of recipients.
1349     */
1350    public function _saveRecipients(Horde_Mail_Rfc822_List $recipients)
1351    {
1352        global $notification, $prefs, $registry;
1353
1354        if (!$prefs->getValue('save_recipients') ||
1355            !$registry->hasMethod('contacts/import') ||
1356            !($abook = $prefs->getValue('add_source'))) {
1357            return;
1358        }
1359
1360        foreach ($recipients as $recipient) {
1361            $name = is_null($recipient->personal)
1362                ? $recipient->mailbox
1363                : $recipient->personal;
1364
1365            try {
1366                $registry->call(
1367                    'contacts/import',
1368                    array(
1369                        array('name' => $name, 'email' => $recipient->bare_address),
1370                        'array',
1371                        $abook,
1372                        array('match_on_email' => true)
1373                    )
1374                );
1375                $notification->push(sprintf(_("Entry \"%s\" was successfully added to the address book"), $name), 'horde.success');
1376            } catch (Turba_Exception_ObjectExists $e) {
1377            } catch (Horde_Exception $e) {
1378                if ($e->getCode() == 'horde.error') {
1379                    $notification->push($e, $e->getCode());
1380                }
1381            }
1382        }
1383    }
1384
1385    /**
1386     * Cleans up and returns the recipient list. Method designed to parse
1387     * user entered data; does not encode/validate addresses.
1388     *
1389     * @param array $hdr  An array of MIME headers and/or address list
1390     *                    objects. Recipients will be extracted from the 'to',
1391     *                    'cc', and 'bcc' entries.
1392     *
1393     * @return array  An array with the following entries:
1394     *   - has_input: (boolean) True if at least one of the headers contains
1395     *                user input.
1396     *   - header: (array) Contains the cleaned up 'to', 'cc', and 'bcc'
1397     *             address list (Horde_Mail_Rfc822_List objects).
1398     *   - list: (Horde_Mail_Rfc822_List) Recipient addresses.
1399     */
1400    public function recipientList($hdr)
1401    {
1402        $addrlist = new Horde_Mail_Rfc822_List();
1403        $has_input = false;
1404        $header = array();
1405
1406        foreach (array('to', 'cc', 'bcc') as $key) {
1407            if (isset($hdr[$key])) {
1408                $ob = IMP::parseAddressList($hdr[$key]);
1409                if (count($ob)) {
1410                    $addrlist->add($ob);
1411                    $header[$key] = $ob;
1412                    $has_input = true;
1413                } else {
1414                    $header[$key] = null;
1415                }
1416            }
1417        }
1418
1419        return array(
1420            'has_input' => $has_input,
1421            'header' => $header,
1422            'list' => $addrlist
1423        );
1424    }
1425
1426    /**
1427     * Create the base Horde_Mime_Part for sending.
1428     *
1429     * @param Horde_Mail_Rfc822_List $to  The recipient list.
1430     * @param string $body                Message body.
1431     * @param array $options              Additional options:
1432     *   - encrypt: (integer) The encryption flag.
1433     *   - from: (Horde_Mail_Rfc822_Address) The outgoing from address (only
1434     *           needed for multiple PGP encryption).
1435     *   - html: (boolean) Is this a HTML message?
1436     *   - identity: (IMP_Prefs_Identity) Identity of the sender.
1437     *   - nofinal: (boolean) This is not a message which will be sent out.
1438     *   - noattach: (boolean) Don't add attachment information.
1439     *   - pgp_attach_pubkey: (boolean) Attach the user's PGP public key?
1440     *   - signature: (IMP_Prefs_Identity|string) If set, add the signature to
1441     *                the message.
1442     *   - vcard_attach: (string) If set, attach user's vcard to message.
1443     *
1444     * @return Horde_Mime_Part  The MIME message to send.
1445     *
1446     * @throws Horde_Exception
1447     * @throws IMP_Compose_Exception
1448     */
1449    protected function _createMimeMessage(
1450        Horde_Mail_Rfc822_List $to, $body, array $options = array()
1451    )
1452    {
1453        global $conf, $injector, $prefs, $registry;
1454
1455        /* Get body text. */
1456        if (empty($options['html'])) {
1457            $body_html = null;
1458        } else {
1459            $tfilter = $injector->getInstance('Horde_Core_Factory_TextFilter');
1460
1461            $body_html = $tfilter->filter(
1462                $body,
1463                'Xss',
1464                array(
1465                    'return_dom' => true,
1466                    'strip_style_attributes' => false
1467                )
1468            );
1469            $body_html_body = $body_html->getBody();
1470
1471            $body = $tfilter->filter(
1472                $body_html->returnHtml(),
1473                'Html2text',
1474                array(
1475                    'width' => 0
1476                )
1477            );
1478        }
1479
1480        $hooks = $injector->getInstance('Horde_Core_Hooks');
1481
1482        /* We need to do the attachment check before any of the body text
1483         * has been altered. */
1484        if (!count($this) && !$this->getMetadata('attach_body_check')) {
1485            $this->_setMetadata('attach_body_check', true);
1486
1487            try {
1488                $check = $hooks->callHook(
1489                    'attach_body_check',
1490                    'imp',
1491                    array($body)
1492                );
1493            } catch (Horde_Exception_HookNotSet $e) {
1494                $check = array();
1495            }
1496
1497            if (!empty($check) &&
1498                preg_match('/\b(' . implode('|', array_map('preg_quote', $check)) . ')\b/i', $body, $matches)) {
1499                throw IMP_Compose_Exception::createAndLog('DEBUG', sprintf(_("Found the word %s in the message text although there are no files attached to the message. Did you forget to attach a file? (This check will not be performed again for this message.)"), $matches[0]));
1500            }
1501        }
1502
1503        /* Add signature data. */
1504        if (!empty($options['signature'])) {
1505            if (is_string($options['signature'])) {
1506                if (empty($options['html'])) {
1507                    $body .= "\n\n" . trim($options['signature']);
1508                } else {
1509                    $html_sig = trim($options['signature']);
1510                    $body .= "\n" . $tfilter->filter($html_sig, 'Html2text');
1511                }
1512            } else {
1513                $sig = $options['signature']->getSignature('text');
1514                $body .= $sig;
1515
1516                if (!empty($options['html'])) {
1517                    $html_sig = $options['signature']->getSignature('html');
1518                    if (!strlen($html_sig) && strlen($sig)) {
1519                        $html_sig = $this->text2html($sig);
1520                    }
1521                }
1522            }
1523
1524            if (!empty($options['html'])) {
1525                try {
1526                    $sig_ob = new IMP_Compose_HtmlSignature($html_sig);
1527                } catch (IMP_Exception $e) {
1528                    throw new IMP_Compose_Exception($e);
1529                }
1530
1531                foreach ($sig_ob->dom->getBody()->childNodes as $child) {
1532                    $body_html_body->appendChild(
1533                        $body_html->dom->importNode($child, true)
1534                    );
1535                }
1536            }
1537        }
1538
1539        /* Add linked attachments. */
1540        if (empty($options['nofinal'])) {
1541            $this->_linkAttachments($body, $body_html);
1542        }
1543
1544        /* Get trailer text (if any). */
1545        if (empty($options['nofinal'])) {
1546            try {
1547                $trailer = $hooks->callHook(
1548                    'trailer',
1549                    'imp',
1550                    array(false, $options['identity'], $to)
1551                );
1552                $html_trailer = $hooks->callHook(
1553                    'trailer',
1554                    'imp',
1555                    array(true, $options['identity'], $to)
1556                );
1557            } catch (Horde_Exception_HookNotSet $e) {
1558                $trailer = $html_trailer = null;
1559            }
1560
1561            $body .= strval($trailer);
1562
1563            if (!empty($options['html'])) {
1564                if (is_null($html_trailer) && strlen($trailer)) {
1565                    $html_trailer = $this->text2html($trailer);
1566                }
1567
1568                if (strlen($html_trailer)) {
1569                    $t_dom = new Horde_Domhtml($html_trailer, 'UTF-8');
1570                    foreach ($t_dom->getBody()->childNodes as $child) {
1571                        $body_html_body->appendChild($body_html->dom->importNode($child, true));
1572                    }
1573                }
1574            }
1575        }
1576
1577        /* Convert text to sending charset. HTML text will be converted
1578         * via Horde_Domhtml. */
1579        $body = Horde_String::convertCharset($body, 'UTF-8', $this->charset);
1580
1581        /* Set up the body part now. */
1582        $textBody = new Horde_Mime_Part();
1583        $textBody->setType('text/plain');
1584        $textBody->setCharset($this->charset);
1585        $textBody->setDisposition('inline');
1586
1587        /* Send in flowed format. */
1588        $flowed = new Horde_Text_Flowed($body, $this->charset);
1589        $flowed->setDelSp(true);
1590        $textBody->setContentTypeParameter('format', 'flowed');
1591        $textBody->setContentTypeParameter('DelSp', 'Yes');
1592        $text_contents = $flowed->toFlowed();
1593        $textBody->setContents($text_contents);
1594
1595        /* Determine whether or not to send a multipart/alternative
1596         * message with an HTML part. */
1597        if (!empty($options['html'])) {
1598            $htmlBody = new Horde_Mime_Part();
1599            $htmlBody->setType('text/html');
1600            $htmlBody->setCharset($this->charset);
1601            $htmlBody->setDisposition('inline');
1602            $htmlBody->setDescription(Horde_String::convertCharset(_("HTML Message"), 'UTF-8', $this->charset));
1603
1604            /* Add default font CSS information here. */
1605            $styles = array();
1606            if ($font_family = $prefs->getValue('compose_html_font_family')) {
1607                $styles[] = 'font-family:' . $font_family;
1608            }
1609            if ($font_size = intval($prefs->getValue('compose_html_font_size'))) {
1610                $styles[] = 'font-size:' . $font_size . 'px';
1611            }
1612
1613            if (!empty($styles)) {
1614                $body_html_body->setAttribute('style', implode(';', $styles));
1615            }
1616
1617            if (empty($options['nofinal'])) {
1618                $this->_cleanHtmlOutput($body_html);
1619            }
1620
1621            $to_add = $this->_convertToRelated($body_html, $htmlBody);
1622
1623            /* Now, all parts referred to in the HTML data have been added
1624             * to the attachment list. Convert to multipart/related if
1625             * this is the case. Exception: if text representation is empty,
1626             * just send HTML part. */
1627            if (strlen(trim($text_contents))) {
1628                $textpart = new Horde_Mime_Part();
1629                $textpart->setType('multipart/alternative');
1630                $textpart->addPart($textBody);
1631                $textpart->addPart($to_add);
1632                $textpart->setHeaderCharset($this->charset);
1633
1634                $textBody->setDescription(Horde_String::convertCharset(_("Plaintext Message"), 'UTF-8', $this->charset));
1635            } else {
1636                $textpart = $to_add;
1637            }
1638
1639            $htmlBody->setContents(
1640                $tfilter->filter(
1641                    $body_html->returnHtml(array(
1642                        'charset' => $this->charset,
1643                        'metacharset' => true
1644                    )),
1645                    'Cleanhtml',
1646                    array(
1647                        'charset' => $this->charset
1648                    )
1649                )
1650            );
1651        } else {
1652            $textpart = $textBody;
1653        }
1654
1655        /* Add attachments. */
1656        $base = $textpart;
1657        if (empty($options['noattach'])) {
1658            $parts = array();
1659
1660            foreach ($this as $val) {
1661                if (!$val->related && !$val->linked) {
1662                    $parts[] = $val->getPart(true);
1663                }
1664            }
1665
1666            if (!empty($options['pgp_attach_pubkey'])) {
1667                $parts[] = $injector->getInstance('IMP_Crypt_Pgp')->publicKeyMIMEPart();
1668            }
1669
1670            if (!empty($options['vcard_attach'])) {
1671                try {
1672                    $vpart = new Horde_Mime_Part();
1673                    $vpart->setType('text/x-vcard');
1674                    $vpart->setCharset('UTF-8');
1675                    $vpart->setContents($registry->call('contacts/ownVCard'));
1676                    $vpart->setName($options['vcard_attach']);
1677
1678                    $parts[] = $vpart;
1679                } catch (Horde_Exception $e) {
1680                    throw new IMP_Compose_Exception(sprintf(_("Can't attach contact information: %s"), $e->getMessage()));
1681                }
1682            }
1683
1684            if (!empty($parts)) {
1685                $base = new Horde_Mime_Part();
1686                $base->setType('multipart/mixed');
1687                $base->addPart($textpart);
1688                foreach ($parts as $val) {
1689                    $base->addPart($val);
1690                }
1691            }
1692        }
1693
1694        /* Set up the base message now. */
1695        $encrypt = empty($options['encrypt'])
1696            ? IMP::ENCRYPT_NONE
1697            : $options['encrypt'];
1698        if ($prefs->getValue('use_pgp') &&
1699            !empty($conf['gnupg']['path']) &&
1700            in_array($encrypt, array(IMP_Crypt_Pgp::ENCRYPT, IMP_Crypt_Pgp::SIGN, IMP_Crypt_Pgp::SIGNENC, IMP_Crypt_Pgp::SYM_ENCRYPT, IMP_Crypt_Pgp::SYM_SIGNENC))) {
1701            $imp_pgp = $injector->getInstance('IMP_Crypt_Pgp');
1702            $symmetric_passphrase = null;
1703
1704            switch ($encrypt) {
1705            case IMP_Crypt_Pgp::SIGN:
1706            case IMP_Crypt_Pgp::SIGNENC:
1707            case IMP_Crypt_Pgp::SYM_SIGNENC:
1708                /* Check to see if we have the user's passphrase yet. */
1709                $passphrase = $imp_pgp->getPassphrase('personal');
1710                if (empty($passphrase)) {
1711                    $e = new IMP_Compose_Exception(_("PGP: Need passphrase for personal private key."));
1712                    $e->encrypt = 'pgp_passphrase_dialog';
1713                    throw $e;
1714                }
1715                break;
1716
1717            case IMP_Crypt_Pgp::SYM_ENCRYPT:
1718            case IMP_Crypt_Pgp::SYM_SIGNENC:
1719                /* Check to see if we have the user's symmetric passphrase
1720                 * yet. */
1721                $symmetric_passphrase = $imp_pgp->getPassphrase('symmetric', 'imp_compose_' . $this->_cacheid);
1722                if (empty($symmetric_passphrase)) {
1723                    $e = new IMP_Compose_Exception(_("PGP: Need passphrase to encrypt your message with."));
1724                    $e->encrypt = 'pgp_symmetric_passphrase_dialog';
1725                    throw $e;
1726                }
1727                break;
1728            }
1729
1730            /* Do the encryption/signing requested. */
1731            try {
1732                switch ($encrypt) {
1733                case IMP_Crypt_Pgp::SIGN:
1734                    $base = $imp_pgp->impSignMimePart($base);
1735                    $this->_setMetadata('encrypt_sign', true);
1736                    break;
1737
1738                case IMP_Crypt_Pgp::ENCRYPT:
1739                case IMP_Crypt_Pgp::SYM_ENCRYPT:
1740                    $to_list = clone $to;
1741                    if (count($options['from'])) {
1742                        $to_list->add($options['from']);
1743                    }
1744                    $base = $imp_pgp->IMPencryptMIMEPart($base, $to_list, ($encrypt == IMP_Crypt_Pgp::SYM_ENCRYPT) ? $symmetric_passphrase : null);
1745                    break;
1746
1747                case IMP_Crypt_Pgp::SIGNENC:
1748                case IMP_Crypt_Pgp::SYM_SIGNENC:
1749                    $to_list = clone $to;
1750                    if (count($options['from'])) {
1751                        $to_list->add($options['from']);
1752                    }
1753                    $base = $imp_pgp->IMPsignAndEncryptMIMEPart($base, $to_list, ($encrypt == IMP_Crypt_Pgp::SYM_SIGNENC) ? $symmetric_passphrase : null);
1754                    break;
1755                }
1756            } catch (Horde_Exception $e) {
1757                throw new IMP_Compose_Exception(_("PGP Error: ") . $e->getMessage(), $e->getCode());
1758            }
1759        } elseif ($prefs->getValue('use_smime') &&
1760                  in_array($encrypt, array(IMP_Crypt_Smime::ENCRYPT, IMP_Crypt_Smime::SIGN, IMP_Crypt_Smime::SIGNENC))) {
1761            $imp_smime = $injector->getInstance('IMP_Crypt_Smime');
1762
1763            /* Check to see if we have the user's passphrase yet. */
1764            if (in_array($encrypt, array(IMP_Crypt_Smime::SIGN, IMP_Crypt_Smime::SIGNENC))) {
1765                $passphrase = $imp_smime->getPassphrase();
1766                if ($passphrase === false) {
1767                    $e = new IMP_Compose_Exception(_("S/MIME Error: Need passphrase for personal private key."));
1768                    $e->encrypt = 'smime_passphrase_dialog';
1769                    throw $e;
1770                }
1771            }
1772
1773            /* Do the encryption/signing requested. */
1774            try {
1775                switch ($encrypt) {
1776                case IMP_Crypt_Smime::SIGN:
1777                    $base = $imp_smime->IMPsignMIMEPart($base);
1778                    $this->_setMetadata('encrypt_sign', true);
1779                    break;
1780
1781                case IMP_Crypt_Smime::ENCRYPT:
1782                    $base = $imp_smime->IMPencryptMIMEPart($base, $to[0]);
1783                    break;
1784
1785                case IMP_Crypt_Smime::SIGNENC:
1786                    $base = $imp_smime->IMPsignAndEncryptMIMEPart($base, $to[0]);
1787                    break;
1788                }
1789            } catch (Horde_Exception $e) {
1790                throw new IMP_Compose_Exception(_("S/MIME Error: ") . $e->getMessage(), $e->getCode());
1791            }
1792        }
1793
1794        /* Flag this as the base part and rebuild MIME IDs. */
1795        $base->isBasePart(true);
1796        $base->buildMimeIds();
1797
1798        return $base;
1799    }
1800
1801    /**
1802     * Determines the reply text and headers for a message.
1803     *
1804     * @param integer $type           The reply type (self::REPLY* constant).
1805     * @param IMP_Contents $contents  An IMP_Contents object.
1806     * @param array $opts             Additional options:
1807     *   - format: (string) Force to this format.
1808     *             DEFAULT: Auto-determine.
1809     *   - to: (string) The recipient of the reply. Overrides the
1810     *         automatically determined value.
1811     *
1812     * @return array  An array with the following keys:
1813     *   - addr: (array) Address lists (to, cc, bcc; Horde_Mail_Rfc822_List
1814     *           objects).
1815     *   - body: (string) The text of the body part.
1816     *   - format: (string) The format of the body message (html, text).
1817     *   - identity: (integer) The identity to use for the reply based on the
1818     *               original message's addresses.
1819     *   - lang: (array) Language code (keys)/language name (values) of the
1820     *           original sender's preferred language(s).
1821     *   - reply_list_id: (string) List ID label.
1822     *   - reply_recip: (integer) Number of recipients in reply list.
1823     *   - subject: (string) Formatted subject.
1824     *   - type: (integer) The reply type used (either self::REPLY_ALL,
1825     *           self::REPLY_LIST, or self::REPLY_SENDER).
1826     * @throws IMP_Exception
1827     */
1828    public function replyMessage($type, $contents, array $opts = array())
1829    {
1830        global $injector, $language, $prefs;
1831
1832        if (!($contents instanceof IMP_Contents)) {
1833            throw new IMP_Exception(
1834                _("Could not retrieve message data from the mail server.")
1835            );
1836        }
1837
1838        $alist = new Horde_Mail_Rfc822_List();
1839        $addr = array(
1840            'to' => clone $alist,
1841            'cc' => clone $alist,
1842            'bcc' => clone $alist
1843        );
1844
1845        $h = $contents->getHeader();
1846        $match_identity = $this->_getMatchingIdentity($h);
1847        $reply_type = self::REPLY_SENDER;
1848
1849        if (!$this->_replytype) {
1850            $this->_setMetadata('indices', $contents->getIndicesOb());
1851
1852            /* Set the Message-ID related headers (RFC 5322 [3.6.4]). */
1853            $msg_id = new Horde_Mail_Rfc822_Identification(
1854                $h->getValue('message-id')
1855            );
1856            if (count($msg_id->ids)) {
1857                $this->_setMetadata('in_reply_to', reset($msg_id->ids));
1858            }
1859
1860            $ref_ob = new Horde_Mail_Rfc822_Identification(
1861                $h->getValue('references')
1862            );
1863            if (!count($ref_ob->ids)) {
1864                $ref_ob = new Horde_Mail_Rfc822_Identification(
1865                    $h->getValue('in-reply-to')
1866                );
1867                if (count($ref_ob->ids) > 1) {
1868                    $ref_ob->ids = array();
1869                }
1870            }
1871
1872            if (count($ref_ob->ids)) {
1873                $this->_setMetadata(
1874                    'references',
1875                    array_merge($ref_ob->ids, array(reset($msg_id->ids)))
1876                );
1877            }
1878        }
1879
1880        $subject = strlen($s = $h->getValue('subject'))
1881            ? 'Re: ' . strval(new Horde_Imap_Client_Data_BaseSubject($s, array('keepblob' => true)))
1882            : 'Re: ';
1883
1884        $force = false;
1885        if (in_array($type, array(self::REPLY_AUTO, self::REPLY_SENDER))) {
1886            if (isset($opts['to'])) {
1887                $addr['to']->add($opts['to']);
1888                $force = true;
1889            } elseif ($tmp = $h->getOb('reply-to')) {
1890                $addr['to']->add($tmp);
1891                $force = true;
1892            } else {
1893                $addr['to']->add($h->getOb('from'));
1894            }
1895        } elseif ($type === self::REPLY_ALL) {
1896            $force = isset($h['reply-to']);
1897        }
1898
1899        /* We might need $list_info in the reply_all section. */
1900        $list_info = in_array($type, array(self::REPLY_AUTO, self::REPLY_LIST))
1901            ? $injector->getInstance('IMP_Message_Ui')->getListInformation($h)
1902            : null;
1903
1904        if (!is_null($list_info) && !empty($list_info['reply_list'])) {
1905            /* If To/Reply-To and List-Reply address are the same, no need
1906             * to handle these address separately. */
1907            $rlist = new Horde_Mail_Rfc822_Address($list_info['reply_list']);
1908            if (!$rlist->match($addr['to'])) {
1909                $addr['to'] = clone $alist;
1910                $addr['to']->add($rlist);
1911                $reply_type = self::REPLY_LIST;
1912            }
1913        } elseif (in_array($type, array(self::REPLY_ALL, self::REPLY_AUTO))) {
1914            /* Clear the To field if we are auto-determining addresses. */
1915            if ($type == self::REPLY_AUTO) {
1916                $addr['to'] = clone $alist;
1917            }
1918
1919            /* Filter out our own address from the addresses we reply to. */
1920            $identity = $injector->getInstance('IMP_Identity');
1921            $all_addrs = $identity->getAllFromAddresses();
1922
1923            /* Build the To: header. It is either:
1924             * 1) the Reply-To address (if not a personal address)
1925             * 2) the From address(es) (if it doesn't contain a personal
1926             * address)
1927             * 3) all remaining Cc addresses. */
1928            $to_fields = array('from', 'reply-to');
1929
1930            foreach (array('reply-to', 'from', 'to', 'cc') as $val) {
1931                /* If either a reply-to or $to is present, we use this address
1932                 * INSTEAD of the from address. */
1933                if (($force && ($val == 'from')) ||
1934                    !($ob = $h->getOb($val))) {
1935                    continue;
1936                }
1937
1938                /* For From: need to check if at least one of the addresses is
1939                 * personal. */
1940                if ($val == 'from') {
1941                    foreach ($ob->raw_addresses as $addr_ob) {
1942                        if ($all_addrs->contains($addr_ob)) {
1943                            /* The from field contained a personal address.
1944                             * Use the 'To' header as the primary reply-to
1945                             * address instead. */
1946                            $to_fields[] = 'to';
1947
1948                            /* Add other non-personal from addresses to the
1949                             * list of CC addresses. */
1950                            $ob->setIteratorFilter($ob::BASE_ELEMENTS, $all_addrs);
1951                            $addr['cc']->add($ob);
1952                            $all_addrs->add($ob);
1953                            continue 2;
1954                        }
1955                    }
1956                }
1957
1958                $ob->setIteratorFilter($ob::BASE_ELEMENTS, $all_addrs);
1959
1960                foreach ($ob as $hdr_ob) {
1961                    if ($hdr_ob instanceof Horde_Mail_Rfc822_Group) {
1962                        $addr['cc']->add($hdr_ob);
1963                        $all_addrs->add($hdr_ob->addresses);
1964                    } elseif (($val != 'to') ||
1965                              is_null($list_info) ||
1966                              !$force ||
1967                              empty($list_info['exists'])) {
1968                        /* Don't add as To address if this is a list that
1969                         * doesn't have a post address but does have a
1970                         * reply-to address. */
1971                        if (in_array($val, $to_fields)) {
1972                            /* If from/reply-to doesn't have personal
1973                             * information, check from address. */
1974                            if (is_null($hdr_ob->personal) &&
1975                                ($to_ob = $h->getOb('from')) &&
1976                                !is_null($to_ob[0]->personal) &&
1977                                ($hdr_ob->match($to_ob[0]))) {
1978                                $addr['to']->add($to_ob);
1979                            } else {
1980                                $addr['to']->add($hdr_ob);
1981                            }
1982                        } else {
1983                            $addr['cc']->add($hdr_ob);
1984                        }
1985
1986                        $all_addrs->add($hdr_ob);
1987                    }
1988                }
1989            }
1990
1991            /* Build the Cc: (or possibly the To:) header. If this is a
1992             * reply to a message that was already replied to by the user,
1993             * this reply will go to the original recipients (Request
1994             * #8485).  */
1995            if (count($addr['cc'])) {
1996                $reply_type = self::REPLY_ALL;
1997            }
1998            if (!count($addr['to'])) {
1999                $addr['to'] = $addr['cc'];
2000                $addr['cc'] = clone $alist;
2001            }
2002
2003            /* Build the Bcc: header. */
2004            if ($bcc = $h->getOb('bcc')) {
2005                $bcc->add($identity->getBccAddresses());
2006                $bcc->setIteratorFilter(0, $all_addrs);
2007                foreach ($bcc as $val) {
2008                    $addr['bcc']->add($val);
2009                }
2010            }
2011        }
2012
2013        if (!$this->_replytype || ($reply_type != $this->_replytype)) {
2014            $this->_replytype = $reply_type;
2015            $this->changed = 'changed';
2016        }
2017
2018        $ret = $this->replyMessageText($contents, array(
2019            'format' => isset($opts['format']) ? $opts['format'] : null
2020        ));
2021        if ($prefs->getValue('reply_charset') &&
2022            ($ret['charset'] != $this->charset)) {
2023            $this->charset = $ret['charset'];
2024            $this->changed = 'changed';
2025        }
2026        unset($ret['charset']);
2027
2028        if ($type == self::REPLY_AUTO) {
2029            switch ($reply_type) {
2030            case self::REPLY_ALL:
2031                try {
2032                    $recip_list = $this->recipientList($addr);
2033                    $ret['reply_recip'] = count($recip_list['list']);
2034                } catch (IMP_Compose_Exception $e) {
2035                    $ret['reply_recip'] = 0;
2036                }
2037                break;
2038
2039            case self::REPLY_LIST:
2040                if (($list_parse = $injector->getInstance('Horde_ListHeaders')->parse('list-id', $h->getValue('list-id'))) &&
2041                    !is_null($list_parse->label)) {
2042                    $ret['reply_list_id'] = $list_parse->label;
2043                }
2044                break;
2045            }
2046        }
2047
2048        if (($lang = $h->getValue('accept-language')) ||
2049            ($lang = $h->getValue('x-accept-language'))) {
2050            $langs = array();
2051            foreach (explode(',', $lang) as $val) {
2052                if (($name = Horde_Nls::getLanguageISO($val)) !== null) {
2053                    $langs[trim($val)] = $name;
2054                }
2055            }
2056            $ret['lang'] = array_unique($langs);
2057
2058            /* Don't show display if original recipient is asking for reply in
2059             * the user's native language. */
2060            if ((count($ret['lang']) == 1) &&
2061                reset($ret['lang']) &&
2062                (substr(key($ret['lang']), 0, 2) == substr($language, 0, 2))) {
2063                unset($ret['lang']);
2064            }
2065        }
2066
2067        return array_merge(array(
2068            'addr' => $addr,
2069            'identity' => $match_identity,
2070            'subject' => $subject,
2071            'type' => $reply_type
2072        ), $ret);
2073    }
2074
2075    /**
2076     * Returns the reply text for a message.
2077     *
2078     * @param IMP_Contents $contents  An IMP_Contents object.
2079     * @param array $opts             Additional options:
2080     *   - format: (string) Force to this format.
2081     *             DEFAULT: Auto-determine.
2082     *
2083     * @return array  An array with the following keys:
2084     *   - body: (string) The text of the body part.
2085     *   - charset: (string) The guessed charset to use for the reply.
2086     *   - format: (string) The format of the body message ('html', 'text').
2087     */
2088    public function replyMessageText($contents, array $opts = array())
2089    {
2090        global $prefs;
2091
2092        if (!$prefs->getValue('reply_quote')) {
2093            return array(
2094                'body' => '',
2095                'charset' => '',
2096                'format' => 'text'
2097            );
2098        }
2099
2100        $h = $contents->getHeader();
2101
2102        $from = strval($h->getOb('from'));
2103
2104        if ($prefs->getValue('reply_headers') && !empty($h)) {
2105            $msg_pre = '----- ' .
2106                ($from ? sprintf(_("Message from %s"), $from) : _("Message")) .
2107                /* Extra '-'s line up with "End Message" below. */
2108                " ---------\n" .
2109                $this->_getMsgHeaders($h);
2110
2111            $msg_post = "\n\n----- " .
2112                ($from ? sprintf(_("End message from %s"), $from) : _("End message")) .
2113                " -----\n";
2114        } else {
2115            $msg_pre = strval(new IMP_Prefs_AttribText($from, $h));
2116            $msg_post = '';
2117        }
2118
2119        list($compose_html, $force_html) = $this->_msgTextFormat($opts, 'reply_format');
2120
2121        $msg_text = $this->_getMessageText($contents, array(
2122            'html' => $compose_html,
2123            'replylimit' => true,
2124            'toflowed' => true
2125        ));
2126
2127        if (!empty($msg_text) &&
2128            (($msg_text['mode'] == 'html') || $force_html)) {
2129            $msg = '<p>' . $this->text2html(trim($msg_pre)) . '</p>' .
2130                   self::HTML_BLOCKQUOTE .
2131                   (($msg_text['mode'] == 'text') ? $this->text2html($msg_text['flowed'] ? $msg_text['flowed'] : $msg_text['text']) : $msg_text['text']) .
2132                   '</blockquote><br />' .
2133                   ($msg_post ? $this->text2html($msg_post) : '') . '<br />';
2134            $msg_text['mode'] = 'html';
2135        } else {
2136            $msg = empty($msg_text['text'])
2137                ? '[' . _("No message body text") . ']'
2138                : $msg_pre . "\n\n" . $msg_text['text'] . $msg_post;
2139            $msg_text['mode'] = 'text';
2140        }
2141
2142        // Bug #10148: Message text might be us-ascii, but reply headers may
2143        // contain 8-bit characters.
2144        if (($msg_text['charset'] == 'us-ascii') &&
2145            (Horde_Mime::is8bit($msg_pre, 'UTF-8') ||
2146             Horde_Mime::is8bit($msg_post, 'UTF-8'))) {
2147            $msg_text['charset'] = 'UTF-8';
2148        }
2149
2150        return array(
2151            'body' => $msg . "\n",
2152            'charset' => $msg_text['charset'],
2153            'format' => $msg_text['mode']
2154        );
2155    }
2156
2157    /**
2158     * Determine text editor format.
2159     *
2160     * @param array $opts        Options (contains 'format' param).
2161     * @param string $pref_name  The pref name that controls formatting.
2162     *
2163     * @return array  Use HTML? and Force HTML?
2164     */
2165    protected function _msgTextFormat($opts, $pref_name)
2166    {
2167        if (!empty($opts['format'])) {
2168            $compose_html = $force_html = ($opts['format'] == 'html');
2169        } elseif ($GLOBALS['prefs']->getValue('compose_html')) {
2170            $compose_html = $force_html = true;
2171        } else {
2172            $compose_html = $GLOBALS['prefs']->getValue($pref_name);
2173            $force_html = false;
2174        }
2175
2176        return array($compose_html, $force_html);
2177    }
2178
2179    /**
2180     * Determine the text and headers for a forwarded message.
2181     *
2182     * @param integer $type           The forward type (self::FORWARD*
2183     *                                constant).
2184     * @param IMP_Contents $contents  An IMP_Contents object.
2185     * @param boolean $attach         Attach the forwarded message?
2186     * @param array $opts             Additional options:
2187     *   - format: (string) Force to this format.
2188     *             DEFAULT: Auto-determine.
2189     *
2190     * @return array  An array with the following keys:
2191     *   - attach: (boolean) True if original message was attached.
2192     *   - body: (string) The text of the body part.
2193     *   - format: (string) The format of the body message ('html', 'text').
2194     *   - identity: (mixed) See IMP_Prefs_Identity#getMatchingIdentity().
2195     *   - subject: (string) Formatted subject.
2196     *   - title: (string) Title to use on page.
2197     *   - type: (integer) - The compose type.
2198     * @throws IMP_Exception
2199     */
2200    public function forwardMessage($type, $contents, $attach = true,
2201                                   array $opts = array())
2202    {
2203        global $prefs;
2204
2205        if (!($contents instanceof IMP_Contents)) {
2206            throw new IMP_Exception(
2207                _("Could not retrieve message data from the mail server.")
2208            );
2209        }
2210
2211        if ($type == self::FORWARD_AUTO) {
2212            switch ($prefs->getValue('forward_default')) {
2213            case 'body':
2214                $type = self::FORWARD_BODY;
2215                break;
2216
2217            case 'both':
2218                $type = self::FORWARD_BOTH;
2219                break;
2220
2221            case 'editasnew':
2222                $ret = $this->editAsNew(new IMP_Indices($contents));
2223                $ret['title'] = _("New Message");
2224                return $ret;
2225
2226            case 'attach':
2227            default:
2228                $type = self::FORWARD_ATTACH;
2229                break;
2230            }
2231        }
2232
2233        $h = $contents->getHeader();
2234
2235        $this->_replytype = $type;
2236        $this->_setMetadata('indices', $contents->getIndicesOb());
2237
2238        if (strlen($s = $h->getValue('subject'))) {
2239            $s = strval(new Horde_Imap_Client_Data_BaseSubject($s, array(
2240                'keepblob' => true
2241            )));
2242            $subject = 'Fwd: ' . $s;
2243            $title = _("Forward") . ': ' . $s;
2244        } else {
2245            $subject = 'Fwd:';
2246            $title = _("Forward");
2247        }
2248
2249        $fwd_attach = false;
2250        if ($attach &&
2251            in_array($type, array(self::FORWARD_ATTACH, self::FORWARD_BOTH))) {
2252            try {
2253                $this->attachImapMessage(new IMP_Indices($contents));
2254                $fwd_attach = true;
2255            } catch (IMP_Exception $e) {}
2256        }
2257
2258        if (in_array($type, array(self::FORWARD_BODY, self::FORWARD_BOTH))) {
2259            $ret = $this->forwardMessageText($contents, array(
2260                'format' => isset($opts['format']) ? $opts['format'] : null
2261            ));
2262            unset($ret['charset']);
2263        } else {
2264            $ret = array(
2265                'body' => '',
2266                'format' => $prefs->getValue('compose_html') ? 'html' : 'text'
2267            );
2268        }
2269
2270        return array_merge(array(
2271            'attach' => $fwd_attach,
2272            'identity' => $this->_getMatchingIdentity($h),
2273            'subject' => $subject,
2274            'title' => $title,
2275            'type' => $type
2276        ), $ret);
2277    }
2278
2279    /**
2280     * Returns the forward text for a message.
2281     *
2282     * @param IMP_Contents $contents  An IMP_Contents object.
2283     * @param array $opts             Additional options:
2284     *   - format: (string) Force to this format.
2285     *             DEFAULT: Auto-determine.
2286     *
2287     * @return array  An array with the following keys:
2288     *   - body: (string) The text of the body part.
2289     *   - charset: (string) The guessed charset to use for the forward.
2290     *   - format: (string) The format of the body message ('html', 'text').
2291     */
2292    public function forwardMessageText($contents, array $opts = array())
2293    {
2294        $h = $contents->getHeader();
2295
2296        $from = strval($h->getOb('from'));
2297
2298        $msg_pre = "\n----- " .
2299            ($from ? sprintf(_("Forwarded message from %s"), $from) : _("Forwarded message")) .
2300            " -----\n" . $this->_getMsgHeaders($h) . "\n";
2301        $msg_post = "\n\n----- " . _("End forwarded message") . " -----\n";
2302
2303        list($compose_html, $force_html) = $this->_msgTextFormat($opts, 'forward_format');
2304
2305        $msg_text = $this->_getMessageText($contents, array(
2306            'html' => $compose_html
2307        ));
2308
2309        if (!empty($msg_text) &&
2310            (($msg_text['mode'] == 'html') || $force_html)) {
2311            $msg = $this->text2html($msg_pre) .
2312                (($msg_text['mode'] == 'text') ? $this->text2html($msg_text['text']) : $msg_text['text']) .
2313                $this->text2html($msg_post);
2314            $format = 'html';
2315        } else {
2316            $msg = $msg_pre . $msg_text['text'] . $msg_post;
2317            $format = 'text';
2318        }
2319
2320        // Bug #10148: Message text might be us-ascii, but forward headers may
2321        // contain 8-bit characters.
2322        if (($msg_text['charset'] == 'us-ascii') &&
2323            (Horde_Mime::is8bit($msg_pre, 'UTF-8') ||
2324             Horde_Mime::is8bit($msg_post, 'UTF-8'))) {
2325            $msg_text['charset'] = 'UTF-8';
2326        }
2327
2328        return array(
2329            'body' => $msg,
2330            'charset' => $msg_text['charset'],
2331            'format' => $format
2332        );
2333    }
2334
2335    /**
2336     * Prepares a forwarded message using multiple messages.
2337     *
2338     * @param IMP_Indices $indices  An indices object containing the indices
2339     *                              of the forwarded messages.
2340     *
2341     * @return array  An array with the following keys:
2342     *   - body: (string) The text of the body part.
2343     *   - format: (string) The format of the body message ('html', 'text').
2344     *   - identity: (mixed) See IMP_Prefs_Identity#getMatchingIdentity().
2345     *   - subject: (string) Formatted subject.
2346     *   - title: (string) Title to use on page.
2347     *   - type: (integer) The compose type.
2348     */
2349    public function forwardMultipleMessages(IMP_Indices $indices)
2350    {
2351        global $injector, $prefs, $session;
2352
2353        $this->_setMetadata('indices', $indices);
2354        $this->_replytype = self::FORWARD_ATTACH;
2355
2356        $subject = $this->attachImapMessage($indices);
2357
2358        return array(
2359            'body' => '',
2360            'format' => ($prefs->getValue('compose_html') && $session->get('imp', 'rteavail')) ? 'html' : 'text',
2361            'identity' => $injector->getInstance('IMP_Identity')->getDefault(),
2362            'subject' => $subject,
2363            'title' => $subject,
2364            'type' => self::FORWARD
2365        );
2366    }
2367
2368    /**
2369     * Prepare a redirect message.
2370     *
2371     * @param IMP_Indices $indices  An indices object.
2372     */
2373    public function redirectMessage(IMP_Indices $indices)
2374    {
2375        $this->_setMetadata('redirect_indices', $indices);
2376        $this->_replytype = self::REDIRECT;
2377    }
2378
2379    /**
2380     * Send a redirect (a/k/a resent) message. See RFC 5322 [3.6.6].
2381     *
2382     * @param mixed $to  The addresses to redirect to.
2383     * @param boolean $log  Whether to log the resending in the history and
2384     *                      sentmail log.
2385     *
2386     * @return array  An object with the following properties for each
2387     *                redirected message:
2388     *   - contents: (IMP_Contents) The contents object.
2389     *   - headers: (Horde_Mime_Headers) The header object.
2390     *   - mbox: (IMP_Mailbox) Mailbox of the message.
2391     *   - uid: (string) UID of the message.
2392     *
2393     * @throws IMP_Compose_Exception
2394     */
2395    public function sendRedirectMessage($to, $log = true)
2396    {
2397        global $injector, $registry;
2398
2399        $recip = $this->recipientList(array('to' => $to));
2400        if (!count($recip['list'])) {
2401            if ($recip['has_input']) {
2402                throw new IMP_Compose_Exception(_("Invalid e-mail address."));
2403            }
2404            throw new IMP_Compose_Exception(_("Need at least one message recipient."));
2405        }
2406
2407        $identity = $injector->getInstance('IMP_Identity');
2408        $from_addr = $identity->getFromAddress();
2409
2410        $out = array();
2411
2412        foreach ($this->getMetadata('redirect_indices') as $val) {
2413            foreach ($val->uids as $val2) {
2414                try {
2415                    $contents = $injector->getInstance('IMP_Factory_Contents')->create($val->mbox->getIndicesOb($val2));
2416                } catch (IMP_Exception $e) {
2417                    throw new IMP_Compose_Exception(_("Error when redirecting message."));
2418                }
2419
2420                $headers = $contents->getHeader();
2421
2422                /* We need to set the Return-Path header to the current user -
2423                 * see RFC 2821 [4.4]. */
2424                $headers->removeHeader('return-path');
2425                $headers->addHeader('Return-Path', $from_addr);
2426
2427                /* Generate the 'Resent' headers (RFC 5322 [3.6.6]). These
2428                 * headers are prepended to the message. */
2429                $resent_headers = new Horde_Mime_Headers();
2430                $resent_headers->addHeader('Resent-Date', date('r'));
2431                $resent_headers->addHeader('Resent-From', $from_addr);
2432                $resent_headers->addHeader('Resent-To', $recip['header']['to']);
2433                $resent_headers->addHeader('Resent-Message-ID', Horde_Mime::generateMessageId());
2434
2435                $header_text = trim($resent_headers->toString(array('encode' => 'UTF-8'))) . "\n" . trim($contents->getHeader(IMP_Contents::HEADER_TEXT));
2436
2437                $this->_prepSendMessageAssert($recip['list']);
2438                $to = $this->_prepSendMessage($recip['list']);
2439                $hdr_array = $headers->toArray(array('charset' => 'UTF-8'));
2440                $hdr_array['_raw'] = $header_text;
2441
2442                try {
2443                    $injector->getInstance('IMP_Mail')->send($to, $hdr_array, $contents->getBody());
2444                } catch (Horde_Mail_Exception $e) {
2445                    $e2 = new IMP_Compose_Exception($e);
2446
2447                    if (($prev = $e->getPrevious()) &&
2448                        ($prev instanceof Horde_Smtp_Exception)) {
2449                        Horde::log(
2450                            sprintf(
2451                                "SMTP Error: %s (%u; %s)",
2452                                $prev->raw_msg,
2453                                $prev->getCode(),
2454                                $prev->getEnhancedSmtpCode() ?: 'N/A'
2455                            ),
2456                            'ERR'
2457                        );
2458                        $e2->logged = true;
2459                    }
2460
2461                    throw $e2;
2462                }
2463
2464                $recipients = strval($recip['list']);
2465
2466                Horde::log(sprintf("%s Redirected message sent to %s from %s", $_SERVER['REMOTE_ADDR'], $recipients, $registry->getAuth()), 'INFO');
2467
2468                if ($log) {
2469                    /* Store history information. */
2470                    $msg_id = new Horde_Mail_Rfc822_Identification(
2471                        $headers->getValue('message-id')
2472                    );
2473
2474                    $injector->getInstance('IMP_Maillog')->log(
2475                        new IMP_Maillog_Message(reset($msg_id->ids)),
2476                        new IMP_Maillog_Log_Redirect($recipients)
2477                    );
2478
2479                    $injector->getInstance('IMP_Sentmail')->log(
2480                        IMP_Sentmail::REDIRECT,
2481                        reset($msg_id->ids),
2482                        $recipients
2483                    );
2484                }
2485
2486                $tmp = new stdClass;
2487                $tmp->contents = $contents;
2488                $tmp->headers = $headers;
2489                $tmp->mbox = $val->mbox;
2490                $tmp->uid = $val2;
2491
2492                $out[] = $tmp;
2493            }
2494        }
2495
2496        return $out;
2497    }
2498
2499    /**
2500     * Get "tieto" identity information.
2501     *
2502     * @param Horde_Mime_Headers $h  The headers object for the message.
2503     * @param array $only            Only use these headers.
2504     *
2505     * @return integer  The matching identity. If no exact match, returns the
2506     *                  default identity.
2507     */
2508    protected function _getMatchingIdentity($h, array $only = array())
2509    {
2510        global $injector;
2511
2512        $identity = $injector->getInstance('IMP_Identity');
2513        $msgAddresses = array();
2514        if (empty($only)) {
2515            /* Bug #9271: Check 'from' address first; if replying to a message
2516             * originally sent by user, this should be the identity used for
2517             * the reply also. */
2518            $only = array('from', 'to', 'cc', 'bcc');
2519        }
2520
2521        foreach ($only as $val) {
2522            $msgAddresses[] = $h->getValue($val);
2523        }
2524
2525        $match = $identity->getMatchingIdentity($msgAddresses);
2526
2527        return is_null($match)
2528            ? $identity->getDefault()
2529            : $match;
2530    }
2531
2532    /**
2533     * Add mail message(s) from the mail server as a message/rfc822
2534     * attachment.
2535     *
2536     * @param IMP_Indices $indices  An indices object.
2537     *
2538     * @return string  Subject string.
2539     *
2540     * @throws IMP_Exception
2541     */
2542    public function attachImapMessage($indices)
2543    {
2544        if (!count($indices)) {
2545            return false;
2546        }
2547
2548        $attached = 0;
2549        foreach ($indices as $ob) {
2550            foreach ($ob->uids as $idx) {
2551                ++$attached;
2552                $contents = $GLOBALS['injector']->getInstance('IMP_Factory_Contents')->create(new IMP_Indices($ob->mbox, $idx));
2553                $headerob = $contents->getHeader();
2554
2555                $part = new Horde_Mime_Part();
2556                $part->setCharset('UTF-8');
2557                $part->setType('message/rfc822');
2558                $part->setName(_("Forwarded Message"));
2559                $part->setContents($contents->fullMessageText(array(
2560                    'stream' => true
2561                )), array(
2562                    'usestream' => true
2563                ));
2564
2565                // Throws IMP_Compose_Exception.
2566                $this->addAttachmentFromPart($part);
2567
2568                $part->clearContents();
2569            }
2570        }
2571
2572        if ($attached > 1) {
2573            return 'Fwd: ' . sprintf(_("%u Forwarded Messages"), $attached);
2574        }
2575
2576        if ($name = $headerob->getValue('subject')) {
2577            $name = Horde_String::truncate($name, 80);
2578        } else {
2579            $name = _("[No Subject]");
2580        }
2581
2582        return 'Fwd: ' . strval(new Horde_Imap_Client_Data_BaseSubject($name, array('keepblob' => true)));
2583    }
2584
2585    /**
2586     * Determine the header information to display in the forward/reply.
2587     *
2588     * @param Horde_Mime_Headers $h  The headers object for the message.
2589     *
2590     * @return string  The header information for the original message.
2591     */
2592    protected function _getMsgHeaders($h)
2593    {
2594        $tmp = array();
2595
2596        if (($ob = $h->getValue('date'))) {
2597            $tmp[_("Date")] = $ob;
2598        }
2599
2600        if (($ob = strval($h->getOb('from')))) {
2601            $tmp[_("From")] = $ob;
2602        }
2603
2604        if (($ob = strval($h->getOb('reply-to')))) {
2605            $tmp[_("Reply-To")] = $ob;
2606        }
2607
2608        if (($ob = $h->getValue('subject'))) {
2609            $tmp[_("Subject")] = $ob;
2610        }
2611
2612        if (($ob = strval($h->getOb('to')))) {
2613            $tmp[_("To")] = $ob;
2614        }
2615
2616        if (($ob = strval($h->getOb('cc')))) {
2617            $tmp[_("Cc")] = $ob;
2618        }
2619
2620        $text = '';
2621
2622        if (!empty($tmp)) {
2623            $max = max(array_map(array('Horde_String', 'length'), array_keys($tmp))) + 2;
2624
2625            foreach ($tmp as $key => $val) {
2626                $text .= Horde_String::pad($key . ': ', $max, ' ', STR_PAD_LEFT) . $val . "\n";
2627            }
2628        }
2629
2630        return $text;
2631    }
2632
2633    /**
2634     * Add an attachment referred to in a related part.
2635     *
2636     * @param IMP_Compose_Attachment $act_ob  Attachment data.
2637     * @param DOMElement $node                Node element containg the
2638     *                                        related reference.
2639     * @param string $attribute               Element attribute containing the
2640     *                                        related reference.
2641     */
2642    public function addRelatedAttachment(IMP_Compose_Attachment $atc_ob,
2643                                         DOMElement $node, $attribute)
2644    {
2645        $atc_ob->related = true;
2646        $node->setAttribute(self::RELATED_ATTR, $attribute . ';' . $atc_ob->id);
2647    }
2648
2649    /**
2650     * Deletes all attachments.
2651     */
2652    public function deleteAllAttachments()
2653    {
2654        foreach (array_keys($this->_atc) as $key) {
2655            unset($this[$key]);
2656        }
2657    }
2658
2659    /**
2660     * Obtains the cache ID for the session object.
2661     *
2662     * @return string  The message cache ID.
2663     */
2664    public function getCacheId()
2665    {
2666        return $this->_cacheid;
2667    }
2668
2669    /**
2670     * Generate HMAC hash used to validate data on a session expiration. Uses
2671     * the unique compose cache ID of the expired message, the username, and
2672     * the secret key of the server to generate a reproducible value that can
2673     * be validated if session data doesn't exist.
2674     *
2675     * @param string $cacheid  The cache ID to use. If null, uses cache ID of
2676     *                         the compose object.
2677     * @param string $user     The user ID to use. If null, uses the current
2678     *                         authenticated username.
2679     *
2680     * @return string  The HMAC hash string.
2681     */
2682    public function getHmac($cacheid = null, $user = null)
2683    {
2684        global $conf, $registry;
2685
2686        return hash_hmac(
2687            'sha1',
2688            (is_null($cacheid) ? $this->getCacheId() : $cacheid) . '|' .
2689                (is_null($user) ? $registry->getAuth() : $user),
2690            $conf['secret_key']
2691        );
2692    }
2693
2694    /**
2695     * How many more attachments are allowed?
2696     *
2697     * @return mixed  Returns true if no attachment limit.
2698     *                Else returns the number of additional attachments
2699     *                allowed.
2700     */
2701    public function additionalAttachmentsAllowed()
2702    {
2703        global $conf;
2704
2705        return empty($conf['compose']['attach_count_limit'])
2706            ? true
2707            : ($conf['compose']['attach_count_limit'] - count($this));
2708    }
2709
2710    /**
2711     * What is the maximum attachment size?
2712     *
2713     * @return integer  The maximum attachment size (in bytes).
2714     */
2715    public function maxAttachmentSize()
2716    {
2717        $size = $GLOBALS['session']->get('imp', 'file_upload');
2718
2719        return empty($GLOBALS['conf']['compose']['attach_size_limit'])
2720            ? $size
2721            : min($size, $GLOBALS['conf']['compose']['attach_size_limit']);
2722    }
2723
2724    /**
2725     * Clean outgoing HTML (remove unexpected data URLs).
2726     *
2727     * @param Horde_Domhtml $html  The HTML data.
2728     */
2729    protected function _cleanHtmlOutput(Horde_Domhtml $html)
2730    {
2731        global $registry;
2732
2733        $xpath = new DOMXPath($html->dom);
2734
2735        foreach ($xpath->query('//*[@src]') as $node) {
2736            $src = $node->getAttribute('src');
2737
2738            /* Check for attempts to sneak data URL information into the
2739             * output. */
2740            if (Horde_Url_Data::isData($src)) {
2741                if (IMP_Compose_HtmlSignature::isSigImage($node, true)) {
2742                    /* This is HTML signature image data. Convert to an
2743                     * attachment. */
2744                    $sig_img = new Horde_Url_Data($src);
2745                    if ($sig_img->data) {
2746                        $data_part = new Horde_Mime_Part();
2747                        $data_part->setContents($sig_img->data);
2748                        $data_part->setType($sig_img->type);
2749
2750                        try {
2751                            $this->addRelatedAttachment(
2752                                $this->addAttachmentFromPart($data_part),
2753                                $node,
2754                                'src'
2755                            );
2756                        } catch (IMP_Compose_Exception $e) {
2757                            // Remove image on error.
2758                        }
2759                    }
2760                }
2761
2762                $node->removeAttribute('src');
2763            } elseif (strcasecmp($node->tagName, 'IMG') === 0) {
2764                /* Check for smileys. They live in the JS directory, under
2765                 * the base ckeditor directory, so search for that and replace
2766                 * with the filesystem information if found (Request
2767                 * #13051). Need to ignore other image links that may have
2768                 * been explicitly added by the user. */
2769                $js_path = strval(Horde::url($registry->get('jsuri', 'horde'), true));
2770                if (stripos($src, $js_path . '/ckeditor') === 0) {
2771                    $file = str_replace(
2772                        $js_path,
2773                        $registry->get('jsfs', 'horde'),
2774                        $src
2775                    );
2776
2777                    if (is_readable($file)) {
2778                        $data_part = new Horde_Mime_Part();
2779                        $data_part->setContents(file_get_contents($file));
2780                        $data_part->setName(basename($file));
2781
2782                        try {
2783                            $this->addRelatedAttachment(
2784                                $this->addAttachmentFromPart($data_part),
2785                                $node,
2786                                'src'
2787                            );
2788                        } catch (IMP_Compose_Exception $e) {
2789                            // Keep existing data on error.
2790                        }
2791                    }
2792                }
2793            }
2794        }
2795    }
2796
2797    /**
2798     * Converts an HTML part to a multipart/related part, if necessary.
2799     *
2800     * @param Horde_Domhtml $html    HTML data.
2801     * @param Horde_Mime_Part $part  The HTML part.
2802     *
2803     * @return Horde_Mime_Part  The part to add to the compose output.
2804     */
2805    protected function _convertToRelated(Horde_Domhtml $html,
2806                                         Horde_Mime_Part $part)
2807    {
2808        $r_part = false;
2809        foreach ($this as $atc) {
2810            if ($atc->related) {
2811                $r_part = true;
2812                break;
2813            }
2814        }
2815
2816        if (!$r_part) {
2817            return $part;
2818        }
2819
2820        /* Create new multipart/related part. */
2821        $related = new Horde_Mime_Part();
2822        $related->setType('multipart/related');
2823        /* Get the CID for the 'root' part. Although by default the first part
2824         * is the root part (RFC 2387 [3.2]), we may as well be explicit and
2825         * put the CID in the 'start' parameter. */
2826        $related->setContentTypeParameter('start', $part->setContentId());
2827        $related->addPart($part);
2828
2829        /* HTML iteration is from child->parent, so need to gather related
2830         * parts and add at end after sorting to generate a more sensible
2831         * attachment list. */
2832        $add = array();
2833
2834        foreach ($html as $node) {
2835            if (($node instanceof DOMElement) &&
2836                $node->hasAttribute(self::RELATED_ATTR)) {
2837                list($attr_name, $atc_id) = explode(';', $node->getAttribute(self::RELATED_ATTR));
2838
2839                /* If attachment can't be found, ignore. */
2840                if ($r_atc = $this[$atc_id]) {
2841                    if ($r_atc->linked) {
2842                        $attr = strval($r_atc->link_url);
2843                    } else {
2844                        $related_part = $r_atc->getPart(true);
2845                        $attr = 'cid:' . $related_part->setContentId();
2846                        $add[] = $related_part;
2847                    }
2848
2849                    $node->setAttribute($attr_name, $attr);
2850                }
2851
2852                $node->removeAttribute(self::RELATED_ATTR);
2853            }
2854        }
2855
2856        array_map(array($related, 'addPart'), array_reverse($add));
2857
2858        return $related;
2859    }
2860
2861    /**
2862     * Adds linked attachments to message.
2863     *
2864     * @param string &$body  Plaintext data.
2865     * @param mixed $html    HTML data (Horde_Domhtml) or null.
2866     *
2867     * @throws IMP_Compose_Exception
2868     */
2869    protected function _linkAttachments(&$body, $html)
2870    {
2871        global $conf;
2872
2873        if (empty($conf['compose']['link_attachments'])) {
2874            return;
2875        }
2876
2877        $link_all = false;
2878        $linked = array();
2879
2880        if (!empty($conf['compose']['link_attach_size_hard'])) {
2881            $limit = intval($conf['compose']['link_attach_size_hard']);
2882            foreach ($this as $val) {
2883                if (($limit -= $val->getPart()->getBytes()) < 0) {
2884                    $link_all = true;
2885                    break;
2886                }
2887            }
2888        }
2889
2890        foreach (iterator_to_array($this) as $key => $val) {
2891            if ($link_all && !$val->linked) {
2892                $val = new IMP_Compose_Attachment($this, $val->getPart(), $val->storage->getTempFile());
2893                $val->forceLinked = true;
2894                unset($this[$key]);
2895                $this[$key] = $val;
2896            }
2897
2898            if ($val->linked && !$val->related) {
2899                $linked[] = $val;
2900            }
2901        }
2902
2903        if (empty($linked)) {
2904            return;
2905        }
2906
2907        if ($del_time = IMP_Compose_LinkedAttachment::keepDate(false)) {
2908            /* Subtract 1 from time to get the last day of the previous
2909             * month. */
2910            $expire = ' (' . sprintf(_("links will expire on %s"), strftime('%x', $del_time - 1)) . ')';
2911        }
2912
2913        $body .= "\n-----\n" . _("Attachments") . $expire . ":\n";
2914        if ($html) {
2915            $body = $html->getBody();
2916            $dom = $html->dom;
2917
2918            $body->appendChild($dom->createElement('HR'));
2919            $body->appendChild($div = $dom->createElement('DIV'));
2920            $div->appendChild($dom->createElement('H4', _("Attachments") . $expire . ':'));
2921            $div->appendChild($ol = $dom->createElement('OL'));
2922        }
2923
2924        $i = 0;
2925        foreach ($linked as $val) {
2926            $apart = $val->getPart();
2927            $name = $apart->getName(true);
2928            $size = IMP::sizeFormat($apart->getBytes());
2929            $url = strval($val->link_url->setRaw(true));
2930
2931            $body .= "\n" . (++$i) . '. ' .
2932                $name . ' (' . $size . ') [' . $apart->getType() . "]\n" .
2933                sprintf(_("Download link: %s"), $url) . "\n";
2934
2935            if ($html) {
2936                $ol->appendChild($li = $dom->createElement('LI'));
2937                $li->appendChild($dom->createElement('STRONG', $name));
2938                $li->appendChild($dom->createTextNode(' (' . $size . ') [' . htmlspecialchars($apart->getType()) . ']'));
2939                $li->appendChild($dom->createElement('BR'));
2940                $li->appendChild($dom->createTextNode(_("Download link") . ': '));
2941                $li->appendChild($a = $dom->createElement('A', htmlspecialchars($url)));
2942                $a->setAttribute('href', $url);
2943            }
2944        }
2945    }
2946
2947    /**
2948     * Regenerates body text for use in the compose screen from IMAP data.
2949     *
2950     * @param IMP_Contents $contents  An IMP_Contents object.
2951     * @param array $options          Additional options:
2952     * <ul>
2953     *  <li>html: (boolean) Return text/html part, if available.</li>
2954     *  <li>imp_msg: (integer) If non-empty, the message data was created by
2955     *               IMP. Either:
2956     *   <ul>
2957     *    <li>self::COMPOSE</li>
2958     *    <li>self::FORWARD</li>
2959     *    <li>self::REPLY</li>
2960     *   </ul>
2961     *  </li>
2962     *  <li>replylimit: (boolean) Enforce length limits?</li>
2963     *  <li>toflowed: (boolean) Do flowed conversion?</li>
2964     * </ul>
2965     *
2966     * @return mixed  Null if bodypart not found, or array with the following
2967     *                keys:
2968     *   - charset: (string) The guessed charset to use.
2969     *   - flowed: (Horde_Text_Flowed) A flowed object, if the text is flowed.
2970     *             Otherwise, null.
2971     *   - id: (string) The MIME ID of the bodypart.
2972     *   - mode: (string) Either 'text' or 'html'.
2973     *   - text: (string) The body text.
2974     */
2975    protected function _getMessageText($contents, array $options = array())
2976    {
2977        global $conf, $injector, $notification, $prefs, $session;
2978
2979        $body_id = null;
2980        $mode = 'text';
2981        $options = array_merge(array(
2982            'imp_msg' => self::COMPOSE
2983        ), $options);
2984
2985        if (!empty($options['html']) &&
2986            $session->get('imp', 'rteavail') &&
2987            (($body_id = $contents->findBody('html')) !== null)) {
2988            $mime_message = $contents->getMIMEMessage();
2989
2990            switch ($mime_message->getPrimaryType()) {
2991            case 'multipart':
2992                if (($body_id != '1') &&
2993                    ($mime_message->getSubType() == 'mixed') &&
2994                    !Horde_Mime::isChild('1', $body_id)) {
2995                    $body_id = null;
2996                } else {
2997                    $mode = 'html';
2998                }
2999                break;
3000
3001            default:
3002                if (strval($body_id) != '1') {
3003                    $body_id = null;
3004                } else {
3005                    $mode = 'html';
3006                }
3007                break;
3008            }
3009        }
3010
3011        if (is_null($body_id)) {
3012            $body_id = $contents->findBody();
3013            if (is_null($body_id)) {
3014                return null;
3015            }
3016        }
3017
3018        $part = $contents->getMIMEPart($body_id);
3019        $type = $part->getType();
3020        $part_charset = $part->getCharset();
3021
3022        $msg = Horde_String::convertCharset($part->getContents(), $part_charset, 'UTF-8');
3023
3024        /* Enforce reply limits. */
3025        if (!empty($options['replylimit']) &&
3026            !empty($conf['compose']['reply_limit'])) {
3027            $limit = $conf['compose']['reply_limit'];
3028            if (Horde_String::length($msg) > $limit) {
3029                $msg = Horde_String::substr($msg, 0, $limit) . "\n" . _("[Truncated Text]");
3030            }
3031        }
3032
3033        if ($mode == 'html') {
3034            $dom = $injector->getInstance('Horde_Core_Factory_TextFilter')->filter(
3035                $msg,
3036                'Xss',
3037                array(
3038                    'charset' => $this->charset,
3039                    'return_dom' => true,
3040                    'strip_style_attributes' => false
3041                )
3042            );
3043
3044            /* If we are replying to a related part, and this part refers
3045             * to local message parts, we need to move those parts into this
3046             * message (since the original message may disappear during the
3047             * compose process). */
3048            if ($related_part = $contents->findMimeType($body_id, 'multipart/related')) {
3049                $this->_setMetadata('related_contents', $contents);
3050                $related_ob = new Horde_Mime_Related($related_part);
3051                $related_ob->cidReplace($dom, array($this, '_getMessageTextCallback'), $part_charset);
3052                $this->_setMetadata('related_contents', null);
3053            }
3054
3055            /* Convert any Data URLs to attachments. */
3056            $xpath = new DOMXPath($dom->dom);
3057            foreach ($xpath->query('//*[@src]') as $val) {
3058                $data_url = new Horde_Url_Data($val->getAttribute('src'));
3059                if (strlen($data_url->data)) {
3060                    $data_part = new Horde_Mime_Part();
3061                    $data_part->setContents($data_url->data);
3062                    $data_part->setType($data_url->type);
3063
3064                    try {
3065                        $atc = $this->addAttachmentFromPart($data_part);
3066                        $val->setAttribute('src', $atc->viewUrl());
3067                        $this->addRelatedAttachment($atc, $val, 'src');
3068                    } catch (IMP_Compose_Exception $e) {
3069                        $notification->push($e, 'horde.warning');
3070                    }
3071                }
3072            }
3073
3074            $msg = $dom->returnBody();
3075        } elseif ($type == 'text/html') {
3076            $msg = $injector->getInstance('Horde_Core_Factory_TextFilter')->filter($msg, 'Html2text');
3077            $type = 'text/plain';
3078        }
3079
3080        /* Always remove leading/trailing whitespace. The data in the
3081         * message body is not intended to be the exact representation of the
3082         * original message (use forward as message/rfc822 part for that). */
3083        $msg = trim($msg);
3084
3085        if ($type == 'text/plain') {
3086            if ($prefs->getValue('reply_strip_sig') &&
3087                (($pos = strrpos($msg, "\n-- ")) !== false)) {
3088                $msg = rtrim(substr($msg, 0, $pos));
3089            }
3090
3091            /* Remove PGP armored text. */
3092            $pgp = $injector->getInstance('Horde_Crypt_Pgp_Parse')->parseToPart($msg);
3093            if (!is_null($pgp)) {
3094                $msg = '';
3095                $pgp->buildMimeIds();
3096                foreach ($pgp->contentTypeMap() as $key => $val) {
3097                    if (strpos($val, 'text/') === 0) {
3098                        $msg .= $pgp[$key]->getContents();
3099                    }
3100                }
3101            }
3102
3103            if ($part->getContentTypeParameter('format') == 'flowed') {
3104                $flowed = new Horde_Text_Flowed($msg, 'UTF-8');
3105                if (Horde_String::lower($part->getContentTypeParameter('delsp')) == 'yes') {
3106                    $flowed->setDelSp(true);
3107                }
3108                $flowed->setMaxLength(0);
3109                $msg = $flowed->toFixed(false);
3110            } else {
3111                /* If the input is *not* in flowed format, make sure there is
3112                 * no padding at the end of lines. */
3113                $msg = preg_replace("/\s*\n/U", "\n", $msg);
3114            }
3115
3116            if (isset($options['toflowed'])) {
3117                $flowed = new Horde_Text_Flowed($msg, 'UTF-8');
3118                $msg = $options['toflowed']
3119                    ? $flowed->toFlowed(true)
3120                    : $flowed->toFlowed(false, array('nowrap' => true));
3121            }
3122        }
3123
3124        if (strcasecmp($part->getCharset(), 'windows-1252') === 0) {
3125            $part_charset = 'ISO-8859-1';
3126        }
3127
3128        return array(
3129            'charset' => $part_charset,
3130            'flowed' => isset($flowed) ? $flowed : null,
3131            'id' => $body_id,
3132            'mode' => $mode,
3133            'text' => $msg
3134        );
3135    }
3136
3137    /**
3138     * Callback used in _getMessageText().
3139     *
3140     * @return Horde_Url
3141     */
3142    public function _getMessageTextCallback($id, $attribute, $node)
3143    {
3144        $atc = $this->addAttachmentFromPart($this->getMetadata('related_contents')->getMIMEPart($id));
3145        $this->addRelatedAttachment($atc, $node, $attribute);
3146
3147        return $atc->viewUrl();
3148    }
3149
3150    /**
3151     * Adds an attachment from Horde_Mime_Part data.
3152     *
3153     * @param Horde_Mime_Part $part  The object that contains the attachment
3154     *                               data.
3155     *
3156     * @return IMP_Compose_Attachment  Attachment object.
3157     * @throws IMP_Compose_Exception
3158     */
3159    public function addAttachmentFromPart($part)
3160    {
3161        /* Extract the data from the Horde_Mime_Part. */
3162        $atc_file = Horde::getTempFile('impatt');
3163        $stream = $part->getContents(array(
3164            'stream' => true
3165        ));
3166        rewind($stream);
3167        $dest_handle = fopen($atc_file, 'w+b');
3168        while (!feof($stream)) {
3169            fwrite($dest_handle, fread($stream, 1024));
3170        }
3171        fclose($dest_handle);
3172        $size = ftell($stream);
3173        if ($size === false) {
3174            throw new IMP_Compose_Exception(sprintf(_("Could not attach %s to the message."), $part->getName()));
3175        }
3176
3177        return $this->_addAttachment(
3178            $atc_file,
3179            $size,
3180            $part->getName(true),
3181            $part->getType()
3182        );
3183    }
3184
3185    /**
3186     * Add attachment from uploaded (form) data.
3187     *
3188     * @param string $field  The form field name.
3189     *
3190     * @return array  A list of IMP_Compose_Attachment objects (if
3191     *                successfully attached) or IMP_Compose_Exception objects
3192     *                (if error when attaching).
3193     * @throws IMP_Compose_Exception
3194     */
3195    public function addAttachmentFromUpload($field)
3196    {
3197        global $browser;
3198
3199        try {
3200            $browser->wasFileUploaded($field, _("attachment"));
3201        } catch (Horde_Browser_Exception $e) {
3202            throw new IMP_Compose_Exception($e);
3203        }
3204
3205        $finfo = array();
3206        if (is_array($_FILES[$field]['size'])) {
3207            for ($i = 0; $i < count($_FILES[$field]['size']); ++$i) {
3208                $tmp = array();
3209                foreach ($_FILES[$field] as $key => $val) {
3210                    $tmp[$key] = $val[$i];
3211                }
3212                $finfo[] = $tmp;
3213            }
3214        } else {
3215            $finfo[] = $_FILES[$field];
3216        }
3217
3218        $out = array();
3219
3220        foreach ($finfo as $val) {
3221            switch (empty($val['type']) ? $val['type'] : '') {
3222            case 'application/unknown':
3223            case '':
3224                $type = 'application/octet-stream';
3225                break;
3226
3227            default:
3228                $type = $val['type'];
3229                break;
3230            }
3231
3232            try {
3233                $out[] = $this->_addAttachment(
3234                    $val['tmp_name'],
3235                    $val['size'],
3236                    Horde_Util::dispelMagicQuotes($val['name']),
3237                    $type
3238                );
3239            } catch (IMP_Compose_Exception $e) {
3240                $out[] = $e;
3241            }
3242        }
3243
3244        return $out;
3245    }
3246
3247    /**
3248     * Adds an attachment to the outgoing compose message.
3249     *
3250     * @param string $atc_file  Temporary file containing attachment contents.
3251     * @param integer $bytes    Size of data, in bytes.
3252     * @param string $filename  Filename of data.
3253     * @param string $type      MIME type of data.
3254     *
3255     * @return IMP_Compose_Attachment  Attachment object.
3256     * @throws IMP_Compose_Exception
3257     */
3258    protected function _addAttachment($atc_file, $bytes, $filename, $type)
3259    {
3260        global $conf, $injector;
3261
3262        $atc = new Horde_Mime_Part();
3263        $atc->setBytes($bytes);
3264
3265        /* Try to determine the MIME type from 1) the extension and
3266         * then 2) analysis of the file (if available). */
3267        if (strlen($filename)) {
3268            $atc->setName($filename);
3269            if ($type == 'application/octet-stream') {
3270                $type = Horde_Mime_Magic::filenameToMIME($filename, false);
3271            }
3272        }
3273
3274        $atc->setType($type);
3275        $atc->setHeaderCharset('UTF-8');
3276
3277        if (($atc->getType() == 'application/octet-stream') ||
3278            ($atc->getPrimaryType() == 'text')) {
3279            $analyze = Horde_Mime_Magic::analyzeFile($atc_file, empty($conf['mime']['magic_db']) ? null : $conf['mime']['magic_db'], array(
3280                'nostrip' => true
3281            ));
3282
3283            if ($analyze) {
3284                $analyze = Horde_Mime::decodeParam('Content-Type', $analyze);
3285                $atc->setType($analyze['val']);
3286                $atc->setCharset(isset($analyze['params']['charset']) ? $analyze['params']['charset'] : 'UTF-8');
3287            } else {
3288                $atc->setCharset('UTF-8');
3289            }
3290        }
3291
3292        $atc_ob = new IMP_Compose_Attachment($this, $atc, $atc_file);
3293
3294        /* Check for attachment size limitations. */
3295        $size_limit = null;
3296        if ($atc_ob->linked) {
3297            if (!empty($conf['compose']['link_attach_size_limit'])) {
3298                $linked = true;
3299                $size_limit = 'link_attach_size_limit';
3300            }
3301        } elseif (!empty($conf['compose']['attach_size_limit'])) {
3302            $linked = false;
3303            $size_limit = 'attach_size_limit';
3304        }
3305
3306        if (!is_null($size_limit)) {
3307            $total_size = $conf['compose'][$size_limit] - $bytes;
3308            foreach ($this as $val) {
3309                if ($val->linked == $linked) {
3310                    $total_size -= $val->getPart()->getBytes();
3311                }
3312            }
3313
3314            if ($total_size < 0) {
3315                throw new IMP_Compose_Exception(strlen($filename) ? sprintf(_("Attached file \"%s\" exceeds the attachment size limits. File NOT attached."), $filename) : _("Attached file exceeds the attachment size limits. File NOT attached."));
3316            }
3317        }
3318
3319        try {
3320            $injector->getInstance('Horde_Core_Hooks')->callHook(
3321                'compose_attachment',
3322                'imp',
3323                array($atc_ob)
3324            );
3325        } catch (Horde_Exception_HookNotSet $e) {}
3326
3327        $this->_atc[$atc_ob->id] = $atc_ob;
3328        $this->changed = 'changed';
3329
3330        return $atc_ob;
3331    }
3332
3333    /**
3334     * Store draft compose data if session expires.
3335     *
3336     * @param Horde_Variables $vars  Object with the form data.
3337     */
3338    public function sessionExpireDraft(Horde_Variables $vars)
3339    {
3340        global $conf, $injector;
3341
3342        if (empty($conf['compose']['use_vfs']) ||
3343            !isset($vars->composeCache) ||
3344            !isset($vars->composeHmac) ||
3345            !isset($vars->user) ||
3346            ($this->getHmac($vars->composeCache, $vars->user) != $vars->composeHmac)) {
3347            return;
3348        }
3349
3350        $headers = array();
3351        foreach (array('to', 'cc', 'bcc', 'subject') as $val) {
3352            $headers[$val] = $vars->$val;
3353        }
3354
3355        try {
3356            $body = $this->_saveDraftMsg($headers, $vars->message, array(
3357                'html' => $vars->rtemode,
3358                'priority' => $vars->priority,
3359                'readreceipt' => $vars->request_read_receipt
3360            ));
3361
3362            $injector->getInstance('Horde_Core_Factory_Vfs')->create()->writeData(self::VFS_DRAFTS_PATH, hash('sha1', $vars->user), $body, true);
3363        } catch (Exception $e) {}
3364    }
3365
3366    /**
3367     * Restore session expiration draft compose data.
3368     */
3369    public function recoverSessionExpireDraft()
3370    {
3371        global $conf, $injector, $notification;
3372
3373        if (empty($conf['compose']['use_vfs'])) {
3374            return;
3375        }
3376
3377        $filename = hash('sha1', $GLOBALS['registry']->getAuth());
3378
3379        try {
3380            $vfs = $injector->getInstance('Horde_Core_Factory_Vfs')->create();
3381
3382            if ($vfs->exists(self::VFS_DRAFTS_PATH, $filename)) {
3383                $data = $vfs->read(self::VFS_DRAFTS_PATH, $filename);
3384                $this->_saveDraftServer($data);
3385                $vfs->deleteFile(self::VFS_DRAFTS_PATH, $filename);
3386                $notification->push(
3387                    _("A message you were composing when your session expired has been recovered. You may resume composing your message by going to your Drafts mailbox."),
3388                    'horde.message',
3389                    array('sticky')
3390                );
3391            }
3392        } catch (Exception $e) {}
3393    }
3394
3395    /**
3396     * If this object contains sufficient metadata, return an IMP_Contents
3397     * object reflecting that metadata.
3398     *
3399     * @return mixed  Either an IMP_Contents object or null.
3400     */
3401    public function getContentsOb()
3402    {
3403        return ($this->_replytype && ($indices = $this->getMetadata('indices')) && (count($indices) === 1))
3404            ? $GLOBALS['injector']->getInstance('IMP_Factory_Contents')->create($indices)
3405            : null;
3406    }
3407
3408    /**
3409     * Return the reply type.
3410     *
3411     * @param boolean $base  Return the base reply type?
3412     *
3413     * @return string  The reply type, or null if not a reply.
3414     */
3415    public function replyType($base = false)
3416    {
3417        switch ($this->_replytype) {
3418        case self::FORWARD:
3419        case self::FORWARD_ATTACH:
3420        case self::FORWARD_BODY:
3421        case self::FORWARD_BOTH:
3422            return $base
3423                ? self::FORWARD
3424                : $this->_replytype;
3425
3426        case self::REPLY:
3427        case self::REPLY_ALL:
3428        case self::REPLY_LIST:
3429        case self::REPLY_SENDER:
3430            return $base
3431                ? self::REPLY
3432                : $this->_replytype;
3433
3434        case self::REDIRECT:
3435            return $this->_replytype;
3436
3437        default:
3438            return null;
3439        }
3440    }
3441
3442    /* Static methods. */
3443
3444    /**
3445     * Is composing messages allowed?
3446     *
3447     * @return boolean  True if compose allowed.
3448     * @throws Horde_Exception
3449     */
3450    static public function canCompose()
3451    {
3452        try {
3453            return !$GLOBALS['injector']->getInstance('Horde_Core_Hooks')->callHook('disable_compose', 'imp');
3454        } catch (Horde_Exception_HookNotSet $e) {
3455            return true;
3456        }
3457    }
3458
3459    /**
3460     * Can attachments be uploaded?
3461     *
3462     * @return boolean  True if attachments can be uploaded.
3463     */
3464    static public function canUploadAttachment()
3465    {
3466        return ($GLOBALS['session']->get('imp', 'file_upload') != 0);
3467    }
3468
3469    /**
3470     * Shortcut function to convert text -> HTML for purposes of composition.
3471     *
3472     * @param string $msg  The message text.
3473     *
3474     * @return string  HTML text.
3475     */
3476    static public function text2html($msg)
3477    {
3478        return $GLOBALS['injector']->getInstance('Horde_Core_Factory_TextFilter')->filter($msg, 'Text2html', array(
3479            'always_mailto' => true,
3480            'flowed' => self::HTML_BLOCKQUOTE,
3481            'parselevel' => Horde_Text_Filter_Text2html::MICRO
3482        ));
3483    }
3484
3485    /* ArrayAccess methods. */
3486
3487    public function offsetExists($offset)
3488    {
3489        return isset($this->_atc[$offset]);
3490    }
3491
3492    public function offsetGet($offset)
3493    {
3494        return isset($this->_atc[$offset])
3495            ? $this->_atc[$offset]
3496            : null;
3497    }
3498
3499    public function offsetSet($offset, $value)
3500    {
3501        $this->_atc[$offset] = $value;
3502        $this->changed = 'changed';
3503    }
3504
3505    public function offsetUnset($offset)
3506    {
3507        if (($atc = $this->_atc[$offset]) === null) {
3508            return;
3509        }
3510
3511        $atc->delete();
3512        unset($this->_atc[$offset]);
3513
3514        $this->changed = 'changed';
3515    }
3516
3517    /* Magic methods. */
3518
3519    /**
3520     * String representation: the cache ID.
3521     */
3522    public function __toString()
3523    {
3524        return $this->getCacheId();
3525    }
3526
3527    /* Countable method. */
3528
3529    /**
3530     * Returns the number of attachments currently in this message.
3531     *
3532     * @return integer  The number of attachments in this message.
3533     */
3534    public function count()
3535    {
3536        return count($this->_atc);
3537    }
3538
3539    /* IteratorAggregate method. */
3540
3541    /**
3542     */
3543    public function getIterator()
3544    {
3545        return new ArrayIterator($this->_atc);
3546    }
3547
3548}
3549