<?php

require_once 'Horde/MIME.php';

/**
 * The virtual path to use for VFS data.
 */
define('IMP_VFS_ATTACH_PATH', '.horde/imp/compose');

/**
 * The virtual path to save linked attachments.
 */
define('IMP_VFS_LINK_ATTACH_PATH', '.horde/imp/attachments');

/**
 * The IMP_Compose:: class contains functions related to generating
 * outgoing mail messages.
 *
 * $Horde: imp/lib/Compose.php,v 1.107.2.2 2005/02/04 03:46:50 slusarz Exp $
 *
 * Copyright 2002-2005 Michael Slusarz <slusarz@bigworm.colorado.edu>
 *
 * See the enclosed file COPYING for license information (GPL). If you
 * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
 *
 * @author  Michael Slusarz <slusarz@bigworm.colorado.edu>
 * @version $Revision: 1.107.2.2 $
 * @since   IMP 4.0
 * @package IMP
 */
class IMP_Compose {

    /**
     * The cached attachment data.
     *
     * @var array $_cache
     */
    var $_cache = array();

    /**
     * For findBody, the MIME ID of the "body" part.
     *
     * @var string $_mimeid
     */
    var $_mimeid = null;

    /**
     * The aggregate size of all attachments (in bytes).
     *
     * @var integer $_size
     */
    var $_size = 0;

    /**
     * Constructor
     *
     * @access public
     *
     * @param optional array $params  Parameters needed.
     */
    function IMP_Compose($params = array())
    {
        if (isset($params['cacheID'])) {
            $this->_retrieveMimeCache($params['cacheID']);
        }
    }

    /**
     * Send a message.
     *
     * @access public
     *
     * @param string $email                 The e-mail list to send to.
     * @param object IMP_Headers &$headers  The IMP_Headers object holding
     *                                      this messages headers.
     * @param mixed &$message               Either the message text (string)
     *                                      or a MIME_Message object that
     *                                      contains the text to send.
     * @param string $charset               The charset that was used for the
     *                                      headers.
     *
     * @return mixed  True on success, PEAR_Error object on error.
     */
    function sendMessage($email, &$headers, &$message, $charset)
    {
        global $conf;

        require_once 'Mail.php';

        /* We don't actually want to alter the contents of the $conf['mailer']
           array, so we make a copy of the current settings. We will apply
           our modifications (if any) to the copy, instead. */
        $params = $conf['mailer']['params'];

        /* Force the SMTP host and port value to the current SMTP server if
         * one has been selected for this connection. */
        if (!empty($_SESSION['imp']['smtphost'])) {
            $params['host'] = $_SESSION['imp']['smtphost'];
        }
        if (!empty($_SESSION['imp']['smtpport'])) {
            $params['port'] = $_SESSION['imp']['smtpport'];
        }

        /* If SMTP authentication has been requested, use either the username
           and password provided in the configuration or populate the username
           and password fields based on the current values for the user.
           Note that we assume that the username and password values from the
           current IMAP / POP3 connection are valid for SMTP authentication as
           well. */
        if (!empty($params['auth']) && empty($params['username'])) {
            $params['username'] = $_SESSION['imp']['user'];
            $params['password'] = Secret::read(Secret::getKey('imp'), $_SESSION['imp']['pass']);
        }

        /* Add the site headers. */
        $headers->addSiteHeaders();

        /* If $message is a string, we need to get a MIME_Message
           object to encode the headers. */
        if (is_string($message)) {
            $msg = $message;
            $mime_message = &new MIME_Message($_SESSION['imp']['maildomain']);
            $headerArray = $mime_message->encode($headers->toArray(), $charset);
        } else {
            $msg = $message->toString();
            $headerArray = $message->encode($headers->toArray(), $charset);
        }

        /* Make sure the message has a trailing newline. */
        if (substr($msg, -1) != "\n") {
            $msg .= "\n";
        }

        $mailer = &Mail::factory($conf['mailer']['type'], $params);
        if (is_a($mailer, 'PEAR_Error')) {
            return $mailer;
        }

        return $mailer->send(MIME::encodeAddress($email, null, $_SESSION['imp']['maildomain']), $headerArray, $msg);
    }

    /**
     * Finds the main "body" text part (if any) in a message.
     *
     * @param object IMP_Contents &$imp_contents  An IMP_Contents object.
     *
     * @return string  The text of the "body" part of the message.
     *                 Returns an empty string if no "body" found.
     */
    function findBody(&$imp_contents)
    {
        if (is_null($this->_mimeid)) {
            $this->_mimeid = $imp_contents->findBody();
            if (is_null($this->_mimeid)) {
                return '';
            }
        }

        $mime_part = &$imp_contents->getDecodedMIMEPart($this->_mimeid);
        $body = $mime_part->getContents();

        //if ($mime_message->getType() == 'multipart/encrypted') {
            /* TODO: Maybe someday I can figure out how to show embedded
             * text parts here.  But for now, just output this message. */
        //    return '[' . _("Original message was encrypted") . ']';
        //}

        if ($mime_part->getSubType() == 'html') {
            require_once 'Horde/Text/Filter.php';
            return Text_Filter::filter($body, 'html2text', array('wrap' => ($mime_part->getContentTypeParameter('format') != 'flowed')));
        } else {
            return $body;
        }
    }

    /**
     * Returns the ID of the MIME part containing the "body".
     *
     * @param object IMP_Contents &$imp_contents  An IMP_Contents object.
     *
     * @return string  The ID of the mime part's body.
     */
    function getBodyId(&$imp_contents)
    {
        if (is_null($this->_mimeid)) {
            $this->findBody($imp_contents);
        }
        return $this->_mimeid;
    }

    /**
     * Determine the reply text for a message.
     *
     * @param object IMP_Contents &$imp_contents  An IMP_Contents object.
     * @param string $from                        The email address of the
     *                                            original sender.
     * @param object IMP_Headers &$h              The IMP_Headers object for
     *                                            the message.
     *
     * @return string  The text of the body part of the message to use
     *                 for the reply.
     */
    function replyMessage(&$imp_contents, $from, &$h)
    {
        global $prefs;

        if (!$prefs->getValue('reply_quote')) {
            return '';
        }

        $mime_message = $imp_contents->getMIMEMessage();
        $msg = $this->findBody($imp_contents);
        $msg = $mime_message->replaceEOL($msg, "\n");

        if (!is_null($this->_mimeid)) {
            /* If the input is *not* in flowed format, make sure there is no
             * padding at the end of lines. */
            $old_part = $mime_message->getPart($this->_mimeid);
            if ($old_part->getContentTypeParameter('format') != 'flowed') {
                $msg = preg_replace("/\s*\n/U", "\n", $msg);
            }

            $msg = String::convertCharset($msg, $old_part->getCharset());

            require_once 'Text/Flowed.php';
            $flowed = &new Text_Flowed($msg);
            $msg = $flowed->toFlowed(true);
        }

        if (empty($msg)) {
            $msg = '[' . _("No message body text") . ']';
        } elseif ($prefs->getValue('reply_headers') && !empty($h)) {
            $msghead = '----- ';
            if (($from = $h->getFromAddress())) {
                $msghead .= sprintf(_("Message from %s"), $from);
            } else {
                $msghead .= _("Message");
            }

            /* Extra '-'s line up with "End Message" below. */
            $msghead .= " ---------\n";

            if (($date_ob = $h->getOb('date', true))) {
                $msghead .= _("    Date: ") . $date_ob . "\n";
            }
            if (($from_ob = MIME::addrArray2String($h->getOb('from')))) {
                $msghead .= _("    From: ") . $from_ob . "\n";
            }
            if (($rep_ob = MIME::addrArray2String($h->getOb('reply_to')))) {
                $msghead .= _("Reply-To: ") . $rep_ob . "\n";
            }
            if (($sub_ob = $h->getOb('subject', true))) {
                $msghead .= _(" Subject: ") . $sub_ob . "\n";
            }
            if (($to_ob = MIME::addrArray2String($h->getOb('to')))) {
                $msghead .= _("      To: ") . $to_ob . "\n";
            }
            $msg = $msghead . "\n\n" . $msg;
            if (!empty($from)) {
                $msg .= "\n\n" . '----- ' . sprintf(_("End message from %s"), $from) . " -----\n";
            } else {
                $msg .= "\n\n" . '----- ' . _("End message") . " -----\n";
            }
        } else {
            $msg = $this->_expandAttribution($prefs->getValue('attrib_text'), $from, $h) . "\n\n" . $msg;
        }

        return $msg . "\n";
    }

    /**
     * Determine the text for a forwarded message.
     *
     * @param object IMP_Contents &$imp_contents  An IMP_Contents object.
     * @param object IMP_Headers &$h              The IMP_Headers object for
     *                                            the message.
     *
     * @return string  The text of the body part of the message to use
     *                 for the forward.
     */
    function forwardMessage(&$imp_contents, &$h)
    {
        require_once 'Horde/Text.php';

        $msg = "\n\n\n----- ";

        if (($from = $h->getFromAddress())) {
            $msg .= sprintf(_("Forwarded message from %s"), $from);
        } else {
            $msg .= _("Forwarded message");
        }

        $msg .= " -----\n";

        if (($date = $h->getOb('date', true))) {
            $msg .= _("    Date: ") . $date . "\n";
        }
        if (($from_ob = MIME::addrArray2String($h->getOb('from')))) {
            $msg .= _("    From: ") . $from_ob . "\n";
        }
        if (($rep_ob = MIME::addrArray2String($h->getOb('reply_to')))) {
            $msg .= _("Reply-To: ") . $rep_ob . "\n";
        }
        if (($subject = $h->getOb('subject', true))) {
            $msg .= _(" Subject: ") . $subject . "\n";
        }
        if (($to_ob = MIME::addrArray2String($h->getOb('to')))) {
            $msg .= _("      To: ") . $to_ob . "\n";
        }

        $msg .= "\n";

        $fwd_msg = $this->findBody($imp_contents);
        if (!is_null($this->_mimeid)) {
            $mime_message = $imp_contents->getMIMEMessage();
            $old_part = $mime_message->getPart($this->_mimeid);
            $fwd_msg = String::convertCharset($fwd_msg, $old_part->getCharset());
        }

        $msg .= $fwd_msg;
        $msg .= "\n\n----- " . _("End forwarded message") . " -----\n";

        return $msg;
    }

    /**
     * Adds an attachment to a MIME_Part from an uploaded file.
     * The actual attachment data is stored in a separate file - the
     * MIME_Part information entries 'temp_filename' and 'temp_filetype'
     * are set with this information.
     *
     * @access public
     *
     * @param string $name         The input field name from the form.
     * @param string $disposition  The disposition to use for the file.
     *
     * @return mixed  Returns the filename on success.
     *                Returns PEAR_Error on error.
     */
    function addUploadAttachment($name, $disposition)
    {
        global $conf;

        $res = Browser::wasFileUploaded($name, _("attachment"));
        if (is_a($res, 'PEAR_Error')) {
            return $res;
        }

        $filename = basename($_FILES[$name]['name']);
        $tempfile = $_FILES[$name]['tmp_name'];

        /* Check for filesize limitations. */
        if (!empty($conf['compose']['attach_size_limit']) &&
            (($conf['compose']['attach_size_limit'] - $this->sizeOfAttachments() - $_FILES[$name]['size']) < 0)) {
            return PEAR::raiseError(sprintf(_("Attached file \"%s\" exceeds the attachment size limits. File NOT attached."), $filename), 'horde.error');
        }

        /* Store the data in a MIME_Part. Some browsers do not send the MIME
           type so try an educated guess. */
        if (!empty($_FILES[$name]['type']) &&
            ($_FILES[$name]['type'] != 'application/octet-stream')) {
            $part = &new MIME_Part($_FILES[$name]['type']);
        } else {
            require_once 'Horde/MIME/Magic.php';
            /* Try to determine the MIME type from 1) analysis of the file
               (if available) and, if that fails, 2) from the extension. */
            if (!($type = MIME_Magic::analyzeFile($tempfile))) {
                $type = MIME_Magic::filenameToMIME($filename, false);
            }
            $part = &new MIME_Part($type);
        }
        $part->setCharset(NLS::getCharset());
        $part->setName($filename);
        $part->setBytes($_FILES[$name]['size']);
        if ($disposition) {
            $part->setDisposition($disposition);
        }

        if ($conf['compose']['use_vfs']) {
            $attachment = $tempfile;
        } else {
            $attachment = Horde::getTempFile('impatt', false);
            move_uploaded_file($tempfile, $attachment);
        }

        /* Store the data. */
        $result = $this->_storeAttachment($part, $attachment);
        if (is_a($result, 'PEAR_Error')) {
            return $result;
        }

        return $part->getName();
    }

    /**
     * Adds an attachment to a MIME_Part from data existing in the part.
     *
     * @access public
     *
     * @param object MIME_Part &$part  The MIME_Part object that contains the
     *                                 attachment data.
     *
     * @return object PEAR_Error  Returns a PEAR_Error object on error.
     */
    function addMIMEPartAttachment(&$part)
    {
        global $conf;

        $vfs = $conf['compose']['use_vfs'];

        /* Decode the contents. */
        $part->transferDecodeContents();

        /* Extract the data from the currently existing MIME_Part and then
           delete it. If this is an unknown MIME part, we must save to a
           temporary file to run the file analysis on it. */
        if (!$vfs || ($part->getType() == 'application/octet-stream')) {
            $attachment = Horde::getTempFile('impatt', $vfs);
            $fp = fopen($attachment, 'w');
            fwrite($fp, $part->getContents());
            fclose($fp);

            if ($part->getType() == 'application/octet-stream') {
                require_once 'Horde/MIME/Magic.php';

                /* Try to determine the MIME type from 1) analysis of the file
                   (if available) and, if that fails, 2) from the extension. */
                if (!($type = MIME_Magic::analyzeFile($attachment))) {
                    $type = MIME_Magic::filenameToMIME($attachment, false);
                }
                $part->setType($type);
            }
        }

        /* Set the size of the Part explicitly since we delete the contents
           later on in this function. */
        $part->setBytes($part->getBytes());

        /* Check for filesize limitations. */
        if (!empty($conf['compose']['attach_size_limit']) &&
            (($conf['compose']['attach_size_limit'] - $this->sizeOfAttachments() - $part->getBytes()) < 0)) {
            return PEAR::raiseError(sprintf(_("Attached file \"%s\" exceeds the attachment size limits. File NOT attached."), $part->getName()), 'horde.error');
        }

        /* Store the data. */
        if ($vfs) {
            $vfs_data = $part->getContents();
            $part->clearContents();
            $this->_storeAttachment($part, $vfs_data, false);
        } else {
            $part->clearContents();
            $this->_storeAttachment($part, $attachment);
        }
    }

    /**
     * Stores the attachment data in its correct location.
     *
     * @access private
     *
     * @param object MIME_Part &$part     The MIME_Part of the attachment.
     * @param string $data                Either the filename of the
     *                                    attachment or, if $vfs_file is
     *                                    false, the attachment data.
     * @param optional boolean $vfs_file  If using VFS, is $data a filename?
     */
    function _storeAttachment(&$part, $data, $vfs_file = true)
    {
        global $conf;

        /* Store in VFS. */
        if ($conf['compose']['use_vfs']) {
            require_once 'VFS.php';
            require_once 'VFS/GC.php';
            $vfs = &VFS::singleton($conf['vfs']['type'], Horde::getDriverConfig('vfs', $conf['vfs']['type']));
            VFS_GC::gc($vfs, IMP_VFS_ATTACH_PATH, 86400);
            $cacheID = md5(mt_rand() . microtime());
            if ($vfs_file) {
                $result = $vfs->write(IMP_VFS_ATTACH_PATH, $cacheID, $data, true);
            } else {
                $result = $vfs->writeData(IMP_VFS_ATTACH_PATH, $cacheID, $data, true);
            }
            if (is_a($result, 'PEAR_Error')) {
                return $result;
            }
            $part->setInformation('temp_filename', $cacheID);
            $part->setInformation('temp_filetype', 'vfs');
        } else {
            chmod($data, 0600);
            $part->setInformation('temp_filename', $data);
            $part->setInformation('temp_filetype', 'file');
        }

        /* Add the size information to the counter. */
        $this->_size += $part->getBytes();

        $this->_cache[] = $part;
    }

    /**
     * Delete attached files.
     *
     * @access public
     *
     * @param mixed $number  Either a single integer or an array of integers
     *                       corresponding to the attachment position.
     *
     * @return array  The list of deleted filenames.
     */
    function deleteAttachment($number)
    {
        global $conf;

        $names = array();

        if (!is_array($number)) {
            $number = array($number);
        }

        foreach ($number as $val) {
            $val--;
            $part = &$this->_cache[$val];
            $filename = $part->getInformation('temp_filename');
            if ($part->getInformation('temp_filetype') == 'vfs') {
                /* Delete from VFS. */
                require_once 'VFS.php';
                $vfs = &VFS::singleton($conf['vfs']['type'], Horde::getDriverConfig('vfs', $conf['vfs']['type']));
                $vfs->deleteFile(IMP_VFS_ATTACH_PATH, $filename);
            } else {
                /* Delete from filesystem. */
                @unlink($filename);
            }

            $part->setInformation('temp_filename', '');
            $part->setInformation('temp_filetype', '');

            $names[] = $part->getName(false, true);

            /* Remove the size information from the counter. */
            $this->_size -= $part->getBytes();

            unset($this->_cache[$val]);
        }

        /* Reorder the attachments. */
        $this->_cache = array_values($this->_cache);

        return $names;
    }

    /**
     * Deletes all attachments.
     *
     * @access public
     */
    function deleteAllAttachments()
    {
        $numbers = array();

        for ($i = 1; $i <= $this->numberOfAttachments(); $i++) {
            $numbers[] = $i;
        }

        $this->deleteAttachment($numbers);
    }

    /**
     * Updates information in a specific attachment.
     *
     * @access public
     *
     * @param integer $number  The attachment to update.
     * @param array $params    An array of update information.
     * <pre>
     * 'disposition'  --  The Content-Disposition value.
     * 'description'  --  The Content-Description value.
     * </pre>
     */
    function updateAttachment($number, $params)
    {
        $number--;
        $this->_cache[$number]->setDisposition($params['disposition']);
        $this->_cache[$number]->setDescription($params['description']);
    }

    /**
     * Returns the list of current attachments.
     *
     * @access public
     *
     * @return array  The list of attachments.
     */
    function getAttachments()
    {
        return $this->_cache;
    }

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

    /**
     * Returns the size of the attachments in bytes.
     *
     * @access public
     *
     * @return integer  The size of the attachments (in bytes).
     */
    function sizeOfAttachments()
    {
        return $this->_size;
    }

    /**
     * Build a single attachment part with its data.
     *
     * @access public
     *
     * @param integer $id  The ID of the part to rebuild.
     *
     * @return object MIME_Part  The MIME_Part with its contents.
     */
    function buildAttachment($id)
    {
        $part = $this->_cache[($id - 1)];
        $this->_buildPartData($part);
        return $part;
    }

    /**
     * Build the MIME_Part attachments from the temporary file data.
     *
     * @access public
     *
     * @param object MIME_Part &$base  The base MIME_Part object to add the
     *                                 attachments to.
     */
    function buildAllAttachments(&$base)
    {
        foreach ($this->_cache as $part) {
            /* Store the data inside the current part. */
            $this->_buildPartData($part);

            /* Add to the base part. */
            $base->addPart($part);
        }
    }

    /**
     * Takes the temporary data for a single part and puts it into the
     * contents of that part.
     *
     * @access private
     *
     * @param object MIME_Part &$part  The part to rebuild data into.
     */
    function _buildPartData(&$part)
    {
        global $conf;

        $filename = $part->getInformation('temp_filename');
        if ($part->getInformation('temp_filetype') == 'vfs') {
            require_once 'VFS.php';
            $vfs = &VFS::singleton($conf['vfs']['type'], Horde::getDriverConfig('vfs', $conf['vfs']['type']));
            $data = $vfs->read(IMP_VFS_ATTACH_PATH, $filename);
        } else {
            $fd = fopen($filename, 'rb');
            $data = fread($fd, filesize($filename));
            fclose($fd);
        }

        /* Set the part's contents to the raw attachment data. */
        $part->setContents($data);
    }

    /**
     * Expand macros in attribution text when replying to messages.
     *
     * @access private
     *
     * @param string $line            The line of attribution text.
     * @param string $from            The email address of the original
     *                                sender.
     * @param object IMP_Headers &$h  The IMP_Headers object for the message.
     *
     * @return string  The attribution text.
     */
    function _expandAttribution($line, $from, &$h)
    {
        $addressList = '';
        $nameList = '';

        /* First we'll get a comma seperated list of email addresses
           and a comma seperated list of personal names out of $from
           (there just might be more than one of each). */
        foreach (@imap_rfc822_parse_adrlist($from, '') as $entry) {
            if (isset($entry->mailbox) && isset($entry->host)) {
                if (strlen($addressList) > 0) {
                    $addressList .= ', ';
                }
                $addressList .= $entry->mailbox . '@' . $entry->host;
            } elseif (isset($entry->mailbox)) {
                if (strlen($addressList) > 0) {
                    $addressList .= ', ';
                }
                $addressList .= $entry->mailbox;
            }
            if (isset($entry->personal)){
                if (strlen($nameList) > 0) {
                    $nameList .= ', ';
                }
                $nameList .= $entry->personal;
            } elseif (isset($entry->mailbox)) {
                if (strlen($nameList) > 0) {
                    $nameList .= ', ';
                }
                $nameList .= $entry->mailbox;
            }
        }

        /* Define the macros. */
        if (!($subject = $h->getOb('subject', true))) {
            $subject = _("[No Subject]");
        }
        $udate = strtotime($h->getOb('date', true));

        $match = array(
            /* New line. */
            '/%n/' => "\n",

            /* The '%' character. */
            '/%%/' => '%',

            /* Name and email address of original sender. */
            '/%f/' => $from,

            /* Senders email address(es). */
            '/%a/' => $addressList,

            /* Senders name(s). */
            '/%p/' => $nameList,

            /* RFC 822 date and time. */
            '/%r/' => $h->getOb('date', true),

            /* Date as ddd, dd mmm yyyy. */
            '/%d/' => String::convertCharset(@strftime("%a, %d %b %Y", $udate), NLS::getExternalCharset()),

            /* Date in locale's default. */
            '/%x/' => String::convertCharset(@strftime("%x", $udate), NLS::getExternalCharset()),

            /* Date and time in locale's default. */
            '/%c/' => String::convertCharset(@strftime("%c", $udate), NLS::getExternalCharset()),

            /* Message-ID. */
            '/%m/' => $h->getOb('message_id'),

            /* Message subject. */
            '/%s/' => $subject
        );

        return (preg_replace(array_keys($match), array_values($match), $line));
    }

    /**
     * Obtains the cached array of MIME_Parts to be attached to this message.
     *
     * @access private
     *
     * @param string $cacheID  The cacheID of the session object.
     */
    function _retrieveMimeCache($cacheID)
    {
        if ($cacheID) {
            require_once 'Horde/SessionObjects.php';
            $cacheSess = &Horde_SessionObjects::singleton();
            $result = $cacheSess->query($cacheID);
            $cacheSess->setPruneFlag($cacheID, true);
            $this->_cache = &$result['cache'];
            $this->_size = &$result['size'];
        }
    }

    /**
     * Obtains the cache ID for the session object that contains the
     * MIME_Part objects to be attached to this message.
     * This function needs to be run at least once per pageload to save the
     * session object.
     *
     * @access public
     *
     * @return string  The message cache ID if the object needs to be saved.
     *                 Else, false is returned.
     */
    function getMessageCacheId()
    {
        if (!empty($this->_cache)) {
            require_once 'Horde/SessionObjects.php';
            $cacheSess = &Horde_SessionObjects::singleton();
            $store = array(
                'cache' => $this->_cache,
                'size' => $this->_size
            );
            return $cacheSess->storeOid($store);
        } else {
            return false;
        }
    }

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

        if (!empty($conf['compose']['attach_count_limit'])) {
            return $conf['compose']['attach_count_limit'] - $this->numberOfAttachments();
        } else {
            return true;
        }
    }

    /**
     * What is the maximum attachment size allowed?
     *
     * @access public
     *
     * @return integer  The maximum attachment size allowed (in bytes).
     */
    function maxAttachmentSize()
    {
        global $conf, $imp;

        $size = $imp['file_upload'];

        if (!empty($conf['compose']['attach_size_limit'])) {
            $size = min($size, max($conf['compose']['attach_size_limit'] - $this->sizeOfAttachments(), 0));
        }

        return $size;
    }

    /**
     * Adds the attachments to the message (in the case of a forward with
     * attachments).
     *
     * @param object MIME_Message &$message  The MIME_Message object.
     *
     * @return array  An array of PEAR_Error object on error.
     *                An empty array if successful.
     */
    function attachFilesFromMessage(&$message)
    {
        $errors = array();

        foreach ($message->getParts() as $ref => $mime) {
            if (($ref != 1) ||
                ($mime->getPrimaryType() != MIME::type(TYPETEXT))) {
                $res = $this->addMIMEPartAttachment($mime);
                if (is_a($res, 'PEAR_Error')) {
                    $errors[] = $res;
                }
            }
        }

        return $errors;
    }

    /**
     * Convert a text/html MIME_Part message with embedded image links to
     * a multipart/related MIME_Part with the image data embedded in the part.
     *
     * @access public
     *
     * @param object MIME_Part $mime_part  The text/html MIME_Part object.
     *
     * @return object MIME_Part  The modified MIME_Part.
     */
    function convertToMultipartRelated($mime_part)
    {
        /* Return immediately if not an HTML part. */
        if ($mime_part->getType() != 'text/html') {
            return $mime_part;
        }

        /* Scan for 'img' tags - specifically the 'src' parameter. If
         * none, return the original MIME_Part. */
        if (!preg_match_all('/<img[^>]+src\s*\=\s*([^\s]+)\s+/iU', $mime_part->getContents(), $results)) {
            return $mime_part;
        }

        /* Go through list of results, download the image, and create
         * MIME_Part objects with the data. */
        $img_data = array();
        $img_parts = array();
        foreach ($results[1] as $url) {
            /* Strip any quotation marks. */
            $url = trim($url, '"');

            /* Attempt to download the image data. */
            require_once 'HTTP/Request.php';
            $request = &new HTTP_Request($url, array('timeout' => 5));
            $request->sendRequest();

            if ($request->getResponseCode() == '200') {
                /* We need to determine the image type.  Try getting
                 * that information from the returned HTTP
                 * content-type header.  TODO: Use MIME_Magic if this
                 * fails (?) */
                $part = &new MIME_Part($request->getResponseHeader('content-type'), $request->getResponseBody(), null, 'attachment', '8bit');
                $img_data[$url] = 'cid:' . $part->setContentID();
                $img_parts[] = $part;
            }
        }

        /* If we could not successfully download any data, return the
         * original MIME_Part now. */
        if (empty($img_data)) {
            return $mime_part;
        }

        /* Replace the URLs with with CID tags. */
        $text = $mime_part->getContents();
        $text = str_replace(array_keys($img_data), array_values($img_data), $text);
        $mime_part->setContents($text);

        /* Create new multipart/related part. */
        $related = &new MIME_Part('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', $mime_part->setContentID());

        /* Add the root part and the various images to the multipart
         * object. */
        $related->addPart($mime_part);
        foreach ($img_parts as $val) {
            $related->addPart($val);
        }

        return $related;
    }

    /**
     * Remove all attachments from an email message and replace with
     * urls to downloadable links. Should properly save all
     * attachments to a new folder and remove the MIME_Parts for the
     * attachments.
     *
     * @access public
     *
     * @param string           $baseurl    The base URL for creating the links.
     * @param object MIME_Part $base_part  The body of the message.
     * @param string           $auth       The authorized user who owns the attachments.
     *
     * @returns object MIME_Part  Modified part with links to attachments.
     */
    function linkAttachments($baseurl, $base_part, $auth)
    {
        global $conf;

        if (!$conf['compose']['link_attachments']) {
            return PEAR::raiseError(_("Linked attachments are forbidden."));
        }

        require_once 'VFS.php';
        $vfs = &VFS::singleton($conf['vfs']['type'], Horde::getDriverConfig('vfs', $conf['vfs']['type']));

        $ts = gmmktime();
        $fullpath = sprintf('%s/%s/%d', IMP_VFS_LINK_ATTACH_PATH, $auth, $ts);
        $vfs->autoCreatePath($fullpath);

        $trailer = "\n--\n" . String::convertCharset(_("Attachments"), NLS::getCharset(), $base_part->getCharset());
        foreach ($this->_cache as $att) {
            $trailer .= "\n" . Util::addParameter($baseurl, array('u' => $auth,
                                                                  't' => $ts,
                                                                  'f' => $att->getName()),
                                                  null, false);
            if ($conf['compose']['use_vfs']) {
                $vfs->rename(IMP_VFS_ATTACH_PATH, $att->getInformation('temp_filename'), $fullpath, escapeshellcmd($att->getName()));
            } else {
                $fd = fopen($att->getInformation('temp_filename'), 'rb');
                $data = fread($fd, filesize($att->getInformation('temp_filename')));
                fclose($fd);
                $vfs->writeData($fullpath, escapeshellcmd($att->getName()), $data);
            }
        }
        $this->deleteAllAttachments();
        $base_part->appendContents($trailer, $base_part->getCurrentEncoding());

        return $base_part;
    }

}
