<?php
/**
 * The IMP_Folder:: class provides a set of methods for dealing with
 * folders, accounting for subscription, errors, etc.
 *
 * $Horde: imp/lib/Folder.php,v 1.97 2003/06/12 22:37:06 slusarz Exp $
 *
 * Copyright 2000-2003 Chuck Hagenbuch <chuck@horde.org>
 * Copyright 2000-2003 Jon Parise <jon@csh.rit.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  Chuck Hagenbuch <chuck@horde.org>
 * @author  Jon Parise <jon@csh.rit.edu>
 * @version $Revision: 1.97 $
 * @since   IMP 2.3
 * @package imp
 */
class IMP_Folder {

    /**
     * Keep around identical lists so that we don't hit the server more
     * that once in the same page for the same thing.
     *
     * @var array $_listCache
     */
    var $_listCache = array();

    /**
     * Keep track of mailbox names that we have complained about to prevent
     * giving the user identical error messages.
     *
     * @var array $_errorCache
     */
    var $_errorCache = array();

    /**
     * Cached results from the exists() function.
     *
     * @var array $existsResults
     */
    var $_existsResults = array();

    /**
     * The folder delimiter to use.
     *
     * @var string $_delimiter
     */
    var $_delimiter = null;

    /**
     * Returns a reference to the global IMP_Folder object, only
     * creating it if it doesn't already exist. This ensures that only
     * one IMP_Folder instance is instantiated for any given session
     *
     * This method must be invoked as:
     *   $imp_folder = &IMP_Folder::singleton();
     *
     * @access public
     *
     * @return object IMP_Folder  The IMP_Folder instance.
     */
    function &singleton()
    {
        static $folder;

        if (!isset($folder)) {
            $folder = new IMP_Folder();
        }

        return $folder;
    }

    /**
     * Constructor.
     *
     * @access public
     */
    function IMP_Folder()
    {
    }

    /**
     * List folders.
     *
     * @access public
     *
     * @param resource $stream             An open connection to a mail server.
     * @param string $server               The specification of the server to
     *                                     connect to.
     *                                     Ex: {mail.foo.com/pop3:110}.
     * @param string $prefix               What to restrict the folder list
     *                                     to. For instance, if the IMAP
     *                                     server makes your entire home
     *                                     directory available, but you only
     *                                     want the folders in ~/mail/, then
     *                                     this should be * 'mail/'.
     * @param optional boolean $sub        Should we list only subscribed
     *                                     folders?
     * @param optional array $filter       An list of mailboxes that should be
     *                                     left out of the list.
     * @param optional array $hierarchies  An list of additional hierarchies
     *                                     that are either outside prefix or
     *                                     not advertised by default (#shared/
     *                                     and similar with UW) to include in
     *                                     the list.
     * @param optional boolean $dotfiles   If false, do not include folders
     *                                     beginning with '.'
     * @param optional string $namespace   Namespace prefix prepended on folder
     *                                     names (eg. "INBOX." in cyrus). We
     *                                     remove this for cosmetic purposes.
     *
     * @return array  An array of folders, where each array alement is an
     *                associative array containing three values: 'val', with
     *                entire folder name after the server specification;
     *                'label', with the full-length folder name meant for
     *                display ($prefix has been stripped off, etc); and
     *                'abbrev', containing a shortened (26 characters max)
     *                label for display in situations where space is short.
     */
    function flist($stream, $server, $prefix, $sub = false, $filter = array(),
                   $hierarchies = array(), $dotfiles = true, $namespace = '')
    {
        global $conf, $imp, $notification;

        $list = array();

        /* Make sure we're requesting all folders. */
        $folders = $prefix . '*';

        /* Figure out which list command to use. */
        $listcmd = ($sub) ? 'imap_lsub' : 'imap_list';

        /* Compute values that will uniquely identify this list. */
        $signature = implode('|', array($listcmd, $folders));
        $full_signature = serialize(array($listcmd, $prefix, $filter, $hierarchies, $dotfiles, $namespace));

        /* Either get the list from the cache, or go to the IMAP
           server to obtain it. */
        if ($conf['server']['cache_folders']) {
            require_once HORDE_BASE . '/lib/SessionObjects.php';
            $sessionOb = &Horde_SessionObjects::singleton();
            if (!isset($imp['cache']['folder_cache'])) {
                $imp['cache']['folder_cache'] = array();
            }
            $folder_cache = &$imp['cache']['folder_cache'];
            if (isset($folder_cache[$full_signature])) {
                $data = $sessionOb->query($folder_cache[$full_signature]);
                if ($data) {
                    return $data;
                }
            }
        }

        if (array_key_exists($signature, $this->_listCache)) {
            $mailboxes = $this->_listCache[$signature];
        } else {
            if (!($mailboxes = $listcmd($stream, $server, $folders))) {
                $mailboxes = array();
            }

            if (is_array($hierarchies)) {
                foreach ($hierarchies as $hierarchy) {
                    $hboxes = $listcmd($stream, $server, $hierarchy . '*');
                    if (is_array($hboxes)) {
                        $mailboxes = array_merge($mailboxes, $hboxes);
                    }
                }
            }

            $this->_listCache[$signature] = $mailboxes;
        }

        if (is_array($mailboxes) && (count($mailboxes) > 0)) {
            $delimiter = $this->getDelimiter($stream);
            $parents = array();
            $path = $server . $prefix;
            $seen = array();

            natcasesort($mailboxes);

            foreach ($mailboxes as $mailbox) {
                if (empty($seen[$mailbox])) {
                    $seen[$mailbox] = true;
                    if (!($decoded_mailbox = String::convertCharset($mailbox, 'UTF7-IMAP'))) {
                        if (empty($this->_errorCache[$mailbox])) {
                            $user_box = str_replace($server, '', $mailbox);
                            if (substr($user_box, 0, strlen($prefix)) == $prefix) {
                                $user_box = substr($user_box, strlen($prefix));
                            }
                            if (substr($user_box, 0, strlen($namespace)) == $namespace) {
                                $user_box = substr($user_box, strlen($namespace));
                            }
                            $notification->push(sprintf(_("The folder \"%s\" contains illegal characters in its name. It may cause problems. Please see your system administrator."), $mailbox), 'horde.warning');
                            $this->_errorCache[$mailbox] = true;
                        }
                    }

                    /* Ignore INBOX, since we always tack it on first. */
                    if (strcmp($path . 'INBOX', $mailbox) != 0) {
                        $value = str_replace($server, '', $mailbox);
                        if (substr($value, 0, strlen($prefix)) == $prefix) {
                            $label = substr($value, strlen($prefix));
                        } else {
                            $label = $value;
                        }
                        if (substr($label, 0, strlen($namespace)) == $namespace) {
                            $label = substr($label, strlen($namespace));
                        }
                        $parts = explode($delimiter, $label);
                        $subfolder = array_pop($parts);

                        $parentparts = $parts;
                        $pstack = array();
                        while (count($parentparts)) {
                            $parent = implode($delimiter, $parentparts);
                            if (!empty($parent) && ($parent != 'INBOX') &&
                                !array_key_exists($parent, $list) &&
                                !array_key_exists($prefix . $namespace . $parent, $list) &&
                                !array_key_exists($parent, $parents)) {
                                $parents[$parent] = 1;
                                $plabel = str_repeat(' ', 4 * (count($parentparts) - 1)) . String::convertCharset($parts[count($parentparts) - 1], 'UTF7-IMAP') . $delimiter;
                                if (strlen($plabel) > 26) {
                                    $pabbrev = String::substr($plabel, 0, 10) . '...' . String::substr($plabel, -13, 13);
                                } else {
                                    $pabbrev = $plabel;
                                }
                                $pstack[$parent] = array('val' => '', 'label' => $plabel, 'abbrev' => $pabbrev);
                            }
                            array_pop($parentparts);
                        }
                        if (count($pstack)) {
                            $pstack = array_reverse($pstack);
                            foreach ($pstack as $pkey => $pval) {
                                $list[$pkey] = $pval;
                            }
                        }

                        $folded = str_repeat(' ', 4 * count($parts)) . String::convertCharset($subfolder, 'UTF7-IMAP');

                        if (strlen($folded) > 26) {
                            $abbrev = String::substr($folded, 0, 10) . '...' . String::substr($folded, -13, 13);
                        } else {
                            $abbrev = $folded;
                        }

                        if (($dotfiles || (strlen($label) > 0 && ($label[0] != '.' && !strstr($label, $delimiter . '.'))))
                            && !in_array($value, $filter)) {
                            $list[$value] = array('val' => $value, 'label' => $folded, 'abbrev' => $abbrev);
                        }
                    }
                }
            }
        }

        if (!in_array('INBOX', $filter)) {
            array_unshift($list, array('val' => 'INBOX', 'label' => 'INBOX', 'abbrev' => 'INBOX'));
        }

        /* Save in cache, if needed. */
        if ($conf['server']['cache_folders']) {
            $folder_cache[$full_signature] = $sessionOb->storeOid($list, false);
        }
            
        return $list;
    }

    /**
     * Return an array of folders. This is a wrapper around the
     * flist() function which reduces the number of arguments needed if
     * we can assume that IMP's full environment is present.
     *
     * @access public
     *
     * @param optional array $filter  An array of mailboxes to ignore.
     *
     * @return array  The array of mailboxes returned by flist().
     */
    function flist_IMP($filter = array())
    {
        global $imp, $prefs;

        return $this->flist($imp['stream'], IMP::serverString(), $imp['folders'], $prefs->getValue('subscribe'), $filter, $imp['hierarchies'], $imp['dotfiles'], $imp['namespace']);
    }

    /**
     * List all folders which the user is not subscribed to.
     *
     * @access public
     *
     * @param resource $stream             An open connection to a mail server.
     * @param string $server               The specification of the server to
     *                                     connect to.
     *                                     Ex: {mail.foo.com/pop3:110}.
     * @param string $prefix               What to restrict the folder list to.
     *                                     For instance, if the IMAP server
     *                                     makes your entire home directory
     *                                     available, but you only want the
     *                                     folders in ~/mail/, then this should
     *                                     be 'mail/'.
     * @param optional array $filter       A list of mailboxes that should be
     *                                     left out of the list.
     * @param optional array $hierarchies  An list of additional hierarchies
     *                                     that are either outside prefix or
     *                                     not advertised by default (#shared/
     *                                     and similar with UW) to include in
     *                                     the list.
     * @param optional boolean $dotfiles   If false, do not include folders
     *                                     beginning with '.'
     * @param optional string $namespace   Namespace prefix prepended on folder
     *                                     names (eg. "INBOX." in cyrus). We
     *                                     remove this for cosmetic purposes.
     *
     * @return array  An array of folders. (Format: see flist())
     */
    function flistUnsubscribed($stream, $server, $prefix, $filter = false,
                               $hierarchies = array(), $dotfiles = true,
                               $namespace = '')
    {
        $sub = $this->flist($stream, $server, $prefix, true, $filter, $hierarchies, $dotfiles, $namespace);
        $all = $this->flist($stream, $server, $prefix, false, $filter, $hierarchies, $dotfiles, $namespace);

        $usub = array();
        foreach (array_diff(array_keys($all), array_keys($sub)) as $key) {
            $usub[$key] = $all[$key];
        }

        return $usub;
    }

    /**
     * Return an array of unsubscribed folders. This is a wrapper around
     * the flistUnsubscribed() function which reduces the number of
     * arguments needed if we can assume that IMP's full environment is
     * present.
     *
     * @access public
     *
     * @param optional array $filter  An array of mailboxes to ignore.
     *
     * @return array  The array of mailboxes returned by
     *                flistUnsubscribed().
     */
    function flistUnsubscribed_IMP($filter = array())
    {
        global $imp;

        return $this->flistUnsubscribed($imp['stream'], IMP::serverString(), $imp['folders'], $filter, $imp['hierarchies'], $imp['dotfiles'], $imp['namespace']);
    }

    /**
     * Delete one or more folders.
     *
     * @access public
     *
     * @param resource $stream     A valid c-client stream.
     * @param array $folder_array  An array of full utf encoded folder names
     *                             to be deleted.
     * @param boolean $subscribe   A boolean describing whether or not to use
     *                             folder subscriptions.
     *
     * @return boolean  Whether or not the folders were successfully deleted.
     */
    function delete($stream, $folder_array, $subscribe)
    {
        global $conf, $imp, $notification, $prefs;

        $server = IMP::serverString();
        $return_value = true;
        $update_cache = false;

        if ($subscribe) {
            $subscribed_folders = imap_listsubscribed($stream, $server, '*');
        }

        $pollfolders = @unserialize($prefs->getValue('nav_poll'));
        foreach ($folder_array as $folder) {
            if (!imap_deletemailbox($stream, $server . $folder)) {
                $notification->push(sprintf(_("The folder '%s' was not deleted. This is what the server said"), IMP::displayFolder($folder)) .
                                    ': ' . imap_last_error(), 'horde.error');
                $return_value = false;
            } else {
                if ($subscribe &&
                    in_array($server . $folder, $subscribed_folders) &&
                    !imap_unsubscribe($stream, $server . $folder)) {
                    $notification->push(sprintf(_("The folder \"%s\" was deleted but you were not unsubscribed from it."), IMP::displayFolder($folder)), 'horde.warning');
                    $return_value = false;
                } else {
                    $notification->push(sprintf(_("The folder \"%s\" was successfully deleted."), IMP::displayFolder($folder)), 'horde.success');
                }
                if (isset($pollfolders) && is_array($pollfolders) &&
                    in_array($folder, $pollfolders)) {
                    unset($pollfolders[$folder]);
                }
                $update_cache = true;
            }
        }
        if (isset($pollfolders) && is_array($pollfolders)) {
            $prefs->setValue('nav_poll', serialize($pollfolders));
        }

        /* Reset the folder cache. */
        if ($update_cache && $conf['server']['cache_folders']) {
            unset($imp['cache']['folder_cache']);
        }

        return $return_value;
    }

    /**
     * Create a new IMAP folder if it does not already exist, and
     * subcribe to it as well if requested.
     *
     * @access public
     *
     * @param resource $stream    A valid c-client stream.
     * @param string $folder      The full utf encoded folder to be created.
     * @param boolean $subscribe  A boolean describing whether or not to use
     *                            folder subscriptions.
     *
     * @return boolean  Whether or not the folder was successfully created.
     */
    function create($stream, $folder, $subscribe)
    {
        global $conf, $imp, $notification;

        /* Make sure we are not trying to create a duplicate folder */
        if ($this->exists($stream, $folder)) {
            $notification->push(sprintf(_("The folder \"%s\" already exists"), IMP::displayFolder($folder)), 'horde.warning');
            return false;
        }

        /* Attempt to create the mailbox */
        if (!imap_createmailbox($stream, IMP::serverString($folder))) {
            $notification->push(sprintf(_("The folder \"%s\" was not created. This is what the server said"), IMP::displayFolder($folder)) .
                            ': ' . imap_last_error(), 'horde.error');
            return false;
        }

        /* Reset the folder cache. */
        if ($conf['server']['cache_folders']) {
            unset($imp['cache']['folder_cache']);
        }

        /* If the user uses SUBSCRIBE, then add to the subscribe list */
        if ($subscribe &&
            !imap_subscribe($stream, IMP::serverString($folder))) {
            $notification->push(sprintf(_("The folder \"%s\" was created but you were not subscribed to it."), IMP::displayFolder($folder)), 'horde.warning');
            return false;
        }

        /* The folder creation has been successful */
        $notification->push(sprintf(_("The folder \"%s\" was successfully created."), IMP::displayFolder($folder)), 'horde.success');

        return true;
    }

    /**
     * Find out if a specific folder exists or not.
     *
     * @param resource $stream           A valid c-client stream.
     * @param string $folder             The full utf encoded folder name to
     *                                   be checked.
     * @param optional boolean $recheck  Should we recheck the folder?
     *
     * @return boolean  Whether or not the folder exists.
     */
    function exists($stream, $folder, $recheck = false)
    {
        if ($recheck || !array_key_exists($folder, $this->_existsResults)) {
            $path = IMP::serverString($folder);
            $res = imap_list($stream, '', $path);
            $this->_existsResults[$folder] = ($res ? in_array($path, $res) : false);
        }

        return $this->_existsResults[$folder];
    }

    /**
     * Rename an IMAP folder.
     *
     * @param resource $stream    A valid c-client stream.
     * @param string $old         The old utf encoded folder name.
     * @param string $new         The new utf encoded folder name.
     * @param boolean $subscribe  A boolean saying whether or not to use
     *                            folder subscriptions.
     *
     * @return boolean  Whether or not the folder was successfully renamed.
     */
    function rename($stream, $old, $new, $subscribe)
    {
        global $conf, $imp, $notification;

        $server = IMP::serverString();
        if ($subscribe) {
            $subscribed_folders = imap_listsubscribed($stream, $server, '*');
        }

        if (imap_renamemailbox($stream, $server . $old, $server . $new)) {
            if ($subscribe && in_array($server . $old, $subscribed_folders)) {
                imap_unsubscribe($stream, $server . $old);
                imap_subscribe($stream, $server . $new);
            }

            /* Reset the folder cache. */
            if ($conf['server']['cache_folders']) {
                unset($imp['cache']['folder_cache']);
            }

            $notification->push(sprintf(_("The folder \"%s\" was successfully renamed to \"%s\"."), IMP::displayFolder($old), IMP::displayFolder($new)), 'horde.success');
            return true;
        } else {
            $notification->push(sprintf(_("Renaming \"%s\" to \"%s\" failed. This is what the server said"), IMP::displayFolder($old), IMP::displayFolder($new)) .
                                ' :' . imap_last_error(), 'horde.error');
            return false;
        }
    }

    /**
     * Subscribe to one or more IMAP folders.
     *
     * @param resource $stream     A valid c-client stream.
     * @param array $folder_array  An array of full utf encoded folder names
     *                             to be subscribed.
     *
     * @return boolean  Whether or not the folders were successfully
     *                  subscribed to.
     */
    function subscribe($stream, $folder_array)
    {
        global $conf, $imp, $notification;

        $return_value = true;
        $update_cache = false;

        if (is_array($folder_array)) {
            foreach ($folder_array as $folder) {
                if ($folder != ' ') {
                    if (!imap_subscribe($stream, IMP::serverString($folder))) {
                        $notification->push(sprintf(_("You were not subscribed to \"%s\". Here is what the server said"), IMP::displayFolder($folder)) .
                                            ': ' . imap_last_error(), 'horde.error');
                        $return_value = false;
                    } else {
                        $notification->push(sprintf(_("You were successfully subscribed to \"%s\""), IMP::displayFolder($folder)), 'horde.success');
                        $update_cache = true;
                    }
                }
            }
        } else {
            $notification->push(_("No folders were specified"), 'horde.message');
            $return_value = false;
        }

        /* Reset the folder cache. */
        if ($update_cache && $conf['server']['cache_folders']) {
            unset($imp['cache']['folder_cache']);
        }

        return $return_value;
    }

    /**
     * Unsubscribe from one or more IMAP folders.
     *
     * @param resource $stream     A valid c-client stream.
     * @param array $folder_array  An array of full utf encoded folder names
     *                             to be unsubscribed.
     *
     * @return boolean  Whether or not the folders were successfully
     *                  unsubscribed from.
     */
    function unsubscribe($stream, $folder_array)
    {
        global $conf, $imp, $notification;

        $return_value = true;
        $update_cache = false;

        if (is_array($folder_array)) {
            foreach ($folder_array as $folder) {
                if ($folder != ' ') {
                    if (!imap_unsubscribe($stream, IMP::serverString($folder))) {
                        $notification->push(sprintf(_("You were not unsubscribed from \"%s\". Here is what the server said"), IMP::displayFolder($folder)) .
                                        ': ' . imap_last_error(), 'horde.error');
                        $return_value = false;
                    } else {
                        $notification->push(sprintf(_("You were successfully unsubscribed from \"%s\""), IMP::displayFolder($folder)), 'horde.success');
                        $update_cache = true;
                    }
                }
            }
        } else {
            $notification->push(_("No folders were specified"), 'horde.message');
            $return_value = false;
        }

        /* Reset the folder cache. */
        if ($update_cache && $conf['server']['cache_folders']) {
            unset($imp['cache']['folder_cache']);
        }

        return $return_value;
    }

    /**
     * Generate a string that can be saved out to an mbox format
     * mailbox file for a folder or set of folders, optionally
     * including all subfolders of the selected folders as well. All
     * folders will be put into the same string.
     *
     * @author Didi Rieder <adrieder@sbox.tugraz.at>
     *
     * @param resource $stream             A valid c-client stream.
     * @param array $folder_list           A list of full utf encoded folder
     *                                     names to generate an mbox file for.
     * @param boolean optional $recursive  Include subfolders?
     *
     * @return string  An mbox format mailbox file.
     */
    function generateMbox($stream, $folder_list, $recursive = false)
    {
        $body = '';

        if (is_array($folder_list)) {
            require_once IMP_BASE . '/lib/IMAP.php';
            $imp_imap = &IMP_IMAP::singleton();
            foreach ($folder_list as $folder) {
                $imp_imap->changeMbox($folder, OP_READONLY);
                $count = imap_num_msg($stream);
                for ($i = 1; $i <= $count; $i++) {
                    $h = imap_header($stream, $i);
                    $from = '<>';
                    if (isset($h->from[0])) {
                        if (isset($h->from[0]->mailbox) && isset($h->from[0]->host)) {
                            $from = $h->from[0]->mailbox . '@' . $h->from[0]->host;
                        }
                    }

                    $date = date('D M j H:i:s Y', $h->udate);
                    $body .= 'From ' . $from . ' ' . $date . "\n";
                    $body .= imap_fetchheader($stream, $i, FT_PREFETCHTEXT);
                    $body .= imap_body($stream, $i) . "\n";
                }
            }
        }

        $body = str_replace("\r\n", "\n", $body);
        return $body;
    }

    /**
     * Import messages into a given folder from a mbox format mailbox file.
     *
     * @access public
     *
     * @param resource $stream  A valid c-client stream.
     * @param string $folder    The folder to put the messages into.
     * @param string $mbox      String containing the mbox filename.
     *
     * @return mixed  False (boolean) on fail or the number of messages
     *                imported (integer) on success.
     */
    function importMbox($stream, $folder, $mbox)
    {
        $target = IMP::ServerString() . $folder;

        $message = '';
        $msgcount = 0;

        $fd = fopen ($mbox, "r");
        while (!feof ($fd)) {
            $line = fgets($fd, 4096);

            if (preg_match('/From .+@.+/A', $line)) {
                if (!empty($message)) {
                    // Make absolutely sure there are no bare newlines.
                    $message = preg_replace("|([^\r])\n|", "\\1\r\n", $message);
                    $message = str_replace("\n\n", "\n\r\n", $message);

                    if (imap_append($stream, $target, $message)) {
                        $msgcount++;
                    }
                }
                $message = '';
            } else {
                $message .= $line;
            }
        }
        fclose($fd);

        if (!empty($message)) {
            // Make absolutely sure there are no bare newlines.
            $message = preg_replace("|([^\r])\n|", "\\1\r\n", $message);
            $message = str_replace("\n\n", "\n\r\n", $message);

            if (imap_append($stream, $target, $message)) {
                $msgcount++;
            }
        }

        return ($msgcount > 0) ? $msgcount : false;
    }

    /**
     * Determine the hierarchy delimiter (generally '.' or '/') for
     * the current IMAP stream.
     *
     * @param resource $stream          A valid c-client stream.
     * @param optional string $default  The default delimiter in case we can't
     *                                  determine one.
     *
     * @return string  The hierarchy delimiter.
     */
    function getDelimiter($stream, $default = '/')
    {
        if (is_null($this->_delimiter)) {
            $box = @imap_getmailboxes($stream, IMP::serverString(), 'INBOX');
            $this->_delimiter = !empty($box[0]->delimiter) ? $box[0]->delimiter : $default;
        }

        return $this->_delimiter;
    }

}
