<?php

/* Constants used in copy(). */
define('IMP_MESSAGE_MOVE', 1);
define('IMP_MESSAGE_COPY', 2);

/**
 * The IMP_Message:: class contains all functions related to handling messages
 * within IMP. Actions such as moving, copying, and deleting messages are
 * handled in here so that code need not be repeated between mailbox, message,
 * and other pages.
 *
 * $Horde: imp/lib/Message.php,v 1.164.8.1 2005/01/03 12:25:33 jan Exp $
 *
 * Copyright 2000-2001 Chris Hyde <chris@jeks.net>
 * Copyright 2000-2005 Chuck Hagenbuch <chuck@horde.org>
 * 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  Chris Hyde <chris@jeks.net>
 * @author  Chuck Hagenbuch <chuck@horde.org>
 * @author  Michael Slusarz <slusarz@bigworm.colorado.edu>
 * @version $Revision: 1.164.8.1 $
 * @since   IMP 2.3
 * @package IMP
 */
class IMP_Message {

    /**
     * The IMP_IMAP object for the current server.
     *
     * @var object IMP_IMAP $_impImap
     */
    var $_impImap;

    /**
     * Using POP to access mailboxes?
     *
     * @var boolean $_usepop
     */
    var $_usepop = false;

    /**
     * Constructor.
     *
     * @access public
     */
    function IMP_Message()
    {
        global $imp;

        if ($imp['base_protocol'] == 'pop3') {
            $this->_usepop = true;
        }

        require_once IMP_BASE . '/lib/IMAP.php';
        $this->_impImap = &IMP_IMAP::singleton();
    }

    /**
     * Indices format:
     * ===============
     * For any function below that requires an $indices parameter, the
     * following inputs are allowed:
     * 1. An array of messages indices in the following format:
     *    msg_id IMP_IDX_SEP msg_folder
     *    msg_id = Message index of the message
     *    IMP_IDX_SEP = IMP constant used to separate index/folder
     *    msg_folder = The full folder name containing the message index
     * 2. An array with the full folder name as keys and an array of messsage
     *    indices as the values.
     * 3. An IMP_Mailbox object, which will use the current index/folder
     *    as determined by the object. If an IMP_Mailbox object is used, it
     *    will be updated after the action is performed.
     */

    /**
     * Copies or moves a list of messages to a new folder.
     * Handles use of the IMP_SEARCH_MBOX mailbox and the Trash folder.
     *
     * @access public
     *
     * @param string $targetMbox  The mailbox to move/copy messages to.
     * @param integer $action     Either IMP_MESSAGE_MOVE or IMP_MESSAGE_COPY.
     * @param mixed &$indices     See above.
     *
     * @return boolean  True if successful, false if not.
     */
    function copy($targetMbox, $action, &$indices)
    {
        global $imp, $notification, $prefs;

        if (!($msgList = $this->_getMessageIndices($indices))) {
            return false;
        }

        switch ($action) {
        case IMP_MESSAGE_MOVE:
            $imap_flags = CP_UID | CP_MOVE;
            $message = _("There was an error moving messages from \"%s\" to \"%s\". This is what the server said");
            break;

        case IMP_MESSAGE_COPY:
            $imap_flags = CP_UID;
            $message = _("There was an error copying messages from \"%s\" to \"%s\". This is what the server said");
            break;
        }

        $return_value = true;

        foreach ($msgList as $folder => $msgIndices) {
            $msgIdxString = implode(',', $msgIndices);

            /* Switch folders, if necessary (only valid for IMAP). */
            $this->_impImap->changeMbox($folder);

            /* Attempt to copy/move messages to new mailbox. */
            if (!@imap_mail_copy($imp['stream'], $msgIdxString, $targetMbox, $imap_flags)) {
                $notification->push(sprintf($message, IMP::displayFolder($folder), IMP::displayFolder($targetMbox)) . ': ' . imap_last_error(), 'horde.error');
                $return_value = false;
            }

            /* If moving, and using the trash, expunge immediately. */
            if ($prefs->getValue('use_trash') &&
                ($action == IMP_MESSAGE_MOVE)) {
                @imap_expunge($imp['stream']);
            }
        }

        /* Update the mailbox. */
        if (is_a($indices, 'IMP_Mailbox')) {
            if ($action == IMP_MESSAGE_COPY) {
                $indices->updateMailbox(IMP_MAILBOX_COPY, $return_value);
            } else {
                $indices->updateMailbox(IMP_MAILBOX_MOVE, $return_value);
            }
        }

        return $return_value;
    }

    /**
     * Deletes a list of messages taking into account whether or not a
     * Trash folder is being used.
     * Handles use of the IMP_SEARCH_MBOX mailbox and the Trash folder.
     *
     * @access public
     *
     * @param mixed &$indices   See above.
     * @param boolean $nuke     Override user preferences and nuke (i.e.
     *                          permanently delete) the messages instead?
     * @param boolean $keeplog  Should any history information of the
     *                          message be kept?
     *
     * @return boolean  True if successful, false if not.
     */
    function delete(&$indices, $nuke = false, $keeplog = false)
    {
        global $conf, $imp, $notification, $prefs;

        if (!($msgList = $this->_getMessageIndices($indices))) {
            return false;
        }

        $return_value = true;
        $trash = IMP::addPreambleString($prefs->getValue('trash_folder'));
        $use_trash = (!$nuke && $prefs->getValue('use_trash'));

        /* If the folder we are deleting from has changed. */
        foreach ($msgList as $folder => $msgIndices) {
            $sequence = implode(',', $msgIndices);

            /* Switch folders, if necessary (only valid for IMAP). */
            $this->_impImap->changeMbox($folder);

            /* Trash is only valid for IMAP mailboxes. */
            if (!$this->_usepop && $use_trash && ($folder != $trash)) {
                include_once IMP_BASE . '/lib/Folder.php';
                $imp_folder = &IMP_Folder::singleton();

                if (!$imp_folder->exists($imp['stream'], $trash)) {
                    if (!$imp_folder->create($imp['stream'], $trash, $prefs->getValue('subscribe'))) {
                        return false;
                    }
                }

                if (!@imap_mail_move($imp['stream'], $sequence, $trash, CP_UID)) {
                    $error_msg = imap_last_error();
                    $error = true;

                    /* Handle the case when the mailbox is overquota
                       (moving message to trash would fail) by first
                       deleting then copying message to Trash. */
                    if (stristr('over quota', $error_msg) !== false) {
                        $error = false;
                        $msg_text = array();

                        /* Get text of deleted messages. */
                        foreach ($msgIndices as $val) {
                            $msg_text[] = imap_fetchheader($imp['stream'], $val, FT_UID | FT_PREFETCHTEXT);
                        }
                        @imap_delete($imp['stream'], $sequence, FT_UID);
                        @imap_expunge($imp['stream']);

                        /* Save messages in Trash folder. */
                        foreach ($msg_text as $val) {
                            if (!@imap_append($imp['stream'], $trash, $val)) {
                                $error = true;
                                break;
                            }
                        }
                    }

                    if ($error) {
                        $notification->push(sprintf(_("There was an error deleting messages from the folder \"%s\". This is what the server said"), IMP::displayFolder($folder)) . ': ' . $error_msg, 'horde.error');
                        $return_value = false;
                    }
                } else {
                    @imap_expunge($imp['stream']);
                }
            } else {
                /* Get the list of Message-IDs for the deleted messages. */
                $overview = @imap_fetch_overview($imp['stream'], $sequence, FT_UID);

                /* Delete the messages. */
                if (!@imap_delete($imp['stream'], $sequence, FT_UID)) {
                    if ($this->_usepop) {
                        $notification->push(sprintf(_("There was an error deleting messages. This is what the server said: %s"), imap_last_error()), 'horde.error');
                    } else {
                        $notification->push(sprintf(_("There was an error deleting messages from the folder \"%s\". This is what the server said"), IMP::displayFolder($folder)) . ': ' . imap_last_error(), 'horde.error');
                    }
                    $return_value = false;
                } else {
                    if ($nuke || ($use_trash && ($folder == $trash))) {
                        /* Purge messages in the trash folder immediately. */
                        @imap_expunge($imp['stream']);
                    } elseif ($this->_usepop) {
                        $this->_impImap->reopenIMAPStream(true);
                    }

                    /* Get the list of Message-IDs deleted, and remove
                     * the information from the mail log. */
                    if (!$keeplog && !empty($conf['maillog']['use_maillog'])) {
                        $msg_ids = array();
                        foreach ($overview as $val) {
                            if (!empty($val->message_id)) {
                                $msg_ids[] = $val->message_id;
                            }
                        }
                        require_once IMP_BASE . '/lib/Maillog.php';
                        IMP_Maillog::deleteLog($msg_ids);
                    }
                }
            }
        }

        /* Update the mailbox. */
        if (is_a($indices, 'IMP_Mailbox')) {
            $indices->updateMailbox(IMP_MAILBOX_DELETE, $return_value);
        }

        return $return_value;
    }

    /**
     * Undeletes a list of messages.
     * Handles the IMP_SEARCH_MBOX mailbox.
     * This function works with IMAP only, not POP3.
     *
     * @access public
     *
     * @param mixed &$indices  See above.
     *
     * @return boolean  True if successful, false if not.
     */
    function undelete(&$indices)
    {
        global $imp, $notification;

        if (!($msgList = $this->_getMessageIndices($indices))) {
            return false;
        }

        $return_value = true;

        foreach ($msgList as $folder => $msgIndices) {
            $msgIdxString = implode(',', $msgIndices);

            /* Switch folders, if necessary. */
            $this->_impImap->changeMbox($folder);

            if ($imp['stream'] &&
                !@imap_undelete($imp['stream'], $msgIdxString, FT_UID)) {
                $notification->push(sprintf(_("There was an error deleting messages in the folder \"%s\". This is what the server said"), IMP::displayFolder($folder)) . ': ' . imap_last_error(), 'horde.error');
                $return_value = false;
            }
        }

        /* Update the mailbox. */
        if (is_a($indices, 'IMP_Mailbox')) {
            $indices->updateMailbox(IMP_MAILBOX_UNDELETE, $return_value);
        }

        return $return_value;
    }

    /**
     * Copies or moves a list of messages to a tasklist.
     * Handles use of the IMP_SEARCH_MBOX mailbox and the Trash folder.
     *
     * @access public
     *
     * @param string    $tasklist   The tasklist in which the task will be
     *                              created.
     * @param integer   $action     Either IMP_MESSAGE_MOVE or IMP_MESSAGE_COPY.
     * @param mixed     &$indices   See above.
     *
     * @return boolean  True if successful, false if not.
     */
    function createTasks($tasklist, $action, &$indices)
    {
        global $registry, $notification, $prefs;

        if (!($msgList = $this->_getMessageIndices($indices))) {
            return false;
        }

        require_once IMP_BASE . '/lib/Compose.php';
        require_once IMP_BASE . '/lib/MIME/Contents.php';
        require_once IMP_BASE . '/lib/MIME/Headers.php';
        require_once 'Text/Flowed.php';
        require_once 'Horde/iCalendar.php';

        foreach ($msgList as $folder => $msgIndices) {
            foreach ($msgIndices as $index) {
                /* Fetch the message headers. */
                $imp_headers = &new IMP_Headers($index);
                $imp_headers->buildHeaders();
                $subject = $imp_headers->getValue('subject');

                /* Fetch the message contents. */
                $imp_contents = &IMP_Contents::singleton($index);
                $imp_contents->buildMessage();

                /* Extract the message body. */
                $imp_compose = &new IMP_Compose();
                $mime_message = $imp_contents->getMIMEMessage();
                $body_id = $imp_compose->getBodyId($imp_contents);
                $body = $imp_compose->findBody($imp_contents);

                /* Re-flow the message for prettier formatting. */
                $flowed = &new Text_Flowed($mime_message->replaceEOL($body, "\n"));
                $body = $flowed->toFlowed(false);

                /* Convert to current charset */
                /* TODO: When Horde_iCalendar supports setting of charsets
                 * we need to set it there instead of relying on the fact
                 * that both Nag and IMP use the same charset. */
                $body_part = $mime_message->getPart($body_id);
                $body = String::convertCharset($body, $body_part->getCharset(), NLS::getCharset());

                /* Create a new iCalendar. */
                $vCal = &new Horde_iCalendar();
                $vCal->setAttribute('PRODID', '-//The Horde Project//IMP ' . IMP_VERSION . '//EN');
                $vCal->setAttribute('METHOD', 'PUBLISH');

                /* Create a new vTodo object using this message's contents. */
                $vTodo = &Horde_iCalendar::newComponent('vtodo', $vCal);
                $vTodo->setAttribute('SUMMARY', $subject);
                $vTodo->setAttribute('DESCRIPTION', $body);
                $vTodo->setAttribute('PRIORITY', '3');

                /* Get the list of editable tasklists. */
                $tasklists = $registry->call('tasks/listTasklists',
                                             array(false, PERMS_EDIT));

                /* Attempt to add the new vTodo item to the requested tasklist. */
                $res = $registry->call('tasks/import',
                                       array($vTodo, 'text/x-vtodo', $tasklist));
                if (is_a($res, 'PEAR_Error')) {
                    $notification->push($res, $res->getCode());
                } elseif (!$res) {
                    $notification->push(_("An unknown error occured while creating the new task."), 'horde.error');
                } else {
                    $task_name = '"' . htmlspecialchars($subject) . '"';

                    /* Attempt to convert the task name into a hyperlink. */
                    $task_link = $registry->link('tasks/show',
                                                 array('uid' => $res));
                    if ($task_link && !is_a($task_link, 'PEAR_Error')) {
                        $task_name = sprintf('<a href="%s">%s</a>',
                                             Horde::url($task_link),
                                             $task_name);
                    }

                    $notification->push(sprintf(_("%s was successfully added to %s."), $task_name, htmlspecialchars($tasklists[$tasklist]->get('name'))), 'horde.success', array('content.raw'));
                }
            }
        }

        /* Delete the original messages if this is a "move" operation. */
        if ($action == IMP_MESSAGE_MOVE) {
            $this->delete($indices);
        }

        return true;
    }

    /**
     * Strips a MIME Part out of a message.
     * Handles the IMP_SEARCH_MBOX mailbox.
     *
     * @param object IMP_Mailbox &$imp_mailbox  The IMP_Mailbox object with
     *                                          the current index set to the
     *                                          message to be processed.
     * @param string $partid                    The MIME ID of the part to
     *                                          strip.
     *
     * @return boolean  Returns true if successful.
     *                  Returns a PEAR_Error on error.
     */
    function stripPart(&$imp_mailbox, $partid)
    {
        global $imp;

        /* Return error if no index was provided. */
        if (!($msgList = $this->_getMessageIndices($imp_mailbox))) {
            return PEAR::raiseError('No index provided to IMP_Message::stripPart().');
        }

        /* If more than one index provided, return error. */
        reset($msgList);
        list($folder, $index) = each($msgList);
        if (each($msgList) || (count($index) > 1)) {
            return PEAR::raiseError('More than 1 index provided to IMP_Message::stripPart().');
        }
        $index = implode('', $index);

        require_once 'Horde/MIME/Part.php';
        require_once IMP_BASE . '/lib/MIME/Contents.php';
        require_once IMP_BASE . '/lib/MIME/Headers.php';

        /* Get a local copy of the message and strip out the desired
           MIME_Part object. */
        $contents = &IMP_Contents::singleton($index);
        $contents->rebuildMessage();
        $message = $contents->getMIMEMessage();
        $oldPart = $message->getPart($partid);
        $newPart = new MIME_Part('text/plain');
        $newPart->setContents('[' . _("Attachment stripped: Original attachment type") . ': "' . $oldPart->getType() . '", ' . _("name") . ': "' . $oldPart->getName(false, true) . '"]');
        $message->alterPart($partid, $newPart);

        /* We need to make sure we add "\r\n" after every line for
           imap_append() - some servers require it (e.g. Cyrus). */
        $message->setEOL(MIME_PART_RFC_EOL);

        /* Get the headers for the message. */
        $headers = &new IMP_Headers($index);
        $headertext = $headers->getHeaderText();
        $headers->buildFlags();
        $flags = array();
        foreach (array('answered', 'draft', 'flagged', 'deleted') as $flag) {
            if ($headers->getFlag($flag)) {
                $flags[] = '\\' . String::ucfirst($flag);
            }
        }
        $flags = implode(' ', $flags);

        /* This is a (sort-of) hack. Right before we append the new message
           we check imap_status() to determine the next available UID. We
           use this UID as the new index of the message. */
        $folderstring = IMP::serverString($folder);
        $this->_impImap->changeMbox($folderstring);
        $status = @imap_status($imp['stream'], $folderstring, SA_UIDNEXT);
        if (@imap_append($imp['stream'], $folderstring, $headertext . $message->toString(), '\\SEEN')) {
            $idx_array = array($folder => $index);
            $this->delete($idx_array, true, true);
            $idx_array = array($folder => $status->uidnext);
            $this->flag($flags, $idx_array);
            $imp_mailbox->setNewIndex($status->uidnext);
            $imp_mailbox->updateMailbox(IMP_MAILBOX_UPDATE);

            /* We need to replace the old index in the query string with the
               new index. */
            $_SERVER['QUERY_STRING'] = preg_replace('/' . $index . '/', $imp_mailbox->getIndex(), $_SERVER['QUERY_STRING']);

            return true;
        } else {
            return PEAR::raiseError(_("An error occured while attempting to strip the attachment. The IMAP server said: ") . imap_last_error());
        }
    }

    /**
     * Sets or clears a given flag for a list of messages.
     * Handles use of the IMP_SEARCH_MBOX mailbox.
     * This function works with IMAP only, not POP3.
     *
     * Valid flags are:
     *   \\SEEN
     *   \\FLAGGED
     *   \\ANSWERED
     *   \\DELETED
     *   \\DRAFT
     *
     * @access public
     *
     * @param string $flag              The IMAP flag(s) to set or clear.
     * @param mixed &$indices           See above.
     * @param optional boolean $action  True: set the flag(s) [DEFAULT];
     *                                  false: clear the flag(s).
     *
     * @return boolean  True if successful, false if not.
     */
    function flag($flag, &$indices, $action = true)
    {
        if (!($msgList = $this->_getMessageIndices($indices))) {
            return false;
        }

        $function = ($action) ? 'imap_setflag_full' : 'imap_clearflag_full';
        $return_value = true;

        foreach ($msgList as $folder => $msgIndices) {
            $msgIdxString = implode(',', $msgIndices);

            /* Switch folders, if necessary. */
            $this->_impImap->changeMbox($folder);

            /* Flag/unflag the messages now. */
            if (!call_user_func($function, $GLOBALS['imp']['stream'], $msgIdxString, $flag, ST_UID)) {
                $GLOBALS['notification']->push(sprintf(_("There was an error flagging messages in the folder \"%s\". This is what the server said"), IMP::displayFolder($folder)) . ': ' . imap_last_error(), 'horde.error');
                $return_value = false;
            }
        }

        return $return_value;
    }

    /**
     * Sets or clears a given flag(s) for all messages in a list of mailboxes.
     * This function works with IMAP only, not POP3.
     *
     * Valid flags are:
     *   \\SEEN
     *   \\FLAGGED
     *   \\ANSWERED
     *   \\DELETED
     *   \\DRAFT
     *
     * @access public
     *
     * @param string $flag              The IMAP flag(s) to set or clear.
     * @param array $mboxes             The list of mailboxes to flag.
     * @param optional boolean $action  True: set the flag(s) [DEFAULT];
     *                                  false: clear the flag(s).
     *
     * @return boolean  True if successful, false if not.
     */
    function flagAllInMailbox($flag, $mboxes, $action = true)
    {
        if (empty($mboxes) || !is_array($mboxes)) {
            return false;
        }

        $function = ($action) ? 'imap_setflag_full' : 'imap_clearflag_full';
        $return_value = true;

        foreach ($mboxes as $val) {
            /* Switch folders, if necessary. */
            $this->_impImap->changeMbox($val);

            if (!call_user_func($function, $GLOBALS['imp']['stream'], '1:*', $flag)) {
                $notification->push(sprintf(_("There was an error flagging messages in the folder \"%s\". This is what the server said"), IMP::displayFolder($val)) . ': ' . imap_last_error(), 'horde.error');
                $return_value = false;
            }
        }

        return $return_value;
    }

    /**
     * Expunges all deleted messages from the list of mailboxes.
     *
     * @access public
     *
     * @param array $mbox_list  The list of mailboxes to empty.
     */
    function expungeMailbox($mbox_list)
    {
        global $imp, $notification;

        foreach ($mbox_list as $val) {
            if ($val == IMP_SEARCH_MBOX) {
                for ($i = 0; $i < count($imp['search']['folders']); $i++) {
                    if ($this->_usepop) {
                        $stream = $imp['stream'];
                    } else {
                        $stream = $this->_impImap->openIMAPStream($imp['search']['folders'][$i]);
                    }

                    if (!@imap_expunge($stream)) {
                        $notification->push(sprintf(_("There was a problem expunging %s. This is what the server said"), IMP::displayFolder($imp['search']['folders'][$i])) . ': ' . imap_last_error(), 'horde.error');
                    }
                    if (!$this->_usepop) {
                        @imap_close($stream);
                    }
                }
            } else {
                $this->_impImap->changeMbox($val);
                if (!@imap_expunge($imp['stream'])) {
                    $notification->push(_("There was a problem expunging the mailbox. This is what the server said") . ': ' . imap_last_error(), 'horde.error');
                }
            }
        }
    }

    /**
     * Empties an entire mailbox.
     *
     * @access public
     *
     * @param array $mbox_list  The list of mailboxes to empty.
     */
    function emptyMailbox($mbox_list)
    {
        global $imp, $notification;

        foreach ($mbox_list as $mbox) {
            $display_mbox = IMP::displayFolder($mbox);

            if (($this->_impImap->changeMbox($mbox)) !== true) {
                $notification->push(sprintf(_("Could not delete messages from %s. The server said: %s"), $display_mbox, imap_last_error()), 'horde.error');
                continue;
            }

            /* Make sure there is at least 1 message before attempting to
               delete. */
            $check = @imap_check($imp['stream']);
            $delete_array = array($mbox => '1:*');
            if (!is_object($check)) {
                $notification->push(sprintf(_("%s does not appear to be a valid mailbox."), $display_mbox), 'horde.error');
            } elseif (empty($check->Nmsgs)) {
                $notification->push(sprintf(_("The mailbox %s is already empty."), $display_mbox), 'horde.message');
            } elseif (!$this->delete($delete_array, true)) {
                $notification->push(sprintf(_("There was a problem expunging the mailbox. The server said: %s"), imap_last_error()), 'horde.error');
                continue;
            } else {
                @imap_expunge($imp['stream']);
                $notification->push(sprintf(_("Emptied all messages from %s."), $display_mbox), 'horde.success');
            }
        }
    }

    /**
     * Get message indices list.
     *
     * @access private
     *
     * @param mixed $indices  See above.
     *
     * @return mixed  Returns an array with the folder as key and an array
     *                of message indices as the value (See #2 above).
     *                Else, returns false.
     */
    function _getMessageIndices($indices)
    {
        global $imp;

        $msgList = array();

        if (is_a($indices, 'IMP_Mailbox')) {
            $msgIdx = $indices->getIndex(true);
            if (empty($msgIdx)) {
                return false;
            }
            $parts = explode(IMP_IDX_SEP, $msgIdx);
            $msgList[$parts[1]][] = $parts[0];
            return $msgList;
        }

        if (!is_array($indices)) {
            return false;
        }
        if (!count($indices)) {
            return array();
        }

        reset($indices);
        if (is_array(current($indices))) {
            /* Build the list of indices/mailboxes to delete if input
               is of format #1. */
            foreach ($indices as $mbox => $msgs) {
                foreach ($msgs as $msgIndex) {
                    if (strpos($msgIndex, IMP_IDX_SEP) === false) {
                        $msgList[$mbox][] = $msgIndex;
                    } else {
                        list($val, $key) = explode(IMP_IDX_SEP, $msgIndex);
                        $msgList[$key][] = $val;
                    }
                }
            }
        } else {
            /* We are dealing with format #2. Make sure we don't have
               any duplicate keys. */
            foreach ($indices as $key => $val) {
                if (isset($msgList[$key])) {
                    $msgList[$key] = array_unique(array_merge($val, $msgList[$key]));
                } else {
                    $msgList[$key] = is_array($val) ? $val : array($val);
                }
            }
        }
        return $msgList;
    }

}
