<?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.125 2003/08/20 16:11:03 slusarz Exp $
 *
 * Copyright 2000-2001 Chris Hyde <chris@jeks.net>
 * Copyright 2000-2003 Chuck Hagenbuch <chuck@horde.org>
 * Copyright 2002-2003 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.125 $
 * @since   IMP 2.3
 * @package imp
 */
class IMP_Message {

    /**
     * The IMP_Mailbox object for the current mailbox.
     *
     * @var object IMP_Mailbox $_mailbox
     */
    var $_mailbox = null;

    /**
     * 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();
    }

    /**
     * Set the IMP_Mailbox object for this IMP_Message object.
     *
     * @access public
     *
     * @param optional object IMP_Mailbox &$imp_mailbox  The IMP_Mailbox
     *                                                   object for the
     *                                                   current mailbox.
     */
    function setMailboxObject(&$imp_mailbox)
    {
        $this->_mailbox = &$imp_mailbox;
    }

    /**
     * Copies or moves an array of messages to a new folder. Handles use of
     * the IMP_SEARCH_MBOX mailbox and the Trash folder.
     *
     * @param string $targetMbox       The mailbox to move/copy messages to.
     * @param integer $action          Either IMP_MESSAGE_MOVE or 
     *                                 IMP_MESSAGE_COPY.
     * @param optional array $indices  The array of indices to be copied or
     *                                 moved. If empty, will process the
     *                                 current message.
     *
     * @return boolean  True if successful, false if not.
     */
    function copy($targetMbox, $action, $indices = null)
    {
        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 => $msgIdxs) {
            $msgIdxs = implode(',', $msgIdxs);

            /* 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'], $msgIdxs, $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_null($this->_mailbox)) {
            if ($action == IMP_MESSAGE_COPY) {
                $this->_mailbox->updateMailbox(IMP_MAILBOX_COPY, $indices, $return_value);
            } else {
                $this->_mailbox->updateMailbox(IMP_MAILBOX_MOVE, $indices, $return_value);
            }
        }

        return $return_value;
    }

    /**
     * Deletes a set of messages, taking into account whether or not a
     * Trash folder is being used. This method handles the special
     * IMP_SEARCH_MBOX mailbox. If the search mailbox is not being used, will
     * delete messages from the currently open mailbox ($imp['mailbox']).
     *
     * @param mixed $indices          An array of messages to be deleted, or
     *                                a single message number.
     *                                Index format: msg_id[|msg_folder]
     * @param optional boolean $nuke  Override user preferences and nuke the
     *                                messages instead?
     *
     * @return boolean  True if successful, false if not.
     */
    function delete($indices = null, $nuke = false)
    {
        global $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 => $msgIdxs) {
            $idx = implode(',', $msgIdxs);

            /* 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)) {
                    $imp_folder->create($imp['stream'], $trash, $prefs->getValue('subscribe'));
                }

                if (!@imap_mail_move($imp['stream'], $idx, $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)) {
                        $error = false;
                        $msg_text = array();

                        /* Get text of deleted messages. */
                        foreach ($msgIdxs as $val) {
                            $msg_text[] = imap_fetchheader($imp['stream'], $val, FT_UID | FT_PREFETCHTEXT);
                        }
                        @imap_delete($imp['stream'], $idx, 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 {
                if (!@imap_delete($imp['stream'], $idx, 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;
                } elseif ($use_trash && ($folder == $trash)) {
                    /* Purge messages in the trash folder immediately. */
                    @imap_expunge($imp['stream']);
                } elseif ($this->_usepop) {
                    $this->_impImap->reopenIMAPStream(true);
                }
            }
        }

        /* Update the mailbox. */
        if (!is_null($this->_mailbox)) {
            $this->_mailbox->updateMailbox(IMP_MAILBOX_DELETE, $indices, $return_value);
        }

        return $return_value;
    }

    /**
     * Undeletes a set of messages. Handles the IMP_SEARCH_MBOX mailbox.
     * This function works with IMAP only, not POP3.
     *
     * @param mixed $indices  An array of messages to be deleted, or a single
     *                        message number.
     *
     * @return boolean  True if successful, false if not.
     */
    function undelete($indices = null)
    {
        global $imp, $notification;

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

        $return_value = true;

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

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

            if ($imp['stream'] &&
                !@imap_undelete($imp['stream'], $msgIdxs, 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_null($this->_mailbox)) {
            $this->_mailbox->updateMailbox(IMP_MAILBOX_UNDELETE, $indices, $return_value);
        }

        return $return_value;
    }

    /**
     * Strips a MIME Part out of a message. Handles the IMP_SEARCH_MBOX
     * mailbox. An IMP_Mailbox:: object must be set (via setMailboxObject()).
     *
     * @param mixed $indices  A single message index. Will return false if
     *                        more than one index is passed in.
     * @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($indices, $partid)
    {
        global $imp;

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

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

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

        /* Get a local copy of the message and strip out the desired
           MIME_Part object. */
        $contents = &new IMP_Contents($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) . '"]' . "\n");
        $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);

        /* 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(IMP::getThisMailbox());
        $status = @imap_status($imp['stream'], $folderstring, SA_UIDNEXT);
        if (@imap_append($imp['stream'], $folderstring, $headers->getHeaderText() . $message->toString(), '\\SEEN')) {
            $this->delete($index);
            $this->_mailbox->setNewIndex($status->uidnext);
            $this->_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 . '/', $this->_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 on an array 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
     *
     * @param string $flag     The IMAP flag to set or clear.
     * @param array $indices   The array of messages to be flagged.
     * @param boolean $action  True: set the flag; false: clear the flag.
     *
     * @return boolean  True if successful, false if not.
     */
    function flag($flag, $indices, $action = true)
    {
        global $imp, $notification;

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

        if ($action) {
            $function = 'imap_setflag_full';
        } else {
            $function = 'imap_clearflag_full';
        }

        $return_value = true;

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

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

            /* Flag/unflag the messages now. */
            if (!call_user_func($function, $imp['stream'], $msgIdxs, $flag, ST_UID)) {
                $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;
    }

    /**
     * Expunges all deleted messages from the currently opened mailbox.
     *
     * @access public
     */
    function expungeMailbox()
    {
        global $imp, $notification;

        if ($imp['mailbox'] == IMP_SEARCH_MBOX) {
            for ($i = 0; $i < count($imp['searchfolders']); $i++) {
                if ($this->_usepop) {
                    $stream = $imp['stream'];
                } else {
                    $stream = $this->_impImap->openIMAPStream($imp['searchfolders'][$i]);
                }

                if (!@imap_expunge($stream)) {
                    $notification->push(sprintf(_("There was a problem expunging %s. This is what the server said"), IMP::displayFolder($imp['searchfolders'][$i])) . ': ' . imap_last_error(), 'horde.error');
                }
                if (!$this->_usepop) {
                    @imap_close($stream);
                }
            }
        } else {
            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');
            }
        }

        /* Update Mailbox */
        if (!is_null($this->_mailbox)) {
            $this->_mailbox->updateMailbox(IMP_MAILBOX_EXPUNGE);
        }
    }

    /**
     * Empties the entire opened mailbox.
     *
     * @access public
     *
     * @param optional array $mbox_list  The list of mailboxes to empty. If
     *                                   no list is given, will use the 
     *                                   currently open mailbox.
     */
    function emptyMailbox($mbox_list = null)
    {
        global $imp, $notification;

        /* Use the current mailbox if no parameter given. Also, check that
           the parameter is an array. */
        if (is_null($mbox_list)) {
            $mbox_list = array($imp['thismailbox']);
        }
        if (!is_array($mbox_list)) {
            return;
        }

        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;
            } else {
                $imp['thismailbox'] = $mbox;
            }

            /* Make sure there is at least 1 message before attempting to
               delete. */
            $check = @imap_check($imp['stream']);
            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('1:*', 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');
            }
        }

        /* Update Mailbox */
        if (!is_null($this->_mailbox)) {
            $this->_mailbox->updateMailbox(IMP_MAILBOX_EMPTY);
        }
    }

    /**
     * Get message indices list.
     *
     * @access private
     *
     * @param mixed $indices  An array of messages to be deleted, or a single
     *                        message number.
     *                        Index format: msg_id[IMP_IDX_SEP msg_folder]
     *
     * @return mixed  Returns an array with the folder as key and an array
     *                of message indexes as the value.
     */
    function _getMessageIndices($indices = null)
    {
        global $imp;

        $msgList = array();

        /* Process the current message, if no index passed in. */
        if (is_null($indices)) {
            if (!is_null($this->_mailbox)) {
                $indices = $this->_mailbox->getIndex();
            }
            if (empty($indices)) {
                return $msgList; 
            }
        }      
            
        /* If a single message number is in indices, put it into an array. */
        if (!is_array($indices)) {
            $indices = array($indices);
        }             
                
        /* Build the list of indices/mailboxes to delete. */
        foreach ($indices as $msgIndex) {
            $parts = explode(IMP_IDX_SEP, $msgIndex);
            if (array_key_exists(1, $parts)) {
                $msgList[$parts[1]][] = $parts[0];
            } else {
                $msgList[$imp['thismailbox']][] = $parts[0];
            }   
        } 

        return $msgList;
    }

}
