<?php
/**
 * Copyright 2002-2017 Horde LLC (http://www.horde.org/)
 *
 * See the enclosed file COPYING for license information (GPL). If you
 * did not receive this file, see http://www.horde.org/licenses/gpl.
 *
 * @category  Horde
 * @copyright 2002-2017 Horde LLC
 * @license   http://www.horde.org/licenses/gpl GPL
 * @package   IMP
 */

/**
 * The IMP_Compose:: class represents an outgoing mail message.
 *
 * @author    Michael Slusarz <slusarz@horde.org>
 * @category  Horde
 * @copyright 2002-2017 Horde LLC
 * @license   http://www.horde.org/licenses/gpl GPL
 * @package   IMP
 */
class IMP_Compose implements ArrayAccess, Countable, IteratorAggregate
{
    /* The virtual path to save drafts. */
    const VFS_DRAFTS_PATH = '.horde/imp/drafts';

    /* Compose types. */
    const COMPOSE = 0;
    const REPLY = 1;
    const REPLY_ALL = 2;
    const REPLY_AUTO = 3;
    const REPLY_LIST = 4;
    const REPLY_SENDER = 5;
    const FORWARD = 6;
    const FORWARD_ATTACH = 7;
    const FORWARD_AUTO = 8;
    const FORWARD_BODY = 9;
    const FORWARD_BOTH = 10;
    const REDIRECT = 11;
    const EDITASNEW = 12;
    const TEMPLATE = 13;

    /* Related part attribute name. */
    const RELATED_ATTR = 'imp_related_attr';

    /* The blockquote tag to use to indicate quoted text in HTML data. */
    const HTML_BLOCKQUOTE = '<blockquote type="cite" style="border-left:2px solid blue;margin-left:2px;padding-left:12px;">';

    /**
     * Attachment ID counter.
     *
     * @var integer
     */
    public $atcId = 0;

    /**
     * Mark as changed for purposes of storing in the session.
     * Either empty, 'changed', or 'deleted'.
     *
     * @var string
     */
    public $changed = '';

    /**
     * The charset to use for sending.
     *
     * @var string
     */
    public $charset;

    /**
     * Attachment data.
     *
     * @var array
     */
    protected $_atc = array();

    /**
     * The cache ID used to store object in session.
     *
     * @var string
     */
    protected $_cacheid;

    /**
     * Various metadata for this message.
     *
     * @var array
     */
    protected $_metadata = array();

    /**
     * The reply type.
     *
     * @var integer
     */
    protected $_replytype = self::COMPOSE;

    /**
     * Constructor.
     *
     * @param string $cacheid  The cache ID string.
     */
    public function __construct($cacheid)
    {
        $this->_cacheid = $cacheid;
        $this->charset = $GLOBALS['registry']->getEmailCharset();
    }

    /**
     * Tasks to do upon unserialize().
     */
    public function __wakeup()
    {
        $this->changed = '';
    }

    /**
     * Destroys an IMP_Compose instance.
     *
     * @param string $action  The action performed to cause the end of this
     *                        instance.  Either 'cancel', 'discard',
     *                        'save_draft', or 'send'.
     */
    public function destroy($action)
    {
        switch ($action) {
        case 'discard':
        case 'send':
            /* Delete the draft. */
            $GLOBALS['injector']->getInstance('IMP_Message')->delete(
                new IMP_Indices($this->getMetadata('draft_uid')),
                array('nuke' => true)
            );
            break;

        case 'save_draft':
            /* Don't delete any drafts. */
            $this->changed = 'deleted';
            return;

        case 'cancel':
            if ($this->getMetadata('draft_auto')) {
                $this->destroy('discard');
                return;
            }
            // Fall-through

        default:
            // No-op
            break;
        }

        $this->deleteAllAttachments();

        $this->changed = 'deleted';
    }

    /**
     * Gets metadata about the current object.
     *
     * @param string $name  The metadata name.
     *
     * @return mixed  The metadata value or null if it doesn't exist.
     */
    public function getMetadata($name)
    {
        return isset($this->_metadata[$name])
            ? $this->_metadata[$name]
            : null;
    }

    /**
     * Sets metadata for the current object.
     *
     * @param string $name  The metadata name.
     * @param mixed $value  The metadata value.
     */
    protected function _setMetadata($name, $value)
    {
        if (is_null($value)) {
            unset($this->_metadata[$name]);
        } else {
            $this->_metadata[$name] = $value;
        }
        $this->changed = 'changed';
    }

    /**
     * Saves a draft message.
     *
     * @param array $headers  List of message headers (UTF-8).
     * @param mixed $message  Either the message text (string) or a
     *                        Horde_Mime_Part object that contains the text
     *                        to send.
     * @param array $opts     An array of options w/the following keys:
     * <pre>
     *   - autosave: (boolean) Is this an auto-saved draft?
     *   - html: (boolean) Is this an HTML message?
     *   - priority: (string) The message priority ('high', 'normal', 'low').
     *   - readreceipt: (boolean) Add return receipt headers?
     * </pre>
     *
     * @return string  Notification text on success (not HTML encoded).
     *
     * @throws IMP_Compose_Exception
     */
    public function saveDraft($headers, $message, array $opts = array())
    {
        $body = $this->_saveDraftMsg($headers, $message, $opts);
        $ret = $this->_saveDraftServer($body);
        $this->_setMetadata('draft_auto', !empty($opts['autosave']));
        return $ret;
    }

    /**
     * Prepare the draft message.
     *
     * @param array $headers  List of message headers.
     * @param mixed $message  Either the message text (string) or a
     *                        Horde_Mime_Part object that contains the text
     *                        to send.
     * @param array $opts     An array of options w/the following keys:
     *   - html: (boolean) Is this an HTML message?
     *   - priority: (string) The message priority ('high', 'normal', 'low').
     *   - readreceipt: (boolean) Add return receipt headers?
     *   - verify_email: (boolean) Verify e-mail messages? Default: no.
     *
     * @return string  The body text.
     *
     * @throws IMP_Compose_Exception
     */
    protected function _saveDraftMsg($headers, $message, $opts)
    {
        $has_session = (bool)$GLOBALS['registry']->getAuth();

        /* Set up the base message now. */
        $base = $this->_createMimeMessage(new Horde_Mail_Rfc822_List(), $message, array(
            'html' => !empty($opts['html']),
            'noattach' => !$has_session,
            'nofinal' => true
        ));
        $base->isBasePart(true);

        $recip_list = $this->recipientList($headers);
        if (!empty($opts['verify_email'])) {
            foreach ($recip_list['list'] as $val) {
                try {
                    IMP::parseAddressList($val->writeAddress(true), array(
                        'validate' => true
                    ));
                } catch (Horde_Mail_Exception $e) {
                    throw new IMP_Compose_Exception(sprintf(
                        _("Saving the message failed because it contains an invalid e-mail address: %s."),
                        strval($val),
                        $e->getMessage()
                    ), $e->getCode());
                }
            }
        }
        $headers = array_merge($headers, $recip_list['header']);

        /* Initalize a header object for the draft. */
        $draft_headers = $this->_prepareHeaders($headers, array_merge($opts, array('bcc' => true)));

        /* Add information necessary to log replies/forwards when finally
         * sent. */
        $imp_imap = $GLOBALS['injector']->getInstance('IMP_Factory_Imap')->create();
        if ($this->_replytype) {
            try {
                $indices = $this->getMetadata('indices');

                $imap_url = new Horde_Imap_Client_Url();
                $imap_url->hostspec = $imp_imap->getParam('hostspec');
                $imap_url->protocol = $imp_imap->isImap() ? 'imap' : 'pop';
                $imap_url->username = $imp_imap->getParam('username');

                $urls = array();
                foreach ($indices as $val) {
                    $imap_url->mailbox = $val->mbox;
                    $imap_url->uidvalidity = $val->mbox->uidvalid;
                    foreach ($val->uids as $val2) {
                        $imap_url->uid = $val2;
                        $urls[] = '<' . strval($imap_url) . '>';
                    }
                }

                switch ($this->replyType(true)) {
                case self::FORWARD:
                    $draft_headers->addHeader('X-IMP-Draft-Forward', implode(', ', $urls));
                    break;

                case self::REPLY:
                    $draft_headers->addHeader('X-IMP-Draft-Reply', implode(', ', $urls));
                    $draft_headers->addHeader('X-IMP-Draft-Reply-Type', $this->_replytype);
                    break;
                }
            } catch (Horde_Exception $e) {}
        } else {
            $draft_headers->addHeader('X-IMP-Draft', 'Yes');
        }

        return $base->toString(array(
            'defserver' => $has_session ? $imp_imap->config->maildomain : null,
            'headers' => $draft_headers
        ));
    }

    /**
     * Save a draft message on the IMAP server.
     *
     * @param string $data  The text of the draft message.
     *
     * @return string  Status string (not HTML escaped).
     *
     * @throws IMP_Compose_Exception
     */
    protected function _saveDraftServer($data)
    {
        if (!$drafts_mbox = IMP_Mailbox::getPref(IMP_Mailbox::MBOX_DRAFTS)) {
            throw new IMP_Compose_Exception(_("Saving the draft failed. No drafts mailbox specified."));
        }

        /* Check for access to drafts mailbox. */
        if (!$drafts_mbox->create()) {
            throw new IMP_Compose_Exception(_("Saving the draft failed. Could not create a drafts mailbox."));
        }

        $append_flags = array(
            Horde_Imap_Client::FLAG_DRAFT,
            /* RFC 3503 [3.4] - MUST set MDNSent flag on draft message. */
            Horde_Imap_Client::FLAG_MDNSENT
        );
        if (!$GLOBALS['prefs']->getValue('unseen_drafts')) {
            $append_flags[] = Horde_Imap_Client::FLAG_SEEN;
        }

        $old_uid = $this->getMetadata('draft_uid');

        /* Add the message to the mailbox. */
        try {
            $ids = $drafts_mbox->imp_imap->append($drafts_mbox, array(array('data' => $data, 'flags' => $append_flags)));

            if ($old_uid) {
                $GLOBALS['injector']->getInstance('IMP_Message')->delete($old_uid, array('nuke' => true));
            }

            $this->_setMetadata('draft_uid', $drafts_mbox->getIndicesOb($ids));
            return sprintf(_("The draft has been saved to the \"%s\" mailbox."), $drafts_mbox->display);
        } catch (IMP_Imap_Exception $e) {
            return _("The draft was not successfully saved.");
        }
    }

    /**
     * Edits a message as new.
     *
     * @see resumeDraft().
     *
     * @param IMP_Indices $indices  An indices object.
     * @param array $opts           Additional options:
     *   - format: (string) Force to this format.
     *             DEFAULT: Auto-determine.
     *
     * @return mixed  See resumeDraft().
     *
     * @throws IMP_Compose_Exception
     */
    public function editAsNew($indices, array $opts = array())
    {
        $ret = $this->_resumeDraft($indices, self::EDITASNEW, $opts);
        $ret['type'] = self::EDITASNEW;
        return $ret;
    }

    /**
     * Edit an existing template message. Saving this template later
     * (using saveTemplate()) will cause the original message to be deleted.
     *
     * @param IMP_Indices $indices  An indices object.
     *
     * @return mixed  See resumeDraft().
     *
     * @throws IMP_Compose_Exception
     */
    public function editTemplate($indices)
    {
        $res = $this->useTemplate($indices);
        $this->_setMetadata('template_uid_edit', $indices);
        return $res;
    }

    /**
     * Resumes a previously saved draft message.
     *
     * @param IMP_Indices $indices  An indices object.
     * @param array $opts           Additional options:
     *   - format: (string) Force to this format.
     *             DEFAULT: Auto-determine.
     *
     * @return mixed  An array with the following keys:
     *   - addr: (array) Address lists (to, cc, bcc; Horde_Mail_Rfc822_List
     *           objects).
     *   - body: (string) The text of the body part.
     *   - format: (string) The format of the body message ('html', 'text').
     *   - identity: (mixed) See IMP_Prefs_Identity#getMatchingIdentity().
     *   - priority: (string) The message priority.
     *   - readreceipt: (boolean) Add return receipt headers?
     *   - subject: (string) Formatted subject.
     *   - type: (integer) - The compose type.
     *
     * @throws IMP_Compose_Exception
     */
    public function resumeDraft($indices, array $opts = array())
    {
        $res = $this->_resumeDraft($indices, null, $opts);
        $this->_setMetadata('draft_uid', $indices);
        return $res;
    }

    /**
     * Uses a template to create a message.
     *
     * @see resumeDraft().
     *
     * @param IMP_Indices $indices  An indices object.
     * @param array $opts           Additional options:
     *   - format: (string) Force to this format.
     *             DEFAULT: Auto-determine.
     *
     * @return mixed  See resumeDraft().
     *
     * @throws IMP_Compose_Exception
     */
    public function useTemplate($indices, array $opts = array())
    {
        $ret = $this->_resumeDraft($indices, self::TEMPLATE, $opts);
        $ret['type'] = self::TEMPLATE;
        return $ret;
    }

    /**
     * Resumes a previously saved draft message.
     *
     * @param IMP_Indices $indices  See resumeDraft().
     * @param integer $type         Compose type.
     * @param array $opts           Additional options:
     *   - format: (string) Force to this format.
     *             DEFAULT: Auto-determine.
     *
     * @return mixed  See resumeDraft().
     *
     * @throws IMP_Compose_Exception
     */
    protected function _resumeDraft($indices, $type, $opts)
    {
        global $injector, $notification, $prefs;

        $contents_factory = $injector->getInstance('IMP_Factory_Contents');

        try {
            $contents = $contents_factory->create($indices);
        } catch (IMP_Exception $e) {
            throw new IMP_Compose_Exception($e);
        }

        $headers = $contents->getHeader();
        $imp_draft = false;

        if ($draft_url = $headers->getValue('x-imp-draft-reply')) {
            if (is_null($type) &&
                !($type = $headers->getValue('x-imp-draft-reply-type'))) {
                $type = self::REPLY;
            }
            $imp_draft = self::REPLY;
        } elseif ($draft_url = $headers->getValue('x-imp-draft-forward')) {
            $imp_draft = self::FORWARD;
            if (is_null($type)) {
                $type = self::FORWARD;
            }
        } elseif ($headers->getValue('x-imp-draft')) {
            $imp_draft = self::COMPOSE;
        }

        if (!empty($opts['format'])) {
            $compose_html = ($opts['format'] == 'html');
        } elseif ($prefs->getValue('compose_html')) {
            $compose_html = true;
        } else {
            switch ($type) {
            case self::EDITASNEW:
            case self::FORWARD:
            case self::FORWARD_BODY:
            case self::FORWARD_BOTH:
                $compose_html = $prefs->getValue('forward_format');
                break;

            case self::REPLY:
            case self::REPLY_ALL:
            case self::REPLY_LIST:
            case self::REPLY_SENDER:
                $compose_html = $prefs->getValue('reply_format');
                break;

            case self::TEMPLATE:
                $compose_html = true;
                break;

            default:
                /* If this is an draft saved by IMP, we know 100% for sure
                 * that if an HTML part exists, the user was composing in
                 * HTML. */
                $compose_html = ($imp_draft !== false);
                break;
            }
        }

        $msg_text = $this->_getMessageText($contents, array(
            'html' => $compose_html,
            'imp_msg' => $imp_draft,
            'toflowed' => false
        ));

        if (empty($msg_text)) {
            $body = '';
            $format = 'text';
            $text_id = 0;
        } else {
            /* Use charset at time of initial composition if this is an IMP
             * draft. */
            if ($imp_draft !== false) {
                $this->charset = $msg_text['charset'];
            }
            $body = $msg_text['text'];
            $format = $msg_text['mode'];
            $text_id = $msg_text['id'];
        }

        $mime_message = $contents->getMIMEMessage();

        /* Add attachments. */
        $parts = array();
        if (($mime_message->getPrimaryType() == 'multipart') &&
            ($mime_message->getType() != 'multipart/alternative')) {
            for ($i = 1; ; ++$i) {
                if (intval($text_id) == $i) {
                    continue;
                }

                if ($part = $contents->getMIMEPart($i)) {
                    $parts[] = $part;
                } else {
                    break;
                }
            }
        } elseif ($mime_message->getDisposition() == 'attachment') {
            $parts[] = $contents->getMimePart('1');
        }

        foreach ($parts as $val) {
            try {
                $this->addAttachmentFromPart($val);
            } catch (IMP_Compose_Exception $e) {
                $notification->push($e, 'horde.warning');
            }
        }

        $alist = new Horde_Mail_Rfc822_List();
        $addr = array(
            'to' => clone $alist,
            'cc' => clone $alist,
            'bcc' => clone $alist
        );

        if ($type != self::EDITASNEW) {
            foreach (array('to', 'cc', 'bcc') as $val) {
                if ($tmp = $headers->getOb($val)) {
                    $addr[$val] = $tmp;
                }
            }

            if ($val = $headers->getValue('references')) {
                $ref_ob = new Horde_Mail_Rfc822_Identification($val);
                $this->_setMetadata('references', $ref_ob->ids);

                if ($val = $headers->getValue('in-reply-to')) {
                    $this->_setMetadata('in_reply_to', $val);
                }
            }

            if ($draft_url) {
                $imp_imap = $injector->getInstance('IMP_Factory_Imap')->create();
                $indices = new IMP_Indices();

                foreach (explode(',', $draft_url) as $val) {
                    $imap_url = new Horde_Imap_Client_Url(rtrim(ltrim($val, '<'), '>'));

                    try {
                        if (($imap_url->protocol == ($imp_imap->isImap() ? 'imap' : 'pop')) &&
                            ($imap_url->username == $imp_imap->getParam('username')) &&
                            // Ignore hostspec and port, since these can change
                            // even though the server is the same. UIDVALIDITY
                            // should catch any true server/backend changes.
                            (IMP_Mailbox::get($imap_url->mailbox)->uidvalid == $imap_url->uidvalidity) &&
                            $contents_factory->create(new IMP_Indices($imap_url->mailbox, $imap_url->uid))) {
                            $indices->add($imap_url->mailbox, $imap_url->uid);
                        }
                    } catch (Exception $e) {}
                }

                if (count($indices)) {
                    $this->_setMetadata('indices', $indices);
                    $this->_replytype = $type;
                }
            }
        }

        $mdn = new Horde_Mime_Mdn($headers);
        $readreceipt = (bool)$mdn->getMdnReturnAddr();

        $this->changed = 'changed';

        return array(
            'addr' => $addr,
            'body' => $body,
            'format' => $format,
            'identity' => $this->_getMatchingIdentity($headers, array('from')),
            'priority' => $injector->getInstance('IMP_Mime_Headers')->getPriority($headers),
            'readreceipt' => $readreceipt,
            'subject' => $headers->getValue('subject'),
            'type' => $type
        );
    }

    /**
     * Save a template message on the IMAP server.
     *
     * @param array $headers  List of message headers (UTF-8).
     * @param mixed $message  Either the message text (string) or a
     *                        Horde_Mime_Part object that contains the text
     *                        to save.
     * @param array $opts     An array of options w/the following keys:
     *   - html: (boolean) Is this an HTML message?
     *   - priority: (string) The message priority ('high', 'normal', 'low').
     *   - readreceipt: (boolean) Add return receipt headers?
     *
     * @return string  Notification text on success.
     *
     * @throws IMP_Compose_Exception
     */
    public function saveTemplate($headers, $message, array $opts = array())
    {
        if (!$mbox = IMP_Mailbox::getPref(IMP_Mailbox::MBOX_TEMPLATES)) {
            throw new IMP_Compose_Exception(_("Saving the template failed: no template mailbox exists."));
        }

        /* Check for access to mailbox. */
        if (!$mbox->create()) {
            throw new IMP_Compose_Exception(_("Saving the template failed: could not create the templates mailbox."));
        }

        $append_flags = array(
            // Don't mark as draft, since other MUAs could potentially
            // delete it.
            Horde_Imap_Client::FLAG_SEEN
        );

        $old_uid = $this->getMetadata('template_uid_edit');

        /* Add the message to the mailbox. */
        try {
            $mbox->imp_imap->append($mbox, array(array(
                'data' => $this->_saveDraftMsg($headers, $message, $opts),
                'flags' => $append_flags,
                'verify_email' => true
            )));

            if ($old_uid) {
                $GLOBALS['injector']->getInstance('IMP_Message')->delete($old_uid, array('nuke' => true));
            }
        } catch (IMP_Imap_Exception $e) {
            return _("The template was not successfully saved.");
        }

        return _("The template has been saved.");
    }

    /**
     * Does this message have any drafts associated with it?
     *
     * @return boolean  True if draft messages exist.
     */
    public function hasDrafts()
    {
        return (bool)$this->getMetadata('draft_uid');
    }

    /**
     * Builds and sends a MIME message.
     *
     * @param string $body                  The message body.
     * @param array $header                 List of message headers.
     * @param IMP_Prefs_Identity $identity  The Identity object for the sender
     *                                      of this message.
     * @param array $opts                   An array of options w/the
     *                                      following keys:
     *  - encrypt: (integer) A flag whether to encrypt or sign the message.
     *            One of:
     *    - IMP_Crypt_Pgp::ENCRYPT</li>
     *    - IMP_Crypt_Pgp::SIGNENC</li>
     *    - IMP_Crypt_Smime::ENCRYPT</li>
     *    - IMP_Crypt_Smime::SIGNENC</li>
     *  - html: (boolean) Whether this is an HTML message.
     *          DEFAULT: false
     *  - pgp_attach_pubkey: (boolean) Attach the user's PGP public key to the
     *                       message?
     *  - priority: (string) The message priority ('high', 'normal', 'low').
     *  - save_sent: (boolean) Save sent mail?
     *  - sent_mail: (IMP_Mailbox) The sent-mail mailbox (UTF-8).
     *  - strip_attachments: (bool) Strip attachments from the message?
     *  - signature: (string) The message signature.
     *  - readreceipt: (boolean) Add return receipt headers?
     *  - useragent: (string) The User-Agent string to use.
     *  - vcard_attach: (string) Attach the user's vCard (value is name to
     *                  display as vcard filename).
     *
     * @throws Horde_Exception
     * @throws IMP_Compose_Exception
     * @throws IMP_Compose_Exception_Address
     * @throws IMP_Exception
     */
    public function buildAndSendMessage(
        $body, $header, IMP_Prefs_Identity $identity, array $opts = array()
    )
    {
        global $conf, $injector, $notification, $prefs, $registry, $session;

        /* We need at least one recipient & RFC 2822 requires that no 8-bit
         * characters can be in the address fields. */
        $recip = $this->recipientList($header);
        if (!count($recip['list'])) {
            if ($recip['has_input']) {
                throw new IMP_Compose_Exception(_("Invalid e-mail address."));
            }
            throw new IMP_Compose_Exception(_("Need at least one message recipient."));
        }
        $header = array_merge($header, $recip['header']);

        /* Check for correct identity usage. */
        if (!$this->getMetadata('identity_check') &&
            (count($recip['list']) === 1)) {
            $identity_search = $identity->getMatchingIdentity($recip['list'], false);
            if (!is_null($identity_search) &&
                ($identity->getDefault() != $identity_search)) {
                $this->_setMetadata('identity_check', true);

                $e = new IMP_Compose_Exception(_("Recipient address does not match the currently selected identity."));
                $e->tied_identity = $identity_search;
                throw $e;
            }
        }

        /* Check body size of message. */
        $imp_imap = $injector->getInstance('IMP_Factory_Imap')->create();
        if (!$imp_imap->accessCompose(IMP_Imap::ACCESS_COMPOSE_BODYSIZE, strlen($body))) {
            Horde::permissionDeniedError('imp', 'max_bodysize');
            throw new IMP_Compose_Exception(sprintf(
                _("Your message body has exceeded the limit by body size by %d characters."),
                (strlen($body) - $imp_imap->max_compose_bodysize)
            ));
        }

        $from = new Horde_Mail_Rfc822_Address($header['from']);
        if (is_null($from->host)) {
            $from->host = $imp_imap->config->maildomain;
        }

        /* Prepare the array of messages to send out.  May be more
         * than one if we are encrypting for multiple recipients or
         * are storing an encrypted message locally. */
        $encrypt = empty($opts['encrypt']) ? 0 : $opts['encrypt'];
        $send_msgs = array();
        $msg_options = array(
            'encrypt' => $encrypt,
            'html' => !empty($opts['html']),
            'identity' => $identity,
            'pgp_attach_pubkey' => (!empty($opts['pgp_attach_pubkey']) && $prefs->getValue('use_pgp') && $prefs->getValue('pgp_public_key')),
            'signature' => is_null($opts['signature']) ? $identity : $opts['signature'],
            'vcard_attach' => ((!empty($opts['vcard_attach']) && $registry->hasMethod('contacts/ownVCard')) ? ((strlen($opts['vcard_attach']) ? $opts['vcard_attach'] : 'vcard') . '.vcf') : null)
        );

        /* Must encrypt & send the message one recipient at a time. */
        if ($prefs->getValue('use_smime') &&
            in_array($encrypt, array(IMP_Crypt_Smime::ENCRYPT, IMP_Crypt_Smime::SIGNENC))) {
            foreach ($recip['list'] as $val) {
                $list_ob = new Horde_Mail_Rfc822_List($val);
                $send_msgs[] = array(
                    'base' => $this->_createMimeMessage($list_ob, $body, $msg_options),
                    'recipients' => $list_ob
                );
            }

            /* Must target the encryption for the sender before saving message
             * in sent-mail. */
            $save_msg = $this->_createMimeMessage(IMP::parseAddressList($header['from']), $body, $msg_options);
        } else {
            /* Can send in clear-text all at once, or PGP can encrypt
             * multiple addresses in the same message. */
            $msg_options['from'] = $from;
            $save_msg = $this->_createMimeMessage($recip['list'], $body, $msg_options);
            $send_msgs[] = array(
                'base' => $save_msg,
                'recipients' => $recip['list']
            );
        }

        /* Initalize a header object for the outgoing message. */
        $headers = $this->_prepareHeaders($header, $opts);

        /* Add a Received header for the hop from browser to server. */
        $headers->addReceivedHeader(array(
            'dns' => $injector->getInstance('Net_DNS2_Resolver'),
            'server' => $conf['server']['name']
        ));

        /* Add Reply-To header. */
        if (!empty($header['replyto']) &&
            ($header['replyto'] != $from->bare_address)) {
            $headers->addHeader('Reply-to', $header['replyto']);
        }

        /* Add the 'User-Agent' header. */
        if (empty($opts['useragent'])) {
            $headers->setUserAgent('Internet Messaging Program (IMP) ' . $registry->getVersion());
        } else {
            $headers->setUserAgent($opts['useragent']);
        }
        $headers->addUserAgentHeader();

        /* Add preferred reply language(s). */
        if ($lang = @unserialize($prefs->getValue('reply_lang'))) {
            $headers->addHeader('Accept-Language', implode(',', $lang));
        }

        /* Send the messages out now. */
        $sentmail = $injector->getInstance('IMP_Sentmail');

        foreach ($send_msgs as $val) {
            switch (intval($this->replyType(true))) {
            case self::REPLY:
                $senttype = IMP_Sentmail::REPLY;
                break;

            case self::FORWARD:
                $senttype = IMP_Sentmail::FORWARD;
                break;

            case self::REDIRECT:
                $senttype = IMP_Sentmail::REDIRECT;
                break;

            default:
                $senttype = IMP_Sentmail::NEWMSG;
                break;
            }
            $headers_copy = clone $headers;
            try {
                $this->_prepSendMessageAssert($val['recipients'], $headers_copy, $val['base']);
                $this->sendMessage($val['recipients'], $headers_copy, $val['base']);

                /* Store history information. */
                $msg_id = new Horde_Mail_Rfc822_Identification(
                    $headers_copy->getValue('message-id')
                );
                $sentmail->log(
                    $senttype,
                    reset($msg_id->ids),
                    $val['recipients'],
                    true
                );
            } catch (IMP_Compose_Exception_Address $e) {
                throw $e;
            } catch (IMP_Compose_Exception $e) {
                /* Unsuccessful send. */
                if ($e->log()) {
                    $msg_id = new Horde_Mail_Rfc822_Identification(
                        $headers_copy->getValue('message-id')
                    );
                    $sentmail->log(
                        $senttype,
                        reset($msg_id->ids),
                        $val['recipients'],
                        false
                    );
                }
                throw new IMP_Compose_Exception(sprintf(_("There was an error sending your message: %s"), $e->getMessage()));
            }
        }

        $recipients = strval($recip['list']);

        if ($this->_replytype) {
            /* Log the reply. */
            if ($indices = $this->getMetadata('indices')) {
                switch ($this->_replytype) {
                case self::FORWARD:
                case self::FORWARD_ATTACH:
                case self::FORWARD_BODY:
                case self::FORWARD_BOTH:
                    $log = new IMP_Maillog_Log_Forward($recipients);
                    break;

                case self::REPLY:
                case self::REPLY_SENDER:
                    $log = new IMP_Maillog_Log_Reply();
                    break;

                case IMP_Compose::REPLY_ALL:
                    $log = new IMP_Maillog_Log_Replyall();
                    break;

                case IMP_Compose::REPLY_LIST:
                    $log = new IMP_Maillog_Log_Replylist();
                    break;
                }

                $log_msgs = array();
                foreach ($indices as $val) {
                    foreach ($val->uids as $val2) {
                        $log_msgs[] = new IMP_Maillog_Message(
                            new IMP_Indices($val->mbox, $val2)
                        );
                    }
                }

                $injector->getInstance('IMP_Maillog')->log($log_msgs, $log);
            }

            $imp_message = $injector->getInstance('IMP_Message');
            $reply_uid = new IMP_Indices($this);

            switch ($this->replyType(true)) {
            case self::FORWARD:
                /* Set the Forwarded flag, if possible, in the mailbox.
                 * See RFC 5550 [5.9] */
                $imp_message->flag(array(
                    'add' => array(Horde_Imap_Client::FLAG_FORWARDED)
                ), $reply_uid);
                break;

            case self::REPLY:
                /* Make sure to set the IMAP reply flag and unset any
                 * 'flagged' flag. */
                $imp_message->flag(array(
                    'add' => array(Horde_Imap_Client::FLAG_ANSWERED),
                    'remove' => array(Horde_Imap_Client::FLAG_FLAGGED)
                ), $reply_uid);
                break;
            }
        }

        Horde::log(
            sprintf(
                "Message sent to %s from %s (%s)",
                $recipients,
                $registry->getAuth(),
                $session->get('horde', 'auth/remoteAddr')
            ),
            'INFO'
        );

        /* Should we save this message in the sent mail mailbox? */
        if (!empty($opts['sent_mail']) &&
            ((!$prefs->isLocked('save_sent_mail') &&
              !empty($opts['save_sent'])) ||
             ($prefs->isLocked('save_sent_mail') &&
              $prefs->getValue('save_sent_mail')))) {
            /* Keep Bcc: headers on saved messages. */
            if ((is_array($header['bcc']) || $header['bcc'] instanceof Countable) &&
                count($header['bcc'])) {
                $headers->addHeader('Bcc', $header['bcc']);
            }

            /* Strip attachments if requested. */
            if (!empty($opts['strip_attachments'])) {
                $save_msg->buildMimeIds();

                /* Don't strip any part if this is a text message with both
                 * plaintext and HTML representation, or a signed or encrypted
                 * message. */
                if ($save_msg->getType() != 'multipart/alternative' &&
                    $save_msg->getType() != 'multipart/encrypted' &&
                    $save_msg->getType() != 'multipart/signed') {
                    for ($i = 2; ; ++$i) {
                        if (!($oldPart = $save_msg->getPart($i))) {
                            break;
                        }

                        $replace_part = new Horde_Mime_Part();
                        $replace_part->setType('text/plain');
                        $replace_part->setCharset($this->charset);
                        $replace_part->setLanguage($GLOBALS['language']);
                        $replace_part->setContents('[' . _("Attachment stripped: Original attachment type") . ': "' . $oldPart->getType() . '", ' . _("name") . ': "' . $oldPart->getName(true) . '"]');
                        $save_msg->alterPart($i, $replace_part);
                    }
                }
            }

            /* Generate the message string. */
            $fcc = $save_msg->toString(array(
                'defserver' => $imp_imap->config->maildomain,
                'headers' => $headers,
                'stream' => true
            ));

            /* Make sure sent mailbox is created. */
            $sent_mail = IMP_Mailbox::get($opts['sent_mail']);
            $sent_mail->create();

            $flags = array(
                Horde_Imap_Client::FLAG_SEEN,
                /* RFC 3503 [3.3] - MUST set MDNSent flag on sent message. */
                Horde_Imap_Client::FLAG_MDNSENT
            );

            try {
                $imp_imap->append($sent_mail, array(array('data' => $fcc, 'flags' => $flags)));
            } catch (IMP_Imap_Exception $e) {
                $notification->push(sprintf(_("Message sent successfully, but not saved to %s."), $sent_mail->display));
            }
        }

        /* Delete the attachment data. */
        $this->deleteAllAttachments();

        /* Save recipients to address book? */
        $this->_saveRecipients($recip['list']);

        /* Call post-sent hook. */
        try {
            $injector->getInstance('Horde_Core_Hooks')->callHook(
                'post_sent',
                'imp',
                array($save_msg['msg'], $headers)
            );
        } catch (Horde_Exception_HookNotSet $e) {}
    }

    /**
     * Prepare header object with basic header fields and converts headers
     * to the current compose charset.
     *
     * @param array $headers  Array with 'from', 'to', 'cc', 'bcc', and
     *                        'subject' values.
     * @param array $opts     An array of options w/the following keys:
     *   - bcc: (boolean) Add BCC header to output.
     *   - priority: (string) The message priority ('high', 'normal', 'low').
     *
     * @return Horde_Mime_Headers  Headers object with the appropriate headers
     *                             set.
     */
    protected function _prepareHeaders($headers, array $opts = array())
    {
        $ob = new Horde_Mime_Headers();

        $ob->addHeader('Date', date('r'));
        $ob->addMessageIdHeader();

        if (isset($headers['from']) && strlen($headers['from'])) {
            $ob->addHeader('From', $headers['from']);
        }

        if (isset($headers['to']) &&
            (is_object($headers['to']) || strlen($headers['to']))) {
            $ob->addHeader('To', $headers['to']);
        }

        if (isset($headers['cc']) &&
            (is_object($headers['cc']) || strlen($headers['cc']))) {
            $ob->addHeader('Cc', $headers['cc']);
        }

        if (!empty($opts['bcc']) &&
            isset($headers['bcc']) &&
            (is_object($headers['bcc']) || strlen($headers['bcc']))) {
            $ob->addHeader('Bcc', $headers['bcc']);
        }

        if (isset($headers['subject']) && strlen($headers['subject'])) {
            $ob->addHeader('Subject', $headers['subject']);
        }

        if ($this->replyType(true) == self::REPLY) {
            if ($refs = $this->getMetadata('references')) {
                $ob->addHeader('References', implode(' ', $refs));
            }
            if ($this->getMetadata('in_reply_to')) {
                $ob->addHeader('In-Reply-To', $this->getMetadata('in_reply_to'));
            }
        }

        /* Add priority header, if requested. */
        if (!empty($opts['priority'])) {
            switch ($opts['priority']) {
            case 'high':
                $ob->addHeader('Importance', 'High');
                $ob->addHeader('X-Priority', '1 (Highest)');
                break;

            case 'low':
                $ob->addHeader('Importance', 'Low');
                $ob->addHeader('X-Priority', '5 (Lowest)');
                break;
            }
        }

        /* Add Return Receipt Headers. */
        if (!empty($opts['readreceipt'])) {
            $from = $ob->getOb('from');
            $from = $from[0];
            if (is_null($from->host)) {
                $from->host = $GLOBALS['injector']->getInstance('IMP_Factory_Imap')->create()->config->maildomain;
            }

            $mdn = new Horde_Mime_Mdn($ob);
            $mdn->addMdnRequestHeaders($from);
        }

        return $ob;
    }

    /**
     * Sends a message.
     *
     * @param Horde_Mail_Rfc822_List $email  The e-mail list to send to.
     * @param Horde_Mime_Headers $headers    The object holding this message's
     *                                       headers.
     * @param Horde_Mime_Part $message       The object that contains the text
     *                                       to send.
     *
     * @throws IMP_Compose_Exception
     */
    public function sendMessage(Horde_Mail_Rfc822_List $email,
                                Horde_Mime_Headers $headers,
                                Horde_Mime_Part $message)
    {
        $email = $this->_prepSendMessage($email, $message);

        $opts = array();
        if ($this->getMetadata('encrypt_sign')) {
            /* Signing requires that the body not be altered in transport. */
            $opts['encode'] = Horde_Mime_Part::ENCODE_7BIT;
        }

        try {
            $message->send($email, $headers, $GLOBALS['injector']->getInstance('IMP_Mail'), $opts);
        } catch (Horde_Mime_Exception $e) {
            throw new IMP_Compose_Exception($e);
        }
    }

    /**
     * Sanity checking/MIME formatting before sending a message.
     *
     * @param Horde_Mail_Rfc822_List $email  The e-mail list to send to.
     * @param Horde_Mime_Part $message       The object that contains the text
     *                                       to send.
     *
     * @return string  The encoded $email list.
     *
     * @throws IMP_Compose_Exception
     */
    protected function _prepSendMessage(Horde_Mail_Rfc822_List $email,
                                        $message = null)
    {
        /* Properly encode the addresses we're sending to. Always try
         * charset of original message as we know that the user can handle
         * that charset. */
        try {
            return $this->_prepSendMessageEncode($email, is_null($message) ? 'UTF-8' : $message->getHeaderCharset());
        } catch (IMP_Compose_Exception $e) {
            if (is_null($message)) {
                throw $e;
            }
        }

        /* Fallback to UTF-8 (if replying, original message might be in
         * US-ASCII, for example, but To/Subject/Etc. may contain 8-bit
         * characters. */
        $message->setHeaderCharset('UTF-8');
        return $this->_prepSendMessageEncode($email, 'UTF-8');
    }

    /**
     * Additonal checks to do if this is a user-generated compose message.
     *
     * @param Horde_Mail_Rfc822_List $email  The e-mail list to send to.
     * @param Horde_Mime_Headers $headers    The object holding this message's
     *                                       headers.
     * @param Horde_Mime_Part $message       The object that contains the text
     *                                       to send.
     *
     * @throws IMP_Compose_Exception
     */
    protected function _prepSendMessageAssert(Horde_Mail_Rfc822_List $email,
                                              Horde_Mime_Headers $headers = null,
                                              Horde_Mime_Part $message = null)
    {
        global $injector;

        $email_count = count($email);
        $imp_imap = $injector->getInstance('IMP_Factory_Imap')->create();

        if (!$imp_imap->accessCompose(IMP_Imap::ACCESS_COMPOSE_TIMELIMIT, $email_count)) {
            Horde::permissionDeniedError('imp', 'max_timelimit');
            throw new IMP_Compose_Exception(sprintf(
                ngettext(
                    "You are not allowed to send messages to more than %d recipient within %d hours.",
                    "You are not allowed to send messages to more than %d recipients within %d hours.",
                    $imp_imap->max_compose_timelimit
                ),
                $imp_imap->max_compose_timelimit,
                $injector->getInstance('IMP_Sentmail')->limit_period
            ));
        }

        /* Count recipients if necessary. We need to split email groups
         * because the group members count as separate recipients. */
        if (!$imp_imap->accessCompose(IMP_Imap::ACCESS_COMPOSE_RECIPIENTS, $email_count)) {
            Horde::permissionDeniedError('imp', 'max_recipients');
            throw new IMP_Compose_Exception(sprintf(
                ngettext(
                    "You are not allowed to send messages to more than %d recipient.",
                    "You are not allowed to send messages to more than %d recipients.",
                    $imp_imap->max_compose_recipients
                ),
                $imp_imap->max_compose_recipients
            ));
        }

        /* Pass to hook to allow alteration of message details. */
        if (!is_null($message)) {
            try {
                $injector->getInstance('Horde_Core_Hooks')->callHook(
                    'pre_sent',
                    'imp',
                    array($message, $headers, $this)
                );
            } catch (Horde_Exception_HookNotSet $e) {}
        }
    }

    /**
     * Encode address and do sanity checking on encoded address.
     *
     * @param Horde_Mail_Rfc822_List $email  The e-mail list to send to.
     * @param string $charset                The charset to encode to.
     *
     * @return string  The encoded $email list.
     *
     * @throws IMP_Compose_Exception_Address
     */
    protected function _prepSendMessageEncode(Horde_Mail_Rfc822_List $email,
                                              $charset)
    {
        global $injector;

        $exception = new IMP_Compose_Exception_Address();
        $hook = true;
        $out = array();

        foreach ($email as $val) {
            /* $email contains address objects that already have the default
             * maildomain appended. Need to encode personal part and encode
             * IDN domain names. */
            try {
                $tmp = $val->writeAddress(array(
                    'encode' => $charset,
                    'idn' => true
                ));

                /* We have written address, but it still may not be valid.
                 * So double-check. */
                $alist = IMP::parseAddressList($tmp, array(
                    'validate' => true
                ));

                $error = null;

                if ($hook) {
                    try {
                        $error = $injector->getInstance('Horde_Core_Hooks')->callHook(
                            'compose_addr',
                            'imp',
                            array($alist[0])
                        );
                    } catch (Horde_Exception_HookNotSet $e) {
                        $hook = false;
                    }
                }
            } catch (Horde_Idna_Exception $e) {
                $error = array(
                    'msg' => sprintf(_("Invalid e-mail address (%s): %s"), $val, $e->getMessage())
                );
            } catch (Horde_Mail_Exception $e) {
                $error = array(
                    'msg' => sprintf(_("Invalid e-mail address (%s)."), $val)
                );
            }

            if (is_array($error)) {
                switch (isset($error['level']) ? $error['level'] : $exception::BAD) {
                case $exception::WARN:
                case 'warn':
                    if (($warn = $this->getMetadata('warn_addr')) &&
                        in_array(strval($val), $warn)) {
                        $out[] = $tmp;
                        continue 2;
                    }
                    $warn[] = strval($val);
                    $this->_setMetadata('warn_addr', $warn);
                    $this->changed = 'changed';
                    $level = $exception::WARN;
                    break;

                default:
                    $level = $exception::BAD;
                    break;
                }

                $exception->addAddress($val, $error['msg'], $level);
            } else {
                $out[] = $tmp;
            }
        }

        if (count($exception)) {
            throw $exception;
        }

        return implode(', ', $out);
    }

    /**
     * Save the recipients done in a sendMessage().
     *
     * @param Horde_Mail_Rfc822_List $recipients  The list of recipients.
     */
    public function _saveRecipients(Horde_Mail_Rfc822_List $recipients)
    {
        global $notification, $prefs, $registry;

        if (!$prefs->getValue('save_recipients') ||
            !$registry->hasMethod('contacts/import') ||
            !($abook = $prefs->getValue('add_source'))) {
            return;
        }

        foreach ($recipients as $recipient) {
            $name = is_null($recipient->personal)
                ? $recipient->mailbox
                : $recipient->personal;

            try {
                $registry->call(
                    'contacts/import',
                    array(
                        array('name' => $name, 'email' => $recipient->bare_address),
                        'array',
                        $abook,
                        array('match_on_email' => true)
                    )
                );
                $notification->push(sprintf(_("Entry \"%s\" was successfully added to the address book"), $name), 'horde.success');
            } catch (Turba_Exception_ObjectExists $e) {
            } catch (Horde_Exception $e) {
                if ($e->getCode() == 'horde.error') {
                    $notification->push($e, $e->getCode());
                }
            }
        }
    }

    /**
     * Cleans up and returns the recipient list. Method designed to parse
     * user entered data; does not encode/validate addresses.
     *
     * @param array $hdr  An array of MIME headers and/or address list
     *                    objects. Recipients will be extracted from the 'to',
     *                    'cc', and 'bcc' entries.
     *
     * @return array  An array with the following entries:
     *   - has_input: (boolean) True if at least one of the headers contains
     *                user input.
     *   - header: (array) Contains the cleaned up 'to', 'cc', and 'bcc'
     *             address list (Horde_Mail_Rfc822_List objects).
     *   - list: (Horde_Mail_Rfc822_List) Recipient addresses.
     */
    public function recipientList($hdr)
    {
        $addrlist = new Horde_Mail_Rfc822_List();
        $has_input = false;
        $header = array();

        foreach (array('to', 'cc', 'bcc') as $key) {
            if (isset($hdr[$key])) {
                $ob = IMP::parseAddressList($hdr[$key]);
                if (count($ob)) {
                    $addrlist->add($ob);
                    $header[$key] = $ob;
                    $has_input = true;
                } else {
                    $header[$key] = null;
                }
            }
        }

        return array(
            'has_input' => $has_input,
            'header' => $header,
            'list' => $addrlist
        );
    }

    /**
     * Create the base Horde_Mime_Part for sending.
     *
     * @param Horde_Mail_Rfc822_List $to  The recipient list.
     * @param string $body                Message body.
     * @param array $options              Additional options:
     *   - encrypt: (integer) The encryption flag.
     *   - from: (Horde_Mail_Rfc822_Address) The outgoing from address (only
     *           needed for multiple PGP encryption).
     *   - html: (boolean) Is this a HTML message?
     *   - identity: (IMP_Prefs_Identity) Identity of the sender.
     *   - nofinal: (boolean) This is not a message which will be sent out.
     *   - noattach: (boolean) Don't add attachment information.
     *   - pgp_attach_pubkey: (boolean) Attach the user's PGP public key?
     *   - signature: (IMP_Prefs_Identity|string) If set, add the signature to
     *                the message.
     *   - vcard_attach: (string) If set, attach user's vcard to message.
     *
     * @return Horde_Mime_Part  The MIME message to send.
     *
     * @throws Horde_Exception
     * @throws IMP_Compose_Exception
     */
    protected function _createMimeMessage(
        Horde_Mail_Rfc822_List $to, $body, array $options = array()
    )
    {
        global $conf, $injector, $prefs, $registry;

        /* Get body text. */
        if (empty($options['html'])) {
            $body_html = null;
        } else {
            $tfilter = $injector->getInstance('Horde_Core_Factory_TextFilter');

            $body_html = $tfilter->filter(
                $body,
                'Xss',
                array(
                    'return_dom' => true,
                    'strip_style_attributes' => false
                )
            );
            $body_html_body = $body_html->getBody();

            $body = $tfilter->filter(
                $body_html->returnHtml(),
                'Html2text',
                array(
                    'width' => 0
                )
            );
        }

        $hooks = $injector->getInstance('Horde_Core_Hooks');

        /* We need to do the attachment check before any of the body text
         * has been altered. */
        if (!count($this) && !$this->getMetadata('attach_body_check')) {
            $this->_setMetadata('attach_body_check', true);

            try {
                $check = $hooks->callHook(
                    'attach_body_check',
                    'imp',
                    array($body)
                );
            } catch (Horde_Exception_HookNotSet $e) {
                $check = array();
            }

            if (!empty($check) &&
                preg_match('/\b(' . implode('|', array_map('preg_quote', $check)) . ')\b/i', $body, $matches)) {
                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]));
            }
        }

        /* Add signature data. */
        if (!empty($options['signature'])) {
            if (is_string($options['signature'])) {
                if (empty($options['html'])) {
                    $body .= "\n\n" . trim($options['signature']);
                } else {
                    $html_sig = trim($options['signature']);
                    $body .= "\n" . $tfilter->filter($html_sig, 'Html2text');
                }
            } else {
                $sig = $options['signature']->getSignature('text');
                $body .= $sig;

                if (!empty($options['html'])) {
                    $html_sig = $options['signature']->getSignature('html');
                    if (!strlen($html_sig) && strlen($sig)) {
                        $html_sig = $this->text2html($sig);
                    }
                }
            }

            if (!empty($options['html'])) {
                try {
                    $sig_ob = new IMP_Compose_HtmlSignature($html_sig);
                } catch (IMP_Exception $e) {
                    throw new IMP_Compose_Exception($e);
                }

                foreach ($sig_ob->dom->getBody()->childNodes as $child) {
                    $body_html_body->appendChild(
                        $body_html->dom->importNode($child, true)
                    );
                }
            }
        }

        /* Add linked attachments. */
        if (empty($options['nofinal'])) {
            $this->_linkAttachments($body, $body_html);
        }

        /* Get trailer text (if any). */
        if (empty($options['nofinal'])) {
            try {
                $trailer = $hooks->callHook(
                    'trailer',
                    'imp',
                    array(false, $options['identity'], $to)
                );
                $html_trailer = $hooks->callHook(
                    'trailer',
                    'imp',
                    array(true, $options['identity'], $to)
                );
            } catch (Horde_Exception_HookNotSet $e) {
                $trailer = $html_trailer = null;
            }

            $body .= strval($trailer);

            if (!empty($options['html'])) {
                if (is_null($html_trailer) && strlen($trailer)) {
                    $html_trailer = $this->text2html($trailer);
                }

                if (strlen($html_trailer)) {
                    $t_dom = new Horde_Domhtml($html_trailer, 'UTF-8');
                    foreach ($t_dom->getBody()->childNodes as $child) {
                        $body_html_body->appendChild($body_html->dom->importNode($child, true));
                    }
                }
            }
        }

        /* Convert text to sending charset. HTML text will be converted
         * via Horde_Domhtml. */
        $body = Horde_String::convertCharset($body, 'UTF-8', $this->charset);

        /* Set up the body part now. */
        $textBody = new Horde_Mime_Part();
        $textBody->setType('text/plain');
        $textBody->setCharset($this->charset);
        $textBody->setDisposition('inline');

        /* Send in flowed format. */
        $flowed = new Horde_Text_Flowed($body, $this->charset);
        $flowed->setDelSp(true);
        $textBody->setContentTypeParameter('format', 'flowed');
        $textBody->setContentTypeParameter('DelSp', 'Yes');
        $text_contents = $flowed->toFlowed();
        $textBody->setContents($text_contents);

        /* Determine whether or not to send a multipart/alternative
         * message with an HTML part. */
        if (!empty($options['html'])) {
            $htmlBody = new Horde_Mime_Part();
            $htmlBody->setType('text/html');
            $htmlBody->setCharset($this->charset);
            $htmlBody->setDisposition('inline');
            $htmlBody->setDescription(Horde_String::convertCharset(_("HTML Message"), 'UTF-8', $this->charset));

            /* Add default font CSS information here. */
            $styles = array();
            if ($font_family = $prefs->getValue('compose_html_font_family')) {
                $styles[] = 'font-family:' . $font_family;
            }
            if ($font_size = intval($prefs->getValue('compose_html_font_size'))) {
                $styles[] = 'font-size:' . $font_size . 'px';
            }

            if (!empty($styles)) {
                $body_html_body->setAttribute('style', implode(';', $styles));
            }

            if (empty($options['nofinal'])) {
                $this->_cleanHtmlOutput($body_html);
            }

            $to_add = $this->_convertToRelated($body_html, $htmlBody);

            /* Now, all parts referred to in the HTML data have been added
             * to the attachment list. Convert to multipart/related if
             * this is the case. Exception: if text representation is empty,
             * just send HTML part. */
            if (strlen(trim($text_contents))) {
                $textpart = new Horde_Mime_Part();
                $textpart->setType('multipart/alternative');
                $textpart->addPart($textBody);
                $textpart->addPart($to_add);
                $textpart->setHeaderCharset($this->charset);

                $textBody->setDescription(Horde_String::convertCharset(_("Plaintext Message"), 'UTF-8', $this->charset));
            } else {
                $textpart = $to_add;
            }

            $htmlBody->setContents(
                $tfilter->filter(
                    $body_html->returnHtml(array(
                        'charset' => $this->charset,
                        'metacharset' => true
                    )),
                    'Cleanhtml',
                    array(
                        'charset' => $this->charset
                    )
                )
            );
        } else {
            $textpart = $textBody;
        }

        /* Add attachments. */
        $base = $textpart;
        if (empty($options['noattach'])) {
            $parts = array();

            foreach ($this as $val) {
                if (!$val->related && !$val->linked) {
                    $parts[] = $val->getPart(true);
                }
            }

            if (!empty($options['pgp_attach_pubkey'])) {
                $parts[] = $injector->getInstance('IMP_Crypt_Pgp')->publicKeyMIMEPart();
            }

            if (!empty($options['vcard_attach'])) {
                try {
                    $vpart = new Horde_Mime_Part();
                    $vpart->setType('text/x-vcard');
                    $vpart->setCharset('UTF-8');
                    $vpart->setContents($registry->call('contacts/ownVCard'));
                    $vpart->setName($options['vcard_attach']);

                    $parts[] = $vpart;
                } catch (Horde_Exception $e) {
                    throw new IMP_Compose_Exception(sprintf(_("Can't attach contact information: %s"), $e->getMessage()));
                }
            }

            if (!empty($parts)) {
                $base = new Horde_Mime_Part();
                $base->setType('multipart/mixed');
                $base->addPart($textpart);
                foreach ($parts as $val) {
                    $base->addPart($val);
                }
            }
        }

        /* Set up the base message now. */
        $encrypt = empty($options['encrypt'])
            ? IMP::ENCRYPT_NONE
            : $options['encrypt'];
        if ($prefs->getValue('use_pgp') &&
            !empty($conf['gnupg']['path']) &&
            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))) {
            $imp_pgp = $injector->getInstance('IMP_Crypt_Pgp');
            $symmetric_passphrase = null;

            switch ($encrypt) {
            case IMP_Crypt_Pgp::SIGN:
            case IMP_Crypt_Pgp::SIGNENC:
            case IMP_Crypt_Pgp::SYM_SIGNENC:
                /* Check to see if we have the user's passphrase yet. */
                $passphrase = $imp_pgp->getPassphrase('personal');
                if (empty($passphrase)) {
                    $e = new IMP_Compose_Exception(_("PGP: Need passphrase for personal private key."));
                    $e->encrypt = 'pgp_passphrase_dialog';
                    throw $e;
                }
                break;

            case IMP_Crypt_Pgp::SYM_ENCRYPT:
            case IMP_Crypt_Pgp::SYM_SIGNENC:
                /* Check to see if we have the user's symmetric passphrase
                 * yet. */
                $symmetric_passphrase = $imp_pgp->getPassphrase('symmetric', 'imp_compose_' . $this->_cacheid);
                if (empty($symmetric_passphrase)) {
                    $e = new IMP_Compose_Exception(_("PGP: Need passphrase to encrypt your message with."));
                    $e->encrypt = 'pgp_symmetric_passphrase_dialog';
                    throw $e;
                }
                break;
            }

            /* Do the encryption/signing requested. */
            try {
                switch ($encrypt) {
                case IMP_Crypt_Pgp::SIGN:
                    $base = $imp_pgp->impSignMimePart($base);
                    $this->_setMetadata('encrypt_sign', true);
                    break;

                case IMP_Crypt_Pgp::ENCRYPT:
                case IMP_Crypt_Pgp::SYM_ENCRYPT:
                    $to_list = clone $to;
                    if (count($options['from'])) {
                        $to_list->add($options['from']);
                    }
                    $base = $imp_pgp->IMPencryptMIMEPart($base, $to_list, ($encrypt == IMP_Crypt_Pgp::SYM_ENCRYPT) ? $symmetric_passphrase : null);
                    break;

                case IMP_Crypt_Pgp::SIGNENC:
                case IMP_Crypt_Pgp::SYM_SIGNENC:
                    $to_list = clone $to;
                    if (count($options['from'])) {
                        $to_list->add($options['from']);
                    }
                    $base = $imp_pgp->IMPsignAndEncryptMIMEPart($base, $to_list, ($encrypt == IMP_Crypt_Pgp::SYM_SIGNENC) ? $symmetric_passphrase : null);
                    break;
                }
            } catch (Horde_Exception $e) {
                throw new IMP_Compose_Exception(_("PGP Error: ") . $e->getMessage(), $e->getCode());
            }
        } elseif ($prefs->getValue('use_smime') &&
                  in_array($encrypt, array(IMP_Crypt_Smime::ENCRYPT, IMP_Crypt_Smime::SIGN, IMP_Crypt_Smime::SIGNENC))) {
            $imp_smime = $injector->getInstance('IMP_Crypt_Smime');

            /* Check to see if we have the user's passphrase yet. */
            if (in_array($encrypt, array(IMP_Crypt_Smime::SIGN, IMP_Crypt_Smime::SIGNENC))) {
                $passphrase = $imp_smime->getPassphrase();
                if ($passphrase === false) {
                    $e = new IMP_Compose_Exception(_("S/MIME Error: Need passphrase for personal private key."));
                    $e->encrypt = 'smime_passphrase_dialog';
                    throw $e;
                }
            }

            /* Do the encryption/signing requested. */
            try {
                switch ($encrypt) {
                case IMP_Crypt_Smime::SIGN:
                    $base = $imp_smime->IMPsignMIMEPart($base);
                    $this->_setMetadata('encrypt_sign', true);
                    break;

                case IMP_Crypt_Smime::ENCRYPT:
                    $base = $imp_smime->IMPencryptMIMEPart($base, $to[0]);
                    break;

                case IMP_Crypt_Smime::SIGNENC:
                    $base = $imp_smime->IMPsignAndEncryptMIMEPart($base, $to[0]);
                    break;
                }
            } catch (Horde_Exception $e) {
                throw new IMP_Compose_Exception(_("S/MIME Error: ") . $e->getMessage(), $e->getCode());
            }
        }

        /* Flag this as the base part and rebuild MIME IDs. */
        $base->isBasePart(true);
        $base->buildMimeIds();

        return $base;
    }

    /**
     * Determines the reply text and headers for a message.
     *
     * @param integer $type           The reply type (self::REPLY* constant).
     * @param IMP_Contents $contents  An IMP_Contents object.
     * @param array $opts             Additional options:
     *   - format: (string) Force to this format.
     *             DEFAULT: Auto-determine.
     *   - to: (string) The recipient of the reply. Overrides the
     *         automatically determined value.
     *
     * @return array  An array with the following keys:
     *   - addr: (array) Address lists (to, cc, bcc; Horde_Mail_Rfc822_List
     *           objects).
     *   - body: (string) The text of the body part.
     *   - format: (string) The format of the body message (html, text).
     *   - identity: (integer) The identity to use for the reply based on the
     *               original message's addresses.
     *   - lang: (array) Language code (keys)/language name (values) of the
     *           original sender's preferred language(s).
     *   - reply_list_id: (string) List ID label.
     *   - reply_recip: (integer) Number of recipients in reply list.
     *   - subject: (string) Formatted subject.
     *   - type: (integer) The reply type used (either self::REPLY_ALL,
     *           self::REPLY_LIST, or self::REPLY_SENDER).
     * @throws IMP_Exception
     */
    public function replyMessage($type, $contents, array $opts = array())
    {
        global $injector, $language, $prefs;

        if (!($contents instanceof IMP_Contents)) {
            throw new IMP_Exception(
                _("Could not retrieve message data from the mail server.")
            );
        }

        $alist = new Horde_Mail_Rfc822_List();
        $addr = array(
            'to' => clone $alist,
            'cc' => clone $alist,
            'bcc' => clone $alist
        );

        $h = $contents->getHeader();
        $match_identity = $this->_getMatchingIdentity($h);
        $reply_type = self::REPLY_SENDER;

        if (!$this->_replytype) {
            $this->_setMetadata('indices', $contents->getIndicesOb());

            /* Set the Message-ID related headers (RFC 5322 [3.6.4]). */
            $msg_id = new Horde_Mail_Rfc822_Identification(
                $h->getValue('message-id')
            );
            if (count($msg_id->ids)) {
                $this->_setMetadata('in_reply_to', reset($msg_id->ids));
            }

            $ref_ob = new Horde_Mail_Rfc822_Identification(
                $h->getValue('references')
            );
            if (!count($ref_ob->ids)) {
                $ref_ob = new Horde_Mail_Rfc822_Identification(
                    $h->getValue('in-reply-to')
                );
                if (count($ref_ob->ids) > 1) {
                    $ref_ob->ids = array();
                }
            }

            if (count($ref_ob->ids)) {
                $this->_setMetadata(
                    'references',
                    array_merge($ref_ob->ids, array(reset($msg_id->ids)))
                );
            }
        }

        $subject = strlen($s = $h->getValue('subject'))
            ? 'Re: ' . strval(new Horde_Imap_Client_Data_BaseSubject($s, array('keepblob' => true)))
            : 'Re: ';

        $force = false;
        if (in_array($type, array(self::REPLY_AUTO, self::REPLY_SENDER))) {
            if (isset($opts['to'])) {
                $addr['to']->add($opts['to']);
                $force = true;
            } elseif ($tmp = $h->getOb('reply-to')) {
                $addr['to']->add($tmp);
                $force = true;
            } else {
                $addr['to']->add($h->getOb('from'));
            }
        } elseif ($type === self::REPLY_ALL) {
            $force = isset($h['reply-to']);
        }

        /* We might need $list_info in the reply_all section. */
        $list_info = in_array($type, array(self::REPLY_AUTO, self::REPLY_LIST))
            ? $injector->getInstance('IMP_Message_Ui')->getListInformation($h)
            : null;

        if (!is_null($list_info) && !empty($list_info['reply_list'])) {
            /* If To/Reply-To and List-Reply address are the same, no need
             * to handle these address separately. */
            $rlist = new Horde_Mail_Rfc822_Address($list_info['reply_list']);
            if (!$rlist->match($addr['to'])) {
                $addr['to'] = clone $alist;
                $addr['to']->add($rlist);
                $reply_type = self::REPLY_LIST;
            }
        } elseif (in_array($type, array(self::REPLY_ALL, self::REPLY_AUTO))) {
            /* Clear the To field if we are auto-determining addresses. */
            if ($type == self::REPLY_AUTO) {
                $addr['to'] = clone $alist;
            }

            /* Filter out our own address from the addresses we reply to. */
            $identity = $injector->getInstance('IMP_Identity');
            $all_addrs = $identity->getAllFromAddresses();

            /* Build the To: header. It is either:
             * 1) the Reply-To address (if not a personal address)
             * 2) the From address(es) (if it doesn't contain a personal
             * address)
             * 3) all remaining Cc addresses. */
            $to_fields = array('from', 'reply-to');

            foreach (array('reply-to', 'from', 'to', 'cc') as $val) {
                /* If either a reply-to or $to is present, we use this address
                 * INSTEAD of the from address. */
                if (($force && ($val == 'from')) ||
                    !($ob = $h->getOb($val))) {
                    continue;
                }

                /* For From: need to check if at least one of the addresses is
                 * personal. */
                if ($val == 'from') {
                    foreach ($ob->raw_addresses as $addr_ob) {
                        if ($all_addrs->contains($addr_ob)) {
                            /* The from field contained a personal address.
                             * Use the 'To' header as the primary reply-to
                             * address instead. */
                            $to_fields[] = 'to';

                            /* Add other non-personal from addresses to the
                             * list of CC addresses. */
                            $ob->setIteratorFilter($ob::BASE_ELEMENTS, $all_addrs);
                            $addr['cc']->add($ob);
                            $all_addrs->add($ob);
                            continue 2;
                        }
                    }
                }

                $ob->setIteratorFilter($ob::BASE_ELEMENTS, $all_addrs);

                foreach ($ob as $hdr_ob) {
                    if ($hdr_ob instanceof Horde_Mail_Rfc822_Group) {
                        $addr['cc']->add($hdr_ob);
                        $all_addrs->add($hdr_ob->addresses);
                    } elseif (($val != 'to') ||
                              is_null($list_info) ||
                              !$force ||
                              empty($list_info['exists'])) {
                        /* Don't add as To address if this is a list that
                         * doesn't have a post address but does have a
                         * reply-to address. */
                        if (in_array($val, $to_fields)) {
                            /* If from/reply-to doesn't have personal
                             * information, check from address. */
                            if (is_null($hdr_ob->personal) &&
                                ($to_ob = $h->getOb('from')) &&
                                !is_null($to_ob[0]->personal) &&
                                ($hdr_ob->match($to_ob[0]))) {
                                $addr['to']->add($to_ob);
                            } else {
                                $addr['to']->add($hdr_ob);
                            }
                        } else {
                            $addr['cc']->add($hdr_ob);
                        }

                        $all_addrs->add($hdr_ob);
                    }
                }
            }

            /* Build the Cc: (or possibly the To:) header. If this is a
             * reply to a message that was already replied to by the user,
             * this reply will go to the original recipients (Request
             * #8485).  */
            if (count($addr['cc'])) {
                $reply_type = self::REPLY_ALL;
            }
            if (!count($addr['to'])) {
                $addr['to'] = $addr['cc'];
                $addr['cc'] = clone $alist;
            }

            /* Build the Bcc: header. */
            if ($bcc = $h->getOb('bcc')) {
                $bcc->add($identity->getBccAddresses());
                $bcc->setIteratorFilter(0, $all_addrs);
                foreach ($bcc as $val) {
                    $addr['bcc']->add($val);
                }
            }
        }

        if (!$this->_replytype || ($reply_type != $this->_replytype)) {
            $this->_replytype = $reply_type;
            $this->changed = 'changed';
        }

        $ret = $this->replyMessageText($contents, array(
            'format' => isset($opts['format']) ? $opts['format'] : null
        ));
        if ($prefs->getValue('reply_charset') &&
            ($ret['charset'] != $this->charset)) {
            $this->charset = $ret['charset'];
            $this->changed = 'changed';
        }
        unset($ret['charset']);

        if ($type == self::REPLY_AUTO) {
            switch ($reply_type) {
            case self::REPLY_ALL:
                try {
                    $recip_list = $this->recipientList($addr);
                    $ret['reply_recip'] = count($recip_list['list']);
                } catch (IMP_Compose_Exception $e) {
                    $ret['reply_recip'] = 0;
                }
                break;

            case self::REPLY_LIST:
                if (($list_parse = $injector->getInstance('Horde_ListHeaders')->parse('list-id', $h->getValue('list-id'))) &&
                    !is_null($list_parse->label)) {
                    $ret['reply_list_id'] = $list_parse->label;
                }
                break;
            }
        }

        if (($lang = $h->getValue('accept-language')) ||
            ($lang = $h->getValue('x-accept-language'))) {
            $langs = array();
            foreach (explode(',', $lang) as $val) {
                if (($name = Horde_Nls::getLanguageISO($val)) !== null) {
                    $langs[trim($val)] = $name;
                }
            }
            $ret['lang'] = array_unique($langs);

            /* Don't show display if original recipient is asking for reply in
             * the user's native language. */
            if ((count($ret['lang']) == 1) &&
                reset($ret['lang']) &&
                (substr(key($ret['lang']), 0, 2) == substr($language, 0, 2))) {
                unset($ret['lang']);
            }
        }

        return array_merge(array(
            'addr' => $addr,
            'identity' => $match_identity,
            'subject' => $subject,
            'type' => $reply_type
        ), $ret);
    }

    /**
     * Returns the reply text for a message.
     *
     * @param IMP_Contents $contents  An IMP_Contents object.
     * @param array $opts             Additional options:
     *   - format: (string) Force to this format.
     *             DEFAULT: Auto-determine.
     *
     * @return array  An array with the following keys:
     *   - body: (string) The text of the body part.
     *   - charset: (string) The guessed charset to use for the reply.
     *   - format: (string) The format of the body message ('html', 'text').
     */
    public function replyMessageText($contents, array $opts = array())
    {
        global $prefs;

        if (!$prefs->getValue('reply_quote')) {
            return array(
                'body' => '',
                'charset' => '',
                'format' => 'text'
            );
        }

        $h = $contents->getHeader();

        $from = strval($h->getOb('from'));

        if ($prefs->getValue('reply_headers') && !empty($h)) {
            $msg_pre = '----- ' .
                ($from ? sprintf(_("Message from %s"), $from) : _("Message")) .
                /* Extra '-'s line up with "End Message" below. */
                " ---------\n" .
                $this->_getMsgHeaders($h);

            $msg_post = "\n\n----- " .
                ($from ? sprintf(_("End message from %s"), $from) : _("End message")) .
                " -----\n";
        } else {
            $msg_pre = strval(new IMP_Prefs_AttribText($from, $h));
            $msg_post = '';
        }

        list($compose_html, $force_html) = $this->_msgTextFormat($opts, 'reply_format');

        $msg_text = $this->_getMessageText($contents, array(
            'html' => $compose_html,
            'replylimit' => true,
            'toflowed' => true
        ));

        if (!empty($msg_text) &&
            (($msg_text['mode'] == 'html') || $force_html)) {
            $msg = '<p>' . $this->text2html(trim($msg_pre)) . '</p>' .
                   self::HTML_BLOCKQUOTE .
                   (($msg_text['mode'] == 'text') ? $this->text2html($msg_text['flowed'] ? $msg_text['flowed'] : $msg_text['text']) : $msg_text['text']) .
                   '</blockquote><br />' .
                   ($msg_post ? $this->text2html($msg_post) : '') . '<br />';
            $msg_text['mode'] = 'html';
        } else {
            $msg = empty($msg_text['text'])
                ? '[' . _("No message body text") . ']'
                : $msg_pre . "\n\n" . $msg_text['text'] . $msg_post;
            $msg_text['mode'] = 'text';
        }

        // Bug #10148: Message text might be us-ascii, but reply headers may
        // contain 8-bit characters.
        if (($msg_text['charset'] == 'us-ascii') &&
            (Horde_Mime::is8bit($msg_pre, 'UTF-8') ||
             Horde_Mime::is8bit($msg_post, 'UTF-8'))) {
            $msg_text['charset'] = 'UTF-8';
        }

        return array(
            'body' => $msg . "\n",
            'charset' => $msg_text['charset'],
            'format' => $msg_text['mode']
        );
    }

    /**
     * Determine text editor format.
     *
     * @param array $opts        Options (contains 'format' param).
     * @param string $pref_name  The pref name that controls formatting.
     *
     * @return array  Use HTML? and Force HTML?
     */
    protected function _msgTextFormat($opts, $pref_name)
    {
        if (!empty($opts['format'])) {
            $compose_html = $force_html = ($opts['format'] == 'html');
        } elseif ($GLOBALS['prefs']->getValue('compose_html')) {
            $compose_html = $force_html = true;
        } else {
            $compose_html = $GLOBALS['prefs']->getValue($pref_name);
            $force_html = false;
        }

        return array($compose_html, $force_html);
    }

    /**
     * Determine the text and headers for a forwarded message.
     *
     * @param integer $type           The forward type (self::FORWARD*
     *                                constant).
     * @param IMP_Contents $contents  An IMP_Contents object.
     * @param boolean $attach         Attach the forwarded message?
     * @param array $opts             Additional options:
     *   - format: (string) Force to this format.
     *             DEFAULT: Auto-determine.
     *
     * @return array  An array with the following keys:
     *   - attach: (boolean) True if original message was attached.
     *   - body: (string) The text of the body part.
     *   - format: (string) The format of the body message ('html', 'text').
     *   - identity: (mixed) See IMP_Prefs_Identity#getMatchingIdentity().
     *   - subject: (string) Formatted subject.
     *   - title: (string) Title to use on page.
     *   - type: (integer) - The compose type.
     * @throws IMP_Exception
     */
    public function forwardMessage($type, $contents, $attach = true,
                                   array $opts = array())
    {
        global $prefs;

        if (!($contents instanceof IMP_Contents)) {
            throw new IMP_Exception(
                _("Could not retrieve message data from the mail server.")
            );
        }

        if ($type == self::FORWARD_AUTO) {
            switch ($prefs->getValue('forward_default')) {
            case 'body':
                $type = self::FORWARD_BODY;
                break;

            case 'both':
                $type = self::FORWARD_BOTH;
                break;

            case 'editasnew':
                $ret = $this->editAsNew(new IMP_Indices($contents));
                $ret['title'] = _("New Message");
                return $ret;

            case 'attach':
            default:
                $type = self::FORWARD_ATTACH;
                break;
            }
        }

        $h = $contents->getHeader();

        $this->_replytype = $type;
        $this->_setMetadata('indices', $contents->getIndicesOb());

        if (strlen($s = $h->getValue('subject'))) {
            $s = strval(new Horde_Imap_Client_Data_BaseSubject($s, array(
                'keepblob' => true
            )));
            $subject = 'Fwd: ' . $s;
            $title = _("Forward") . ': ' . $s;
        } else {
            $subject = 'Fwd:';
            $title = _("Forward");
        }

        $fwd_attach = false;
        if ($attach &&
            in_array($type, array(self::FORWARD_ATTACH, self::FORWARD_BOTH))) {
            try {
                $this->attachImapMessage(new IMP_Indices($contents));
                $fwd_attach = true;
            } catch (IMP_Exception $e) {}
        }

        if (in_array($type, array(self::FORWARD_BODY, self::FORWARD_BOTH))) {
            $ret = $this->forwardMessageText($contents, array(
                'format' => isset($opts['format']) ? $opts['format'] : null
            ));
            unset($ret['charset']);
        } else {
            $ret = array(
                'body' => '',
                'format' => $prefs->getValue('compose_html') ? 'html' : 'text'
            );
        }

        return array_merge(array(
            'attach' => $fwd_attach,
            'identity' => $this->_getMatchingIdentity($h),
            'subject' => $subject,
            'title' => $title,
            'type' => $type
        ), $ret);
    }

    /**
     * Returns the forward text for a message.
     *
     * @param IMP_Contents $contents  An IMP_Contents object.
     * @param array $opts             Additional options:
     *   - format: (string) Force to this format.
     *             DEFAULT: Auto-determine.
     *
     * @return array  An array with the following keys:
     *   - body: (string) The text of the body part.
     *   - charset: (string) The guessed charset to use for the forward.
     *   - format: (string) The format of the body message ('html', 'text').
     */
    public function forwardMessageText($contents, array $opts = array())
    {
        $h = $contents->getHeader();

        $from = strval($h->getOb('from'));

        $msg_pre = "\n----- " .
            ($from ? sprintf(_("Forwarded message from %s"), $from) : _("Forwarded message")) .
            " -----\n" . $this->_getMsgHeaders($h) . "\n";
        $msg_post = "\n\n----- " . _("End forwarded message") . " -----\n";

        list($compose_html, $force_html) = $this->_msgTextFormat($opts, 'forward_format');

        $msg_text = $this->_getMessageText($contents, array(
            'html' => $compose_html
        ));

        if (!empty($msg_text) &&
            (($msg_text['mode'] == 'html') || $force_html)) {
            $msg = $this->text2html($msg_pre) .
                (($msg_text['mode'] == 'text') ? $this->text2html($msg_text['text']) : $msg_text['text']) .
                $this->text2html($msg_post);
            $format = 'html';
        } else {
            $msg = $msg_pre . $msg_text['text'] . $msg_post;
            $format = 'text';
        }

        // Bug #10148: Message text might be us-ascii, but forward headers may
        // contain 8-bit characters.
        if (($msg_text['charset'] == 'us-ascii') &&
            (Horde_Mime::is8bit($msg_pre, 'UTF-8') ||
             Horde_Mime::is8bit($msg_post, 'UTF-8'))) {
            $msg_text['charset'] = 'UTF-8';
        }

        return array(
            'body' => $msg,
            'charset' => $msg_text['charset'],
            'format' => $format
        );
    }

    /**
     * Prepares a forwarded message using multiple messages.
     *
     * @param IMP_Indices $indices  An indices object containing the indices
     *                              of the forwarded messages.
     *
     * @return array  An array with the following keys:
     *   - body: (string) The text of the body part.
     *   - format: (string) The format of the body message ('html', 'text').
     *   - identity: (mixed) See IMP_Prefs_Identity#getMatchingIdentity().
     *   - subject: (string) Formatted subject.
     *   - title: (string) Title to use on page.
     *   - type: (integer) The compose type.
     */
    public function forwardMultipleMessages(IMP_Indices $indices)
    {
        global $injector, $prefs, $session;

        $this->_setMetadata('indices', $indices);
        $this->_replytype = self::FORWARD_ATTACH;

        $subject = $this->attachImapMessage($indices);

        return array(
            'body' => '',
            'format' => ($prefs->getValue('compose_html') && $session->get('imp', 'rteavail')) ? 'html' : 'text',
            'identity' => $injector->getInstance('IMP_Identity')->getDefault(),
            'subject' => $subject,
            'title' => $subject,
            'type' => self::FORWARD
        );
    }

    /**
     * Prepare a redirect message.
     *
     * @param IMP_Indices $indices  An indices object.
     */
    public function redirectMessage(IMP_Indices $indices)
    {
        $this->_setMetadata('redirect_indices', $indices);
        $this->_replytype = self::REDIRECT;
    }

    /**
     * Send a redirect (a/k/a resent) message. See RFC 5322 [3.6.6].
     *
     * @param mixed $to  The addresses to redirect to.
     * @param boolean $log  Whether to log the resending in the history and
     *                      sentmail log.
     *
     * @return array  An object with the following properties for each
     *                redirected message:
     *   - contents: (IMP_Contents) The contents object.
     *   - headers: (Horde_Mime_Headers) The header object.
     *   - mbox: (IMP_Mailbox) Mailbox of the message.
     *   - uid: (string) UID of the message.
     *
     * @throws IMP_Compose_Exception
     */
    public function sendRedirectMessage($to, $log = true)
    {
        global $injector, $registry;

        $recip = $this->recipientList(array('to' => $to));
        if (!count($recip['list'])) {
            if ($recip['has_input']) {
                throw new IMP_Compose_Exception(_("Invalid e-mail address."));
            }
            throw new IMP_Compose_Exception(_("Need at least one message recipient."));
        }

        $identity = $injector->getInstance('IMP_Identity');
        $from_addr = $identity->getFromAddress();

        $out = array();

        foreach ($this->getMetadata('redirect_indices') as $val) {
            foreach ($val->uids as $val2) {
                try {
                    $contents = $injector->getInstance('IMP_Factory_Contents')->create($val->mbox->getIndicesOb($val2));
                } catch (IMP_Exception $e) {
                    throw new IMP_Compose_Exception(_("Error when redirecting message."));
                }

                $headers = $contents->getHeader();

                /* We need to set the Return-Path header to the current user -
                 * see RFC 2821 [4.4]. */
                $headers->removeHeader('return-path');
                $headers->addHeader('Return-Path', $from_addr);

                /* Generate the 'Resent' headers (RFC 5322 [3.6.6]). These
                 * headers are prepended to the message. */
                $resent_headers = new Horde_Mime_Headers();
                $resent_headers->addHeader('Resent-Date', date('r'));
                $resent_headers->addHeader('Resent-From', $from_addr);
                $resent_headers->addHeader('Resent-To', $recip['header']['to']);
                $resent_headers->addHeader('Resent-Message-ID', Horde_Mime::generateMessageId());

                $header_text = trim($resent_headers->toString(array('encode' => 'UTF-8'))) . "\n" . trim($contents->getHeader(IMP_Contents::HEADER_TEXT));

                $this->_prepSendMessageAssert($recip['list']);
                $to = $this->_prepSendMessage($recip['list']);
                $hdr_array = $headers->toArray(array('charset' => 'UTF-8'));
                $hdr_array['_raw'] = $header_text;

                try {
                    $injector->getInstance('IMP_Mail')->send($to, $hdr_array, $contents->getBody());
                } catch (Horde_Mail_Exception $e) {
                    $e2 = new IMP_Compose_Exception($e);

                    if (($prev = $e->getPrevious()) &&
                        ($prev instanceof Horde_Smtp_Exception)) {
                        Horde::log(
                            sprintf(
                                "SMTP Error: %s (%u; %s)",
                                $prev->raw_msg,
                                $prev->getCode(),
                                $prev->getEnhancedSmtpCode() ?: 'N/A'
                            ),
                            'ERR'
                        );
                        $e2->logged = true;
                    }

                    throw $e2;
                }

                $recipients = strval($recip['list']);

                Horde::log(sprintf("%s Redirected message sent to %s from %s", $_SERVER['REMOTE_ADDR'], $recipients, $registry->getAuth()), 'INFO');

                if ($log) {
                    /* Store history information. */
                    $msg_id = new Horde_Mail_Rfc822_Identification(
                        $headers->getValue('message-id')
                    );

                    $injector->getInstance('IMP_Maillog')->log(
                        new IMP_Maillog_Message(reset($msg_id->ids)),
                        new IMP_Maillog_Log_Redirect($recipients)
                    );

                    $injector->getInstance('IMP_Sentmail')->log(
                        IMP_Sentmail::REDIRECT,
                        reset($msg_id->ids),
                        $recipients
                    );
                }

                $tmp = new stdClass;
                $tmp->contents = $contents;
                $tmp->headers = $headers;
                $tmp->mbox = $val->mbox;
                $tmp->uid = $val2;

                $out[] = $tmp;
            }
        }

        return $out;
    }

    /**
     * Get "tieto" identity information.
     *
     * @param Horde_Mime_Headers $h  The headers object for the message.
     * @param array $only            Only use these headers.
     *
     * @return integer  The matching identity. If no exact match, returns the
     *                  default identity.
     */
    protected function _getMatchingIdentity($h, array $only = array())
    {
        global $injector;

        $identity = $injector->getInstance('IMP_Identity');
        $msgAddresses = array();
        if (empty($only)) {
            /* Bug #9271: Check 'from' address first; if replying to a message
             * originally sent by user, this should be the identity used for
             * the reply also. */
            $only = array('from', 'to', 'cc', 'bcc');
        }

        foreach ($only as $val) {
            $msgAddresses[] = $h->getValue($val);
        }

        $match = $identity->getMatchingIdentity($msgAddresses);

        return is_null($match)
            ? $identity->getDefault()
            : $match;
    }

    /**
     * Add mail message(s) from the mail server as a message/rfc822
     * attachment.
     *
     * @param IMP_Indices $indices  An indices object.
     *
     * @return string  Subject string.
     *
     * @throws IMP_Exception
     */
    public function attachImapMessage($indices)
    {
        if (!count($indices)) {
            return false;
        }

        $attached = 0;
        foreach ($indices as $ob) {
            foreach ($ob->uids as $idx) {
                ++$attached;
                $contents = $GLOBALS['injector']->getInstance('IMP_Factory_Contents')->create(new IMP_Indices($ob->mbox, $idx));
                $headerob = $contents->getHeader();

                $part = new Horde_Mime_Part();
                $part->setCharset('UTF-8');
                $part->setType('message/rfc822');
                $part->setName(_("Forwarded Message"));
                $part->setContents($contents->fullMessageText(array(
                    'stream' => true
                )), array(
                    'usestream' => true
                ));

                // Throws IMP_Compose_Exception.
                $this->addAttachmentFromPart($part);

                $part->clearContents();
            }
        }

        if ($attached > 1) {
            return 'Fwd: ' . sprintf(_("%u Forwarded Messages"), $attached);
        }

        if ($name = $headerob->getValue('subject')) {
            $name = Horde_String::truncate($name, 80);
        } else {
            $name = _("[No Subject]");
        }

        return 'Fwd: ' . strval(new Horde_Imap_Client_Data_BaseSubject($name, array('keepblob' => true)));
    }

    /**
     * Determine the header information to display in the forward/reply.
     *
     * @param Horde_Mime_Headers $h  The headers object for the message.
     *
     * @return string  The header information for the original message.
     */
    protected function _getMsgHeaders($h)
    {
        $tmp = array();

        if (($ob = $h->getValue('date'))) {
            $tmp[_("Date")] = $ob;
        }

        if (($ob = strval($h->getOb('from')))) {
            $tmp[_("From")] = $ob;
        }

        if (($ob = strval($h->getOb('reply-to')))) {
            $tmp[_("Reply-To")] = $ob;
        }

        if (($ob = $h->getValue('subject'))) {
            $tmp[_("Subject")] = $ob;
        }

        if (($ob = strval($h->getOb('to')))) {
            $tmp[_("To")] = $ob;
        }

        if (($ob = strval($h->getOb('cc')))) {
            $tmp[_("Cc")] = $ob;
        }

        $text = '';

        if (!empty($tmp)) {
            $max = max(array_map(array('Horde_String', 'length'), array_keys($tmp))) + 2;

            foreach ($tmp as $key => $val) {
                $text .= Horde_String::pad($key . ': ', $max, ' ', STR_PAD_LEFT) . $val . "\n";
            }
        }

        return $text;
    }

    /**
     * Add an attachment referred to in a related part.
     *
     * @param IMP_Compose_Attachment $act_ob  Attachment data.
     * @param DOMElement $node                Node element containg the
     *                                        related reference.
     * @param string $attribute               Element attribute containing the
     *                                        related reference.
     */
    public function addRelatedAttachment(IMP_Compose_Attachment $atc_ob,
                                         DOMElement $node, $attribute)
    {
        $atc_ob->related = true;
        $node->setAttribute(self::RELATED_ATTR, $attribute . ';' . $atc_ob->id);
    }

    /**
     * Deletes all attachments.
     */
    public function deleteAllAttachments()
    {
        foreach (array_keys($this->_atc) as $key) {
            unset($this[$key]);
        }
    }

    /**
     * Obtains the cache ID for the session object.
     *
     * @return string  The message cache ID.
     */
    public function getCacheId()
    {
        return $this->_cacheid;
    }

    /**
     * Generate HMAC hash used to validate data on a session expiration. Uses
     * the unique compose cache ID of the expired message, the username, and
     * the secret key of the server to generate a reproducible value that can
     * be validated if session data doesn't exist.
     *
     * @param string $cacheid  The cache ID to use. If null, uses cache ID of
     *                         the compose object.
     * @param string $user     The user ID to use. If null, uses the current
     *                         authenticated username.
     *
     * @return string  The HMAC hash string.
     */
    public function getHmac($cacheid = null, $user = null)
    {
        global $conf, $registry;

        return hash_hmac(
            'sha1',
            (is_null($cacheid) ? $this->getCacheId() : $cacheid) . '|' .
                (is_null($user) ? $registry->getAuth() : $user),
            $conf['secret_key']
        );
    }

    /**
     * How many more attachments are allowed?
     *
     * @return mixed  Returns true if no attachment limit.
     *                Else returns the number of additional attachments
     *                allowed.
     */
    public function additionalAttachmentsAllowed()
    {
        global $conf;

        return empty($conf['compose']['attach_count_limit'])
            ? true
            : ($conf['compose']['attach_count_limit'] - count($this));
    }

    /**
     * What is the maximum attachment size?
     *
     * @return integer  The maximum attachment size (in bytes).
     */
    public function maxAttachmentSize()
    {
        $size = $GLOBALS['session']->get('imp', 'file_upload');

        return empty($GLOBALS['conf']['compose']['attach_size_limit'])
            ? $size
            : min($size, $GLOBALS['conf']['compose']['attach_size_limit']);
    }

    /**
     * Clean outgoing HTML (remove unexpected data URLs).
     *
     * @param Horde_Domhtml $html  The HTML data.
     */
    protected function _cleanHtmlOutput(Horde_Domhtml $html)
    {
        global $registry;

        $xpath = new DOMXPath($html->dom);

        foreach ($xpath->query('//*[@src]') as $node) {
            $src = $node->getAttribute('src');

            /* Check for attempts to sneak data URL information into the
             * output. */
            if (Horde_Url_Data::isData($src)) {
                if (IMP_Compose_HtmlSignature::isSigImage($node, true)) {
                    /* This is HTML signature image data. Convert to an
                     * attachment. */
                    $sig_img = new Horde_Url_Data($src);
                    if ($sig_img->data) {
                        $data_part = new Horde_Mime_Part();
                        $data_part->setContents($sig_img->data);
                        $data_part->setType($sig_img->type);

                        try {
                            $this->addRelatedAttachment(
                                $this->addAttachmentFromPart($data_part),
                                $node,
                                'src'
                            );
                        } catch (IMP_Compose_Exception $e) {
                            // Remove image on error.
                        }
                    }
                }

                $node->removeAttribute('src');
            } elseif (strcasecmp($node->tagName, 'IMG') === 0) {
                /* Check for smileys. They live in the JS directory, under
                 * the base ckeditor directory, so search for that and replace
                 * with the filesystem information if found (Request
                 * #13051). Need to ignore other image links that may have
                 * been explicitly added by the user. */
                $js_path = strval(Horde::url($registry->get('jsuri', 'horde'), true));
                if (stripos($src, $js_path . '/ckeditor') === 0) {
                    $file = str_replace(
                        $js_path,
                        $registry->get('jsfs', 'horde'),
                        $src
                    );

                    if (is_readable($file)) {
                        $data_part = new Horde_Mime_Part();
                        $data_part->setContents(file_get_contents($file));
                        $data_part->setName(basename($file));

                        try {
                            $this->addRelatedAttachment(
                                $this->addAttachmentFromPart($data_part),
                                $node,
                                'src'
                            );
                        } catch (IMP_Compose_Exception $e) {
                            // Keep existing data on error.
                        }
                    }
                }
            }
        }
    }

    /**
     * Converts an HTML part to a multipart/related part, if necessary.
     *
     * @param Horde_Domhtml $html    HTML data.
     * @param Horde_Mime_Part $part  The HTML part.
     *
     * @return Horde_Mime_Part  The part to add to the compose output.
     */
    protected function _convertToRelated(Horde_Domhtml $html,
                                         Horde_Mime_Part $part)
    {
        $r_part = false;
        foreach ($this as $atc) {
            if ($atc->related) {
                $r_part = true;
                break;
            }
        }

        if (!$r_part) {
            return $part;
        }

        /* Create new multipart/related part. */
        $related = new Horde_Mime_Part();
        $related->setType('multipart/related');
        /* Get the CID for the 'root' part. Although by default the first part
         * is the root part (RFC 2387 [3.2]), we may as well be explicit and
         * put the CID in the 'start' parameter. */
        $related->setContentTypeParameter('start', $part->setContentId());
        $related->addPart($part);

        /* HTML iteration is from child->parent, so need to gather related
         * parts and add at end after sorting to generate a more sensible
         * attachment list. */
        $add = array();

        foreach ($html as $node) {
            if (($node instanceof DOMElement) &&
                $node->hasAttribute(self::RELATED_ATTR)) {
                list($attr_name, $atc_id) = explode(';', $node->getAttribute(self::RELATED_ATTR));

                /* If attachment can't be found, ignore. */
                if ($r_atc = $this[$atc_id]) {
                    if ($r_atc->linked) {
                        $attr = strval($r_atc->link_url);
                    } else {
                        $related_part = $r_atc->getPart(true);
                        $attr = 'cid:' . $related_part->setContentId();
                        $add[] = $related_part;
                    }

                    $node->setAttribute($attr_name, $attr);
                }

                $node->removeAttribute(self::RELATED_ATTR);
            }
        }

        array_map(array($related, 'addPart'), array_reverse($add));

        return $related;
    }

    /**
     * Adds linked attachments to message.
     *
     * @param string &$body  Plaintext data.
     * @param mixed $html    HTML data (Horde_Domhtml) or null.
     *
     * @throws IMP_Compose_Exception
     */
    protected function _linkAttachments(&$body, $html)
    {
        global $conf;

        if (empty($conf['compose']['link_attachments'])) {
            return;
        }

        $link_all = false;
        $linked = array();

        if (!empty($conf['compose']['link_attach_size_hard'])) {
            $limit = intval($conf['compose']['link_attach_size_hard']);
            foreach ($this as $val) {
                if (($limit -= $val->getPart()->getBytes()) < 0) {
                    $link_all = true;
                    break;
                }
            }
        }

        foreach (iterator_to_array($this) as $key => $val) {
            if ($link_all && !$val->linked) {
                $val = new IMP_Compose_Attachment($this, $val->getPart(), $val->storage->getTempFile());
                $val->forceLinked = true;
                unset($this[$key]);
                $this[$key] = $val;
            }

            if ($val->linked && !$val->related) {
                $linked[] = $val;
            }
        }

        if (empty($linked)) {
            return;
        }

        if ($del_time = IMP_Compose_LinkedAttachment::keepDate(false)) {
            /* Subtract 1 from time to get the last day of the previous
             * month. */
            $expire = ' (' . sprintf(_("links will expire on %s"), strftime('%x', $del_time - 1)) . ')';
        }

        $body .= "\n-----\n" . _("Attachments") . $expire . ":\n";
        if ($html) {
            $body = $html->getBody();
            $dom = $html->dom;

            $body->appendChild($dom->createElement('HR'));
            $body->appendChild($div = $dom->createElement('DIV'));
            $div->appendChild($dom->createElement('H4', _("Attachments") . $expire . ':'));
            $div->appendChild($ol = $dom->createElement('OL'));
        }

        $i = 0;
        foreach ($linked as $val) {
            $apart = $val->getPart();
            $name = $apart->getName(true);
            $size = IMP::sizeFormat($apart->getBytes());
            $url = strval($val->link_url->setRaw(true));

            $body .= "\n" . (++$i) . '. ' .
                $name . ' (' . $size . ') [' . $apart->getType() . "]\n" .
                sprintf(_("Download link: %s"), $url) . "\n";

            if ($html) {
                $ol->appendChild($li = $dom->createElement('LI'));
                $li->appendChild($dom->createElement('STRONG', $name));
                $li->appendChild($dom->createTextNode(' (' . $size . ') [' . htmlspecialchars($apart->getType()) . ']'));
                $li->appendChild($dom->createElement('BR'));
                $li->appendChild($dom->createTextNode(_("Download link") . ': '));
                $li->appendChild($a = $dom->createElement('A', htmlspecialchars($url)));
                $a->setAttribute('href', $url);
            }
        }
    }

    /**
     * Regenerates body text for use in the compose screen from IMAP data.
     *
     * @param IMP_Contents $contents  An IMP_Contents object.
     * @param array $options          Additional options:
     * <ul>
     *  <li>html: (boolean) Return text/html part, if available.</li>
     *  <li>imp_msg: (integer) If non-empty, the message data was created by
     *               IMP. Either:
     *   <ul>
     *    <li>self::COMPOSE</li>
     *    <li>self::FORWARD</li>
     *    <li>self::REPLY</li>
     *   </ul>
     *  </li>
     *  <li>replylimit: (boolean) Enforce length limits?</li>
     *  <li>toflowed: (boolean) Do flowed conversion?</li>
     * </ul>
     *
     * @return mixed  Null if bodypart not found, or array with the following
     *                keys:
     *   - charset: (string) The guessed charset to use.
     *   - flowed: (Horde_Text_Flowed) A flowed object, if the text is flowed.
     *             Otherwise, null.
     *   - id: (string) The MIME ID of the bodypart.
     *   - mode: (string) Either 'text' or 'html'.
     *   - text: (string) The body text.
     */
    protected function _getMessageText($contents, array $options = array())
    {
        global $conf, $injector, $notification, $prefs, $session;

        $body_id = null;
        $mode = 'text';
        $options = array_merge(array(
            'imp_msg' => self::COMPOSE
        ), $options);

        if (!empty($options['html']) &&
            $session->get('imp', 'rteavail') &&
            (($body_id = $contents->findBody('html')) !== null)) {
            $mime_message = $contents->getMIMEMessage();

            switch ($mime_message->getPrimaryType()) {
            case 'multipart':
                if (($body_id != '1') &&
                    ($mime_message->getSubType() == 'mixed') &&
                    !Horde_Mime::isChild('1', $body_id)) {
                    $body_id = null;
                } else {
                    $mode = 'html';
                }
                break;

            default:
                if (strval($body_id) != '1') {
                    $body_id = null;
                } else {
                    $mode = 'html';
                }
                break;
            }
        }

        if (is_null($body_id)) {
            $body_id = $contents->findBody();
            if (is_null($body_id)) {
                return null;
            }
        }

        $part = $contents->getMIMEPart($body_id);
        $type = $part->getType();
        $part_charset = $part->getCharset();

        $msg = Horde_String::convertCharset($part->getContents(), $part_charset, 'UTF-8');

        /* Enforce reply limits. */
        if (!empty($options['replylimit']) &&
            !empty($conf['compose']['reply_limit'])) {
            $limit = $conf['compose']['reply_limit'];
            if (Horde_String::length($msg) > $limit) {
                $msg = Horde_String::substr($msg, 0, $limit) . "\n" . _("[Truncated Text]");
            }
        }

        if ($mode == 'html') {
            $dom = $injector->getInstance('Horde_Core_Factory_TextFilter')->filter(
                $msg,
                'Xss',
                array(
                    'charset' => $this->charset,
                    'return_dom' => true,
                    'strip_style_attributes' => false
                )
            );

            /* If we are replying to a related part, and this part refers
             * to local message parts, we need to move those parts into this
             * message (since the original message may disappear during the
             * compose process). */
            if ($related_part = $contents->findMimeType($body_id, 'multipart/related')) {
                $this->_setMetadata('related_contents', $contents);
                $related_ob = new Horde_Mime_Related($related_part);
                $related_ob->cidReplace($dom, array($this, '_getMessageTextCallback'), $part_charset);
                $this->_setMetadata('related_contents', null);
            }

            /* Convert any Data URLs to attachments. */
            $xpath = new DOMXPath($dom->dom);
            foreach ($xpath->query('//*[@src]') as $val) {
                $data_url = new Horde_Url_Data($val->getAttribute('src'));
                if (strlen($data_url->data)) {
                    $data_part = new Horde_Mime_Part();
                    $data_part->setContents($data_url->data);
                    $data_part->setType($data_url->type);

                    try {
                        $atc = $this->addAttachmentFromPart($data_part);
                        $val->setAttribute('src', $atc->viewUrl());
                        $this->addRelatedAttachment($atc, $val, 'src');
                    } catch (IMP_Compose_Exception $e) {
                        $notification->push($e, 'horde.warning');
                    }
                }
            }

            $msg = $dom->returnBody();
        } elseif ($type == 'text/html') {
            $msg = $injector->getInstance('Horde_Core_Factory_TextFilter')->filter($msg, 'Html2text');
            $type = 'text/plain';
        }

        /* Always remove leading/trailing whitespace. The data in the
         * message body is not intended to be the exact representation of the
         * original message (use forward as message/rfc822 part for that). */
        $msg = trim($msg);

        if ($type == 'text/plain') {
            if ($prefs->getValue('reply_strip_sig') &&
                (($pos = strrpos($msg, "\n-- ")) !== false)) {
                $msg = rtrim(substr($msg, 0, $pos));
            }

            /* Remove PGP armored text. */
            $pgp = $injector->getInstance('Horde_Crypt_Pgp_Parse')->parseToPart($msg);
            if (!is_null($pgp)) {
                $msg = '';
                $pgp->buildMimeIds();
                foreach ($pgp->contentTypeMap() as $key => $val) {
                    if (strpos($val, 'text/') === 0) {
                        $msg .= $pgp[$key]->getContents();
                    }
                }
            }

            if ($part->getContentTypeParameter('format') == 'flowed') {
                $flowed = new Horde_Text_Flowed($msg, 'UTF-8');
                if (Horde_String::lower($part->getContentTypeParameter('delsp')) == 'yes') {
                    $flowed->setDelSp(true);
                }
                $flowed->setMaxLength(0);
                $msg = $flowed->toFixed(false);
            } else {
                /* If the input is *not* in flowed format, make sure there is
                 * no padding at the end of lines. */
                $msg = preg_replace("/\s*\n/U", "\n", $msg);
            }

            if (isset($options['toflowed'])) {
                $flowed = new Horde_Text_Flowed($msg, 'UTF-8');
                $msg = $options['toflowed']
                    ? $flowed->toFlowed(true)
                    : $flowed->toFlowed(false, array('nowrap' => true));
            }
        }

        if (strcasecmp($part->getCharset(), 'windows-1252') === 0) {
            $part_charset = 'ISO-8859-1';
        }

        return array(
            'charset' => $part_charset,
            'flowed' => isset($flowed) ? $flowed : null,
            'id' => $body_id,
            'mode' => $mode,
            'text' => $msg
        );
    }

    /**
     * Callback used in _getMessageText().
     *
     * @return Horde_Url
     */
    public function _getMessageTextCallback($id, $attribute, $node)
    {
        $atc = $this->addAttachmentFromPart($this->getMetadata('related_contents')->getMIMEPart($id));
        $this->addRelatedAttachment($atc, $node, $attribute);

        return $atc->viewUrl();
    }

    /**
     * Adds an attachment from Horde_Mime_Part data.
     *
     * @param Horde_Mime_Part $part  The object that contains the attachment
     *                               data.
     *
     * @return IMP_Compose_Attachment  Attachment object.
     * @throws IMP_Compose_Exception
     */
    public function addAttachmentFromPart($part)
    {
        /* Extract the data from the Horde_Mime_Part. */
        $atc_file = Horde::getTempFile('impatt');
        $stream = $part->getContents(array(
            'stream' => true
        ));
        rewind($stream);
        $dest_handle = fopen($atc_file, 'w+b');
        while (!feof($stream)) {
            fwrite($dest_handle, fread($stream, 1024));
        }
        fclose($dest_handle);
        $size = ftell($stream);
        if ($size === false) {
            throw new IMP_Compose_Exception(sprintf(_("Could not attach %s to the message."), $part->getName()));
        }

        return $this->_addAttachment(
            $atc_file,
            $size,
            $part->getName(true),
            $part->getType()
        );
    }

    /**
     * Add attachment from uploaded (form) data.
     *
     * @param string $field  The form field name.
     *
     * @return array  A list of IMP_Compose_Attachment objects (if
     *                successfully attached) or IMP_Compose_Exception objects
     *                (if error when attaching).
     * @throws IMP_Compose_Exception
     */
    public function addAttachmentFromUpload($field)
    {
        global $browser;

        try {
            $browser->wasFileUploaded($field, _("attachment"));
        } catch (Horde_Browser_Exception $e) {
            throw new IMP_Compose_Exception($e);
        }

        $finfo = array();
        if (is_array($_FILES[$field]['size'])) {
            for ($i = 0; $i < count($_FILES[$field]['size']); ++$i) {
                $tmp = array();
                foreach ($_FILES[$field] as $key => $val) {
                    $tmp[$key] = $val[$i];
                }
                $finfo[] = $tmp;
            }
        } else {
            $finfo[] = $_FILES[$field];
        }

        $out = array();

        foreach ($finfo as $val) {
            switch (empty($val['type']) ? $val['type'] : '') {
            case 'application/unknown':
            case '':
                $type = 'application/octet-stream';
                break;

            default:
                $type = $val['type'];
                break;
            }

            try {
                $out[] = $this->_addAttachment(
                    $val['tmp_name'],
                    $val['size'],
                    Horde_Util::dispelMagicQuotes($val['name']),
                    $type
                );
            } catch (IMP_Compose_Exception $e) {
                $out[] = $e;
            }
        }

        return $out;
    }

    /**
     * Adds an attachment to the outgoing compose message.
     *
     * @param string $atc_file  Temporary file containing attachment contents.
     * @param integer $bytes    Size of data, in bytes.
     * @param string $filename  Filename of data.
     * @param string $type      MIME type of data.
     *
     * @return IMP_Compose_Attachment  Attachment object.
     * @throws IMP_Compose_Exception
     */
    protected function _addAttachment($atc_file, $bytes, $filename, $type)
    {
        global $conf, $injector;

        $atc = new Horde_Mime_Part();
        $atc->setBytes($bytes);

        /* Try to determine the MIME type from 1) the extension and
         * then 2) analysis of the file (if available). */
        if (strlen($filename)) {
            $atc->setName($filename);
            if ($type == 'application/octet-stream') {
                $type = Horde_Mime_Magic::filenameToMIME($filename, false);
            }
        }

        $atc->setType($type);
        $atc->setHeaderCharset('UTF-8');

        if (($atc->getType() == 'application/octet-stream') ||
            ($atc->getPrimaryType() == 'text')) {
            $analyze = Horde_Mime_Magic::analyzeFile($atc_file, empty($conf['mime']['magic_db']) ? null : $conf['mime']['magic_db'], array(
                'nostrip' => true
            ));

            if ($analyze) {
                $analyze = Horde_Mime::decodeParam('Content-Type', $analyze);
                $atc->setType($analyze['val']);
                $atc->setCharset(isset($analyze['params']['charset']) ? $analyze['params']['charset'] : 'UTF-8');
            } else {
                $atc->setCharset('UTF-8');
            }
        }

        $atc_ob = new IMP_Compose_Attachment($this, $atc, $atc_file);

        /* Check for attachment size limitations. */
        $size_limit = null;
        if ($atc_ob->linked) {
            if (!empty($conf['compose']['link_attach_size_limit'])) {
                $linked = true;
                $size_limit = 'link_attach_size_limit';
            }
        } elseif (!empty($conf['compose']['attach_size_limit'])) {
            $linked = false;
            $size_limit = 'attach_size_limit';
        }

        if (!is_null($size_limit)) {
            $total_size = $conf['compose'][$size_limit] - $bytes;
            foreach ($this as $val) {
                if ($val->linked == $linked) {
                    $total_size -= $val->getPart()->getBytes();
                }
            }

            if ($total_size < 0) {
                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."));
            }
        }

        try {
            $injector->getInstance('Horde_Core_Hooks')->callHook(
                'compose_attachment',
                'imp',
                array($atc_ob)
            );
        } catch (Horde_Exception_HookNotSet $e) {}

        $this->_atc[$atc_ob->id] = $atc_ob;
        $this->changed = 'changed';

        return $atc_ob;
    }

    /**
     * Store draft compose data if session expires.
     *
     * @param Horde_Variables $vars  Object with the form data.
     */
    public function sessionExpireDraft(Horde_Variables $vars)
    {
        global $conf, $injector;

        if (empty($conf['compose']['use_vfs']) ||
            !isset($vars->composeCache) ||
            !isset($vars->composeHmac) ||
            !isset($vars->user) ||
            ($this->getHmac($vars->composeCache, $vars->user) != $vars->composeHmac)) {
            return;
        }

        $headers = array();
        foreach (array('to', 'cc', 'bcc', 'subject') as $val) {
            $headers[$val] = $vars->$val;
        }

        try {
            $body = $this->_saveDraftMsg($headers, $vars->message, array(
                'html' => $vars->rtemode,
                'priority' => $vars->priority,
                'readreceipt' => $vars->request_read_receipt
            ));

            $injector->getInstance('Horde_Core_Factory_Vfs')->create()->writeData(self::VFS_DRAFTS_PATH, hash('sha1', $vars->user), $body, true);
        } catch (Exception $e) {}
    }

    /**
     * Restore session expiration draft compose data.
     */
    public function recoverSessionExpireDraft()
    {
        global $conf, $injector, $notification;

        if (empty($conf['compose']['use_vfs'])) {
            return;
        }

        $filename = hash('sha1', $GLOBALS['registry']->getAuth());

        try {
            $vfs = $injector->getInstance('Horde_Core_Factory_Vfs')->create();

            if ($vfs->exists(self::VFS_DRAFTS_PATH, $filename)) {
                $data = $vfs->read(self::VFS_DRAFTS_PATH, $filename);
                $this->_saveDraftServer($data);
                $vfs->deleteFile(self::VFS_DRAFTS_PATH, $filename);
                $notification->push(
                    _("A message you were composing when your session expired has been recovered. You may resume composing your message by going to your Drafts mailbox."),
                    'horde.message',
                    array('sticky')
                );
            }
        } catch (Exception $e) {}
    }

    /**
     * If this object contains sufficient metadata, return an IMP_Contents
     * object reflecting that metadata.
     *
     * @return mixed  Either an IMP_Contents object or null.
     */
    public function getContentsOb()
    {
        return ($this->_replytype && ($indices = $this->getMetadata('indices')) && (count($indices) === 1))
            ? $GLOBALS['injector']->getInstance('IMP_Factory_Contents')->create($indices)
            : null;
    }

    /**
     * Return the reply type.
     *
     * @param boolean $base  Return the base reply type?
     *
     * @return string  The reply type, or null if not a reply.
     */
    public function replyType($base = false)
    {
        switch ($this->_replytype) {
        case self::FORWARD:
        case self::FORWARD_ATTACH:
        case self::FORWARD_BODY:
        case self::FORWARD_BOTH:
            return $base
                ? self::FORWARD
                : $this->_replytype;

        case self::REPLY:
        case self::REPLY_ALL:
        case self::REPLY_LIST:
        case self::REPLY_SENDER:
            return $base
                ? self::REPLY
                : $this->_replytype;

        case self::REDIRECT:
            return $this->_replytype;

        default:
            return null;
        }
    }

    /* Static methods. */

    /**
     * Is composing messages allowed?
     *
     * @return boolean  True if compose allowed.
     * @throws Horde_Exception
     */
    static public function canCompose()
    {
        try {
            return !$GLOBALS['injector']->getInstance('Horde_Core_Hooks')->callHook('disable_compose', 'imp');
        } catch (Horde_Exception_HookNotSet $e) {
            return true;
        }
    }

    /**
     * Can attachments be uploaded?
     *
     * @return boolean  True if attachments can be uploaded.
     */
    static public function canUploadAttachment()
    {
        return ($GLOBALS['session']->get('imp', 'file_upload') != 0);
    }

    /**
     * Shortcut function to convert text -> HTML for purposes of composition.
     *
     * @param string $msg  The message text.
     *
     * @return string  HTML text.
     */
    static public function text2html($msg)
    {
        return $GLOBALS['injector']->getInstance('Horde_Core_Factory_TextFilter')->filter($msg, 'Text2html', array(
            'always_mailto' => true,
            'flowed' => self::HTML_BLOCKQUOTE,
            'parselevel' => Horde_Text_Filter_Text2html::MICRO
        ));
    }

    /* ArrayAccess methods. */

    public function offsetExists($offset)
    {
        return isset($this->_atc[$offset]);
    }

    public function offsetGet($offset)
    {
        return isset($this->_atc[$offset])
            ? $this->_atc[$offset]
            : null;
    }

    public function offsetSet($offset, $value)
    {
        $this->_atc[$offset] = $value;
        $this->changed = 'changed';
    }

    public function offsetUnset($offset)
    {
        if (($atc = $this->_atc[$offset]) === null) {
            return;
        }

        $atc->delete();
        unset($this->_atc[$offset]);

        $this->changed = 'changed';
    }

    /* Magic methods. */

    /**
     * String representation: the cache ID.
     */
    public function __toString()
    {
        return $this->getCacheId();
    }

    /* Countable method. */

    /**
     * Returns the number of attachments currently in this message.
     *
     * @return integer  The number of attachments in this message.
     */
    public function count()
    {
        return count($this->_atc);
    }

    /* IteratorAggregate method. */

    /**
     */
    public function getIterator()
    {
        return new ArrayIterator($this->_atc);
    }

}
