Import Horde_Imap_Client lib.
authorMichael M Slusarz <slusarz@curecanti.org>
Thu, 6 Nov 2008 05:49:17 +0000 (22:49 -0700)
committerMichael M Slusarz <slusarz@curecanti.org>
Thu, 6 Nov 2008 05:49:17 +0000 (22:49 -0700)
13 files changed:
framework/Imap_Client/lib/Horde/Imap/Client.php [new file with mode: 0644]
framework/Imap_Client/lib/Horde/Imap/Client/Base.php [new file with mode: 0644]
framework/Imap_Client/lib/Horde/Imap/Client/Cache.php [new file with mode: 0644]
framework/Imap_Client/lib/Horde/Imap/Client/Cclient-pop3.php [new file with mode: 0644]
framework/Imap_Client/lib/Horde/Imap/Client/Cclient.php [new file with mode: 0644]
framework/Imap_Client/lib/Horde/Imap/Client/Exception.php [new file with mode: 0644]
framework/Imap_Client/lib/Horde/Imap/Client/Socket.php [new file with mode: 0644]
framework/Imap_Client/lib/Horde/Imap/Client/Sort.php [new file with mode: 0644]
framework/Imap_Client/lib/Horde/Imap/Client/Utf7imap.php [new file with mode: 0644]
framework/Imap_Client/package.xml [new file with mode: 0644]
framework/Imap_Client/test/Horde/Imap/test_client.php [new file with mode: 0644]
framework/Imap_Client/test/Horde/Imap/test_email.txt [new file with mode: 0644]
framework/Imap_Client/test/Horde/Imap/test_email2.txt [new file with mode: 0644]

diff --git a/framework/Imap_Client/lib/Horde/Imap/Client.php b/framework/Imap_Client/lib/Horde/Imap/Client.php
new file mode 100644 (file)
index 0000000..ab2f73c
--- /dev/null
@@ -0,0 +1,548 @@
+<?php
+
+require_once dirname(__FILE__) . '/Client/Base.php';
+require_once dirname(__FILE__) . '/Client/Exception.php';
+require_once dirname(__FILE__) . '/Client/Utf7imap.php';
+
+/**
+ * Horde_Imap_Client:: provides an abstracted API interface to various IMAP
+ * backends (RFC 3501).
+
+ * Required Parameters:
+ *   password - (string) The IMAP user password.
+ *   username - (string) The IMAP username.
+ *
+ * Optional Parameters:
+ *   cache - (array) If set, caches data from fetch() calls. Requires
+ *           Horde_Cache and Horde_Serialize to be installed. The array can
+ *           contain the following keys (see Horde_Imap_Client_Cache:: for
+ *           default values):
+ * <pre>
+ * 'compress' - [OPTIONAL] (string) Compression to use on the cached data.
+ *              Either false, 'gzip' or 'lzf'.
+ * 'driver' - [REQUIRED] (string) The Horde_Cache driver to use.
+ * 'driver_params' - [REQUIRED] (array) The params to pass to the Horde_Cache
+ *                   driver.
+ * 'fields' - [OPTIONAL] (array) The fetch criteria to cache. If not defined,
+ *            all cacheable data is cached. The following is a list of
+ *            criteria that can be cached:
+ * <pre>
+ * Horde_Imap_Client::FETCH_STRUCTURE
+ * Horde_Imap_Client::FETCH_ENVELOPE
+ * Horde_Imap_Client::FETCH_FLAGS (only if server supports CONDSTORE IMAP
+ *                                 extension)
+ * Horde_Imap_Client::FETCH_DATE
+ * Horde_Imap_Client::FETCH_SIZE
+ * </pre>
+ * 'lifetime' - [OPTIONAL] (integer) The lifetime of the cache data (in secs).
+ * 'slicesize' - [OPTIONAL] (integer) The slicesize to use.
+ * </pre>
+ *   comparator - (string) The search comparator to use instead of the default
+ *                IMAP server comparator. See setComparator() for the format.
+ *                DEFAULT: Use the server default
+ *   debug - (string) If set, will output debug information to the stream
+ *           identified. The value can be any PHP supported wrapper that can
+ *           be opened via fopen().
+ *           DEFAULT: No debug output
+ *   hostspec - (string) The hostname or IP address of the server.
+ *              DEFAULT: 'localhost'
+ *   id - (array) Send ID information to the IMAP server (only if server
+ *        supports the ID extension). An array with the keys being the fields
+ *        to send and the values being the associated values. See RFC 2971
+ *        [3.3] for a list of defined field values.
+ *        DEFAULT: No info sent to server
+ *   lang - (array) A list of languages (in priority order) to be used to
+ *          display human readable messages.
+ *          DEFAULT: Messages output in IMAP server default language
+ *   port - (integer) The server port to which we will connect.
+ *           DEFAULT: 143 (imap or imap w/TLS) or 993 (imaps)
+ *   secure - (string) Use SSL or TLS to connect.
+ *            VALUES: false, 'ssl', 'tls'.
+ *            DEFAULT: No encryption
+ *   timeout - (integer)  Connection timeout, in seconds.
+ *             DEFAULT: 10 seconds
+ *
+ * Copyright 2008 The Horde Project (http://www.horde.org/)
+ *
+ * getBaseSubject() code adapted from imap-base-subject.c (Dovecot 1.2)
+ *   Original code released under the LGPL v2.1
+ *   Copyright (c) 2002-2008 Timo Sirainen <tss@iki.fi>
+ *
+ * $Horde: framework/Imap_Client/lib/Horde/Imap/Client.php,v 1.40 2008/10/27 21:02:55 slusarz Exp $
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author   Michael Slusarz <slusarz@curecanti.org>
+ * @category Horde
+ * @package  Horde_Imap_Client
+ */
+class Horde_Imap_Client
+{
+    /* Global constants. */
+    const USE_SEARCHRES = '*SEARCHRES*';
+
+    /* Constants for openMailbox() */
+    const OPEN_READONLY = 1;
+    const OPEN_READWRITE = 2;
+    const OPEN_AUTO = 3;
+
+    /* Constants for listMailboxes() */
+    const MBOX_SUBSCRIBED = 1;
+    const MBOX_UNSUBSCRIBED = 2;
+    const MBOX_ALL = 3;
+
+    /* Constants for status() */
+    const STATUS_MESSAGES = 1;
+    const STATUS_RECENT = 2;
+    const STATUS_UIDNEXT = 4;
+    const STATUS_UIDVALIDITY = 8;
+    const STATUS_UNSEEN = 16;
+    const STATUS_ALL = 32;
+    const STATUS_FIRSTUNSEEN = 64;
+    const STATUS_FLAGS = 128;
+    const STATUS_PERMFLAGS = 256;
+    const STATUS_HIGHESTMODSEQ = 512;
+    const STATUS_UIDNOTSTICKY = 1024;
+
+    /* Constants for search() */
+    const SORT_ARRIVAL = 1;
+    const SORT_CC = 2;
+    const SORT_DATE = 3;
+    const SORT_FROM = 4;
+    const SORT_REVERSE = 5;
+    const SORT_SIZE = 6;
+    const SORT_SUBJECT = 7;
+    const SORT_TO = 8;
+    /* SORT_THREAD provided for completeness - it is not a valid sort criteria
+     * for search() (use thread() instead). */
+    const SORT_THREAD = 9;
+
+    const SORT_RESULTS_COUNT = 1;
+    const SORT_RESULTS_MATCH = 2;
+    const SORT_RESULTS_MAX = 3;
+    const SORT_RESULTS_MIN = 4;
+    const SORT_RESULTS_SAVE = 5;
+
+    /* Constants for thread() */
+    const THREAD_ORDEREDSUBJECT = 1;
+    const THREAD_REFERENCES = 2;
+
+    /* Constants for fetch() */
+    const FETCH_STRUCTURE = 1;
+    const FETCH_FULLMSG = 2;
+    const FETCH_HEADERTEXT = 3;
+    const FETCH_BODYTEXT = 4;
+    const FETCH_MIMEHEADER = 5;
+    const FETCH_BODYPART = 6;
+    const FETCH_BODYPARTSIZE = 7;
+    const FETCH_HEADERS = 8;
+    const FETCH_ENVELOPE = 9;
+    const FETCH_FLAGS = 10;
+    const FETCH_DATE = 11;
+    const FETCH_SIZE = 12;
+    const FETCH_UID = 13;
+    const FETCH_SEQ = 14;
+    const FETCH_MODSEQ = 15;
+
+    /**
+     * The key used to encrypt the password when serializing.
+     *
+     * @var string
+     */
+    public static $encryptKey = null;
+
+    /**
+     * Autoload handler.
+     */
+    public static function autoload($classname)
+    {
+        $res = false;
+
+        $old_error = error_reporting(0);
+        switch ($classname) {
+        case 'Horde_MIME':
+            $res = require_once 'Horde/MIME.php';
+            break;
+
+        case 'Horde_MIME_Headers':
+            $res = require_once 'Horde/MIME/Headers.php';
+            break;
+
+        case 'Horde_MIME_Message':
+            $res = require_once 'Horde/MIME/Message.php';
+            break;
+
+        case 'Secret':
+            $res = require_once 'Horde/Secret.php';
+            break;
+        }
+        error_reporting($old_error);
+
+        return $res;
+    }
+
+    /**
+     * Attempts to return a concrete Horde_Imap_Client instance based on
+     * $driver.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $driver  The type of concrete Horde_Imap_Client subclass
+     *                        to return.
+     * @param array $params   A hash containing any additional configuration or
+     *                        connection parameters a subclass might need.
+     *
+     * @return mixed  The newly created concrete Horde_Imap_Client instance.
+     */
+    static final public function getInstance($driver, $params = array())
+    {
+        $class = 'Horde_Imap_Client_' . strtr(basename($driver), '-', '_');
+        if (!class_exists($class)) {
+            $fname = dirname(__FILE__) . '/Client/' . $driver . '.php';
+            if (is_file($fname)) {
+                require_once $fname;
+            }
+        }
+        if (!class_exists($class)) {
+            throw new Horde_Imap_Client_Exception('Driver ' . $driver . ' not found', Horde_Imap_Client_Exception::DRIVER_NOT_FOUND);
+        }
+        return new $class($params);
+    }
+
+    /**
+     * Create an IMAP message sequence string from a list of indices.
+     * Format: range_start:range_end,uid,uid2,range2_start:range2_end,...
+     *
+     * @param array $in  An array of indices.
+     * @param array $options  Additional options:
+     * <pre>
+     * 'nosort' - (boolean) Do not numerically sort the IDs before creating
+     *            the range?
+     *            DEFAULT: IDs are sorted
+     * </pre>
+     *
+     * @return string  The IMAP message sequence string.
+     */
+    static final public function toSequenceString($ids, $options = array())
+    {
+        if (empty($ids)) {
+            return '';
+        }
+
+        // Make sure IDs are unique
+        $ids = array_keys(array_flip($ids));
+
+        if (empty($options['nosort'])) {
+            sort($ids, SORT_NUMERIC);
+        }
+
+        $first = $last = array_shift($ids);
+        $out = array();
+
+        foreach ($ids as $val) {
+            if ($last + 1 == $val) {
+                $last = $val;
+            } else {
+                $out[] = $first . ($last == $first ? '' : (':' . $last));
+                $first = $last = $val;
+            }
+        }
+        $out[] = $first . ($last == $first ? '' : (':' . $last));
+
+        return implode(',', $out);
+    }
+
+    /**
+     * Parse an IMAP message sequence string into a list of indices.
+     * Format: range_start:range_end,uid,uid2,range2_start:range2_end,...
+     *
+     * @param string $str  The IMAP message sequence string.
+     *
+     * @return array  An array of indices.
+     */
+    static final public function fromSequenceString($str)
+    {
+        $ids = array();
+        $str = trim($str);
+
+        $idarray = explode(',', $str);
+        if (empty($idarray)) {
+            $idarray = array($str);
+        }
+
+        foreach ($idarray as $val) {
+            $range = array_map('intval', explode(':', $val));
+            if (count($range) == 1) {
+                $ids[] = $val;
+            } else {
+                list($low, $high) = ($range[0] < $range[1]) ? $range : array_reverse($range);
+                $ids = array_merge($ids, range($low, $high));
+            }
+        }
+
+        return $ids;
+    }
+
+    /**
+     * Remove "bare newlines" from a string.
+     *
+     * @param string $str  The original string.
+     *
+     * @return string  The string with all bare newlines removed.
+     */
+    static final public function removeBareNewlines($str)
+    {
+        return str_replace(array("\r\n", "\n"), array("\n", "\r\n"), $str);
+    }
+
+    /**
+     * Escape IMAP output via a quoted string (see RFC 3501 [4.3]).
+     *
+     * @param string $str  The unescaped string.
+     *
+     * @return string  The escaped string.
+     */
+    static final public function escape($str)
+    {
+        return '"' . addcslashes($str, '"\\') . '"';
+    }
+
+    /**
+     * Return the "base subject" defined in RFC 5256 [2.1].
+     *
+     * @param string $str     The original subject string.
+     * @param array $options  Additional options:
+     * <pre>
+     * 'keepblob' - (boolean) Don't remove any "blob" information (i.e. text
+     *              leading text between square brackets) from string.
+     * </pre>
+     *
+     * @return string  The cleaned up subject string.
+     */
+    static final public function getBaseSubject($str, $options = array())
+    {
+        // Rule 1a: MIME decode to UTF-8 (if possible).
+        $str = Horde_MIME::decode($str, 'UTF-8');
+
+        // Rule 1b: Remove superfluous whitespace.
+        $str = preg_replace("/\s{2,}/", '', $str);
+
+        do {
+            /* (2) Remove all trailing text of the subject that matches the
+             * the subj-trailer ABNF, repeat until no more matches are
+             * possible. */
+            $str = preg_replace("/(?:\s*\(fwd\)\s*)+$/i", '', $str);
+
+            do {
+                /* (3) Remove all prefix text of the subject that matches the
+                 * subj-leader ABNF. */
+                $found = self::_removeSubjLeader($str, !empty($options['keepblob']));
+
+                /* (4) If there is prefix text of the subject that matches
+                 * the subj-blob ABNF, and removing that prefix leaves a
+                 * non-empty subj-base, then remove the prefix text. */
+                $found = (empty($options['keepblob']) && self::_removeBlobWhenNonempty($str)) || $found;
+
+                /* (5) Repeat (3) and (4) until no matches remain. */
+            } while ($found);
+
+            /* (6) If the resulting text begins with the subj-fwd-hdr ABNF and
+             * ends with the subj-fwd-trl ABNF, remove the subj-fwd-hdr and
+             * subj-fwd-trl and repeat from step (2). */
+        } while (self::_removeSubjFwdHdr($str));
+
+        return $str;
+    }
+
+    /**
+     * Remove all prefix text of the subject that matches the subj-leader
+     * ABNF.
+     *
+     * @param string &$str       The subject string.
+     * @param boolean $keepblob  Remove blob information?
+     *
+     * @return boolean  True if string was altered.
+     */
+    static final protected function _removeSubjLeader(&$str, $keepblob = false)
+    {
+        $ret = false;
+
+        if ($str[0] == ' ') {
+            $str = substr($str, 1);
+            $ret = true;
+        }
+
+        $i = 0;
+
+        if (!$keepblob) {
+            while ($str[$i] == '[') {
+                if (($i = self::_removeBlob($str, $i)) === false) {
+                    return $ret;
+                }
+            }
+        }
+
+        $cmp_str = substr($str, $i);
+        if (stripos($cmp_str, 're') === 0) {
+            $i += 2;
+        } elseif (stripos($cmp_str, 'fwd') === 0) {
+            $i += 3;
+        } elseif (stripos($cmp_str, 'fw') === 0) {
+            $i += 2;
+        } else {
+            return $ret;
+        }
+
+        if ($str[$i] == ' ') {
+            ++$i;
+        }
+
+        if (!$keepblob) {
+            while ($str[$i] == '[') {
+                if (($i = self::_removeBlob($str, $i)) === false) {
+                    return $ret;
+                }
+            }
+        }
+
+        if ($str[$i] != ':') {
+            return $ret;
+        }
+
+        $str = substr($str, ++$i);
+
+        return true;
+    }
+
+    /**
+     * Remove "[...]" text.
+     *
+     * @param string &$str  The subject string.
+     *
+     * @return boolean  True if string was altered.
+     */
+    static final protected function _removeBlob($str, $i)
+    {
+        if ($str[$i] != '[') {
+            return false;
+        }
+
+        ++$i;
+
+        for ($cnt = strlen($str); $i < $cnt; ++$i) {
+            if ($str[$i] == ']') {
+                break;
+            }
+
+            if ($str[$i] == '[') {
+                return false;
+            }
+        }
+
+        if ($i == ($cnt - 1)) {
+            return false;
+        }
+
+        ++$i;
+
+        if ($str[$i] == ' ') {
+            ++$i;
+        }
+
+        return $i;
+    }
+
+    /**
+     * Remove "[...]" text if it doesn't result in the subject becoming
+     * empty.
+     *
+     * @param string &$str  The subject string.
+     *
+     * @return boolean  True if string was altered.
+     */
+    static final protected function _removeBlobWhenNonempty(&$str)
+    {
+        if (($str[0] == '[') &&
+            (($i = self::_removeBlob($str, 0)) !== false) &&
+            ($i != strlen($str))) {
+            $str = substr($str, $i);
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Remove a "[fwd: ... ]" string.
+     *
+     * @param string &$str  The subject string.
+     *
+     * @return boolean  True if string was altered.
+     */
+    static final protected function _removeSubjFwdHdr(&$str)
+    {
+        if ((stripos($str, '[fwd:') !== 0) || (substr($str, -1) != ']')) {
+            return false;
+        }
+
+        $str = substr($str, 5, -1);
+        return true;
+    }
+
+    /**
+     * Parse an IMAP URL (RFC 5092).
+     *
+     * @param string $url  A IMAP URL string.
+     *
+     * @return mixed  False if the URL is invalid.  If valid, a URL with the
+     *                following fields:
+     * <pre>
+     * 'auth' - (string) The authentication method to use.
+     * 'port' - (integer) The remote port
+     * 'hostspec' - (string) The remote server
+     * 'username' - (string) The username to use on the remote server.
+     * </pre>
+     */
+    static final public function parseImapURL($url)
+    {
+        $url = trim($url);
+        if (stripos($url, 'imap://') !== 0) {
+            return false;
+        }
+        $url = substr($url, 7);
+
+        /* At present, only support imap://<iserver>[/] style URLs. */
+        if (($pos = strpos($url, '/')) !== false) {
+            $url = substr($url, 0, $pos);
+        }
+
+        $ret_array = array();
+
+        /* Check for username/auth information. */
+        if (($pos = strpos($url, '@')) !== false) {
+            if ((($apos = stripos($url, ';AUTH=')) !== false) &&
+                ($apos < $pos)) {
+                $auth = substr($url, $apos + 6, $pos - $apos - 6);
+                if ($auth != '*') {
+                    $ret_array['auth'] = $auth;
+                }
+                if ($apos) {
+                    $ret_array['username'] = substr($url, 0, $apos);
+                }
+            }
+            $url = substr($url, $pos + 1);
+        }
+
+        /* Check for port information. */
+        if (($pos = strpos($url, ':')) !== false) {
+            $ret_array['port'] = substr($url, $pos + 1);
+            $url = substr($url, 0, $pos);
+        }
+
+        $ret_array['hostspec'] = $url;
+
+        return $ret_array;
+    }
+}
+
+spl_autoload_register(array('Horde_Imap_Client_Base', 'autoload'));
diff --git a/framework/Imap_Client/lib/Horde/Imap/Client/Base.php b/framework/Imap_Client/lib/Horde/Imap/Client/Base.php
new file mode 100644 (file)
index 0000000..02e0a94
--- /dev/null
@@ -0,0 +1,2983 @@
+<?php
+/**
+ * Horde_Imap_Client_Base:: provides an abstracted API interface to various
+ * IMAP backends supporting the IMAP4rev1 protocol (RFC 3501).
+ *
+ * Required/Optional Parameters: See Horde_Imap_Client::.
+ *
+ * Copyright 2008 The Horde Project (http://www.horde.org/)
+ *
+ * $Horde: framework/Imap_Client/lib/Horde/Imap/Client/Base.php,v 1.81 2008/10/29 05:13:09 slusarz Exp $
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author   Michael Slusarz <slusarz@curecanti.org>
+ * @category Horde
+ * @package  Horde_Imap_Client
+ */
+abstract class Horde_Imap_Client_Base extends Horde_Imap_Client
+{
+    /**
+     * Hash containing connection parameters.
+     *
+     * @var array
+     */
+    protected $_params = array();
+
+    /**
+     * Is there an active authenticated connection to the IMAP Server?
+     *
+     * @var boolean
+     */
+    protected $_isAuthenticated = false;
+
+    /**
+     * Is there a secure connection to the IMAP Server?
+     *
+     * @var boolean
+     */
+    protected $_isSecure = false;
+
+    /**
+     * The currently selected mailbox.
+     *
+     * @var string
+     */
+    protected $_selected = null;
+
+    /**
+     * The current mailbox selection mode.
+     *
+     * @var integer
+     */
+    protected $_mode = 0;
+
+    /**
+     * Server data that will be cached when serialized.
+     *
+     * @var array
+     */
+    protected $_init = array(
+        'enabled' => array(),
+        'namespace' => array()
+    );
+
+    /**
+     * The Horde_Imap_Client_Cache object.
+     *
+     * @var Horde_Cache
+     */
+    protected $_cacheOb = null;
+
+    /**
+     * The debug stream.
+     *
+     * @var resource
+     */
+    protected $_debug = null;
+
+    /**
+     * Constructs a new Horde_Imap_Client object.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param array $params  A hash containing configuration parameters.
+     */
+    public function __construct($params = array())
+    {
+        if (!isset($params['username']) || !isset($params['password'])) {
+            throw new Horde_Imap_Client_Exception('Horde_Imap_Client requires a username and password.');
+        }
+
+        // Default values.
+        if (empty($params['hostspec'])) {
+            $params['hostspec'] = 'localhost';
+        }
+
+        if (empty($params['port'])) {
+            $params['port'] = (isset($params['secure']) && ($params['secure'] == 'ssl')) ? 993 : 143;
+        }
+
+        if (empty($params['timeout'])) {
+            $params['timeout'] = 10;
+        }
+
+        if (empty($params['cache'])) {
+            $params['cache'] = array('fields' => array());
+        } elseif (empty($params['cache']['fields'])) {
+            $params['cache']['fields'] = array(
+                self::FETCH_STRUCTURE => 1,
+                self::FETCH_ENVELOPE => 1,
+                self::FETCH_FLAGS => 1,
+                self::FETCH_DATE => 1,
+                self::FETCH_SIZE => 1
+            );
+        } else {
+            $params['cache']['fields'] = array_flip($params['cache']['fields']);
+        }
+
+        $this->_params = $params;
+
+        // This will initialize debugging, if needed.
+        $this->__wakeup();
+    }
+
+    /**
+     * Destructor.
+     */
+    function __destruct()
+    {
+        $this->_closeDebug();
+    }
+
+    /**
+     * Do cleanup prior to serialization.
+     */
+    function __sleep()
+    {
+        $this->_closeDebug();
+
+        // Don't store Horde_Imap_Client_Cache object.
+        $this->_cacheOb = null;
+
+        // Encrypt password in serialized object.
+        if (!isset($this->_params['_passencrypt'])) {
+            $key = Horde_Imap_Client::$encryptKey;
+            if (!is_null($key)) {
+                $this->_params['_passencrypt'] = Secret::write($key, $this->_params['password']);
+                $this->_params['password'] = null;
+            }
+        }
+    }
+
+    /**
+     * Do re-initialization on unserialize().
+     */
+    function __wakeup()
+    {
+        if (isset($this->_params['_passencrypt']) &&
+            !is_null(Horde_Imap_Client::$encryptKey)) {
+            $this->_params['password'] = Secret::read(Horde_Imap_Client::$encryptKey, $this->_params['_passencrypt']);
+        }
+
+        if (!empty($this->_params['debug'])) {
+            $this->_debug = fopen($this->_params['debug'], 'a');
+        }
+    }
+
+    /**
+     * Close debugging output.
+     */
+    protected function _closeDebug()
+    {
+        if (is_resource($this->_debug)) {
+            fflush($this->_debug);
+            fclose($this->_debug);
+            $this->_debug = null;
+        }
+    }
+
+    /**
+     * Initialize the Horde_Imap_Client_Cache object, if necessary.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @return boolean  Returns true if caching is enabled.
+     */
+    protected function _initCacheOb()
+    {
+        if (empty($this->_params['cache']['fields'])) {
+            return false;
+        }
+
+        if (is_null($this->_cacheOb)) {
+            $p = $this->_params;
+            require_once dirname(__FILE__) . '/Cache.php';
+            $this->_cacheOb = &Horde_Imap_Client_Cache::singleton(array_merge($p['cache'], array(
+                'debug' => $this->_debug,
+                'hostspec' => $p['hostspec'],
+                'username' => $p['username']
+            )));
+        }
+
+        return true;
+    }
+
+    /**
+     * Returns the Horde_Imap_Client_Cache object used, if available.
+     *
+     * @return mixed  Either the object or null.
+     */
+    public function getCacheOb()
+    {
+        $this->_initCacheOb();
+        return $this->_cacheOb;
+    }
+
+    /**
+     * Returns whether the IMAP server supports the given capability
+     * (See RFC 3501 [6.1.1]).
+     *
+     * @param string $capability  The capability string to query.
+     *
+     * @param mixed  True if the server supports the queried capability,
+     *               false if it doesn't, or an array if the capability can
+     *               contain multiple values.
+     */
+    public function queryCapability($capability)
+    {
+        if (!isset($this->_init['capability'])) {
+            try {
+                $this->capability();
+            } catch (Horde_Imap_Client_Exception $e) {
+                return false;
+            }
+        }
+        $capability = strtoupper($capability);
+        return isset($this->_init['capability'][$capability]) ? $this->_init['capability'][$capability] : false;
+    }
+
+    /**
+     * Get CAPABILITY information from the IMAP server.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @return array  The capability array.
+     */
+    public function capability()
+    {
+        if (!isset($this->_init['capability'])) {
+            $this->_init['capability'] = $this->_capability();
+        }
+
+        return $this->_init['capability'];
+    }
+
+    /**
+     * Get CAPABILITY information from the IMAP server.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @return array  The capability array.
+     */
+    abstract protected function _capability();
+
+    /**
+     * Send a NOOP command (RFC 3501 [6.1.2]).
+     * Throws a Horde_Imap_Client_Exception on error.
+     */
+    public function noop()
+    {
+        // NOOP only useful if we are already authenticated.
+        if ($this->_isAuthenticated) {
+            $this->_noop();
+        }
+    }
+
+    /**
+     * Send a NOOP command.
+     * Throws a Horde_Imap_Client_Exception on error.
+     */
+    abstract protected function _noop();
+
+    /**
+     * Get the NAMESPACE information from the IMAP server (RFC 2342).
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param array $additional  If the server supports namespaces, any
+     *                           additional namespaces to add to the
+     *                           namespace list that are not broadcast by
+     *                           the server. The namespaces must either be in
+     *                           UTF7-IMAP or UTF-8.
+     *
+     * @return array  An array of namespace information with the name as the
+     *                key and the following values:
+     * <pre>
+     * 'delimiter' - (string) The namespace delimiter.
+     * 'hidden' - (boolean) Is this a hidden namespace?
+     * 'name' - (string) The namespace name.
+     * 'translation' - OPTIONAL (string) This entry only present if the IMAP
+     *                 server supports RFC 5255 and the language has previous
+     *                 been set via setLanguage(). The translation will be in
+     *                 UTF7-IMAP.
+     * 'type' - (string) The namespace type (either 'personal', 'other' or
+     *          'shared').
+     * </pre>
+     */
+    public function getNamespaces($additional = array())
+    {
+        $additional = array_map(array('Horde_Imap_Client_Utf7imap', 'Utf7ImapToUtf8'), $additional);
+
+        $sig = md5(serialize($additional));
+
+        if (isset($this->_init['namespace'][$sig])) {
+            return $this->_init['namespace'][$sig];
+        }
+
+        $ns = $this->_getNamespaces();
+
+        foreach ($additional as $val) {
+            /* Skip namespaces if we have already auto-detected them. Also,
+             * hidden namespaces cannot be empty. */
+            $val = trim($val);
+            if (empty($val) || isset($ns[$val])) {
+                continue;
+            }
+
+            $mbox = $this->listMailboxes($val, self::MBOX_ALL, array('delimiter' => true));
+            $first = reset($mbox);
+
+            if ($first && ($first['mailbox'] == $val)) {
+                $ns[$val] = array(
+                    'name' => $val,
+                    'delimiter' => $first['delimiter'],
+                    'type' => 'shared',
+                    'hidden' => true
+                );
+            }
+        }
+
+        if (empty($ns)) {
+            /* This accurately determines the namespace information of the
+             * base namespace if the NAMESPACE command is not supported.
+             * See: RFC 3501 [6.3.8] */
+            $mbox = $this->listMailboxes('', self::MBOX_ALL, array('delimiter' => true));
+            $first = reset($mbox);
+            $ns[''] = array(
+                'name' => '',
+                'delimiter' => $first['delimiter'],
+                'type' => 'personal',
+                'hidden' => false
+            );
+        }
+
+        $this->_init['namespace'][$sig] = $ns;
+
+        return $ns;
+    }
+
+    /**
+     * Get the NAMESPACE information from the IMAP server.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @return array  An array of namespace information.
+     */
+    abstract protected function _getNamespaces();
+
+    /**
+     * Display if connection to the server has been secured via TLS or SSL.
+     *
+     * @return boolean  True if the IMAP connection is secured.
+     */
+    public function isSecureConnection()
+    {
+        return $this->_isSecure;
+    }
+
+    /**
+     * Return a list of alerts that MUST be presented to the user (RFC 3501
+     * [7.1]).
+     *
+     * @return array  An array of alert messages.
+     */
+    abstract public function alerts();
+
+    /**
+     * Login to the IMAP server.
+     * Throws a Horde_Imap_Client_Exception on error.
+     */
+    public function login()
+    {
+        if ($this->_isAuthenticated) {
+            return;
+        }
+
+        if ($this->_login()) {
+            if (!empty($this->_params['id'])) {
+                try {
+                    $this->sendID();
+                } catch (Horde_Imap_Client_Exception $e) {
+                    // Ignore if server doesn't support ID
+                    if ($e->getCode() != Horde_Imap_Client_Exception::NOSUPPORTIMAPEXT) {
+                        throw $e;
+                    }
+                }
+            }
+
+            if (!empty($this->_params['comparator'])) {
+                try {
+                    $this->setComparator();
+                } catch (Horde_Imap_Client_Exception $e) {
+                    // Ignore if server doesn't support I18NLEVEL=2
+                    if ($e->getCode() != Horde_Imap_Client_Exception::NOSUPPORTIMAPEXT) {
+                        throw $e;
+                    }
+                }
+            }
+
+            /* Check for ability to cache flags here. */
+            if (isset($this->_params['cache']['fields'][self::FETCH_FLAGS]) &&
+                !isset($this->_init['enabled']['CONDSTORE'])) {
+                unset($this->_params['cache']['fields'][self::FETCH_FLAGS]);
+            }
+        }
+
+        $this->_isAuthenticated = true;
+    }
+
+    /**
+     * Login to the IMAP server.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @return boolean  Return true if global login tasks should be run.
+     */
+    abstract protected function _login();
+
+    /**
+     * Logout from the IMAP server (see RFC 3501 [6.1.3]).
+     */
+    public function logout()
+    {
+        $this->_logout();
+        $this->_isAuthenticated = false;
+        $this->_selected = null;
+        $this->_mode = 0;
+    }
+
+    /**
+     * Logout from the IMAP server (see RFC 3501 [6.1.3]).
+     */
+    abstract protected function _logout();
+
+    /**
+     * Send ID information to the IMAP server (RFC 2971).
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param array $info  Overrides the value of the 'id' param and sends
+     *                     this information instead.
+     */
+    public function sendID($info = null)
+    {
+        if (!$this->queryCapability('ID')) {
+            throw new Horde_Imap_Client_Exception('The IMAP server does not support the ID extension.', Horde_Imap_Client_Exception::NOSUPPORTIMAPEXT);
+        }
+
+        $this->_sendID(is_null($info) ? (empty($this->_params['id']) ? array() : $this->_params['id']) : $info);
+    }
+
+    /**
+     * Send ID information to the IMAP server (RFC 2971).
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param array $info  The information to send to the server.
+     */
+    abstract protected function _sendID($info);
+
+    /**
+     * Return ID information from the IMAP server (RFC 2971).
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @return array  An array of information returned, with the keys as the
+     *                'field' and the values as the 'value'.
+     */
+    public function getID()
+    {
+        if (!$this->queryCapability('ID')) {
+            throw new Horde_Imap_Client_Exception('The IMAP server does not support the ID extension.', Horde_Imap_Client_Exception::NOSUPPORTIMAPEXT);
+        }
+
+        return $this->_getID();
+    }
+
+    /**
+     * Return ID information from the IMAP server (RFC 2971).
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @return array  An array of information returned, with the keys as the
+     *                'field' and the values as the 'value'.
+     */
+    abstract protected function _getID();
+
+    /**
+     * Sets the preferred language for server response messages (RFC 5255).
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param array $info  Overrides the value of the 'lang' param and sends
+     *                     this list of preferred languages instead. The
+     *                     special string 'i-default' can be used to restore
+     *                     the language to the server default.
+     *
+     * @return string  The language accepted by the server, or null if the
+     *                 default language is used.
+     */
+    public function setLanguage($langs = null)
+    {
+        if (!$this->queryCapability('LANGUAGE')) {
+            return null;
+        }
+
+        $lang = is_null($langs) ? (empty($this->_params['lang']) ? null : $this->_params['lang']) : $langs;
+        if (is_null($lang)) {
+            return null;
+        }
+
+        return $this->_setLanguage($lang);
+    }
+
+    /**
+     * Sets the preferred language for server response messages (RFC 5255).
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param array $info  The preferred list of languages.
+     *
+     * @return string  The language accepted by the server, or null if the
+     *                 default language is used.
+     */
+    abstract protected function _setLanguage($langs);
+
+    /**
+     * Gets the preferred language for server response messages (RFC 5255).
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param array $list  If true, return the list of available languages.
+     *
+     * @return mixed  If $list is true, the list of languages available on the
+     *                server (may be empty). If false, the language used by
+     *                the server, or null if the default language is used.
+     */
+    public function getLanguage($list = false)
+    {
+        if (!$this->queryCapability('LANGUAGE')) {
+            return $list ? array() : null;
+        }
+
+        return $this->_getLanguage($list);
+    }
+
+    /**
+     * Gets the preferred language for server response messages (RFC 5255).
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param array $list  If true, return the list of available languages.
+     *
+     * @return mixed  If $list is true, the list of languages available on the
+     *                server (may be empty). If false, the language used by
+     *                the server, or null if the default language is used.
+     */
+    abstract protected function _getLanguage($list);
+
+    /**
+     * Open a mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox  The mailbox to open. Either in UTF7-IMAP or
+     *                         UTF-8.
+     * @param integer $mode    The access mode. Either
+     *                         Horde_Imap_Client::OPEN_READONLY,
+     *                         Horde_Imap_Client::OPEN_READWRITE, or
+     *                         Horde_Imap_Client::OPEN_AUTO.
+     */
+    public function openMailbox($mailbox, $mode = self::OPEN_AUTO)
+    {
+        $change = false;
+
+        $mailbox = Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($mailbox);
+
+        if ($mode == self::OPEN_AUTO) {
+            if (is_null($this->_selected) || ($this->_selected != $mailbox)) {
+                $mode = self::OPEN_READONLY;
+                $change = true;
+            }
+        } elseif (is_null($this->_selected) ||
+                  ($this->_selected != $mailbox) ||
+                  ($mode != $this->_mode)) {
+            $change = true;
+        }
+
+        if ($change) {
+            $this->_openMailbox($mailbox, $mode);
+            $this->_selected = $mailbox;
+            $this->_mode = $mode;
+        }
+    }
+
+    /**
+     * Open a mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox  The mailbox to open (UTF7-IMAP).
+     * @param integer $mode    The access mode.
+     */
+    abstract protected function _openMailbox($mailbox, $mode);
+
+    /**
+     * Return the currently opened mailbox and access mode.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param array $options  Additional options:
+     * <pre>
+     * 'utf8' - (boolean) True if 'mailbox' should be in UTF-8.
+     *          DEFAULT: 'mailbox' returned in UTF7-IMAP.
+     * </pre>
+     *
+     * @return mixed  Either an array with two elements - 'mailbox' and
+     *                'mode' - or null if no mailbox selected.
+     */
+    public function currentMailbox($options = array())
+    {
+        return is_null($this->_selected)
+            ? null
+            : array(
+                'mailbox' => empty($options['utf8']) ? $this->_selected : Horde_Imap_Client_Utf7imap::Utf7ImapToUtf8($this->_selected),
+                'mode' => $this->_mode
+            );
+    }
+
+    /**
+     * Create a mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox  The mailbox to create. Either in UTF7-IMAP or
+     *                         UTF-8.
+     */
+    public function createMailbox($mailbox)
+    {
+        $this->_createMailbox(Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($mailbox));
+    }
+
+    /**
+     * Create a mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox  The mailbox to create (UTF7-IMAP).
+     */
+    abstract protected function _createMailbox($mailbox);
+
+    /**
+     * Delete a mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox  The mailbox to delete. Either in UTF7-IMAP or
+     *                         UTF-8.
+     */
+    public function deleteMailbox($mailbox)
+    {
+        $mailbox = Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($mailbox);
+
+        $this->_deleteMailbox($mailbox);
+
+        /* Delete mailbox cache. */
+        if ($this->_initCacheOb()) {
+            $this->_cacheOb->deleteMailbox($mailbox);
+        }
+
+        /* Unsubscribe from mailbox. */
+        try {
+            $this->subscribeMailbox($mailbox, false);
+        } catch (Horde_Imap_Client_Exception $e) {
+            // Ignore failed unsubscribe request
+        }
+    }
+
+    /**
+     * Delete a mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox  The mailbox to delete (UTF7-IMAP).
+     */
+    abstract protected function _deleteMailbox($mailbox);
+
+    /**
+     * Rename a mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $old     The old mailbox name. Either in UTF7-IMAP or
+     *                        UTF-8.
+     * @param string $new     The new mailbox name. Either in UTF7-IMAP or
+     *                        UTF-8.
+     */
+    public function renameMailbox($old, $new)
+    {
+        $old = Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($old);
+        $new = Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($new);
+
+        /* Check if old mailbox was subscribed to. */
+        $subscribed = $this->listMailboxes($old, self::MBOX_SUBSCRIBED, array('flat' => true));
+
+        $this->_renameMailbox($old, $new);
+
+        /* Delete mailbox cache. */
+        if ($this->_initCacheOb()) {
+            $this->_cacheOb->deleteMailbox($old);
+        }
+
+        /* Clean up subscription information. */
+        try {
+            $this->subscribeMailbox($old, false);
+            if (count($subscribed)) {
+                $this->subscribeMailbox($new, true);
+            }
+        } catch (Horde_Imap_Client_Exception $e) {
+            // Ignore failed unsubscribe request
+        }
+    }
+
+    /**
+     * Rename a mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $old     The old mailbox name (UTF7-IMAP).
+     * @param string $new     The new mailbox name (UTF7-IMAP).
+     */
+    abstract protected function _renameMailbox($old, $new);
+
+    /**
+     * Manage subscription status for a mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox     The mailbox to [un]subscribe to. Either in
+     *                            UTF7-IMAP or UTF-8.
+     * @param boolean $subscribe  True to subscribe, false to unsubscribe.
+     */
+    public function subscribeMailbox($mailbox, $subscribe = true)
+    {
+        $this->_subscribeMailbox(Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($mailbox), (bool)$subscribe);
+    }
+
+    /**
+     * Manage subscription status for a mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox     The mailbox to [un]subscribe to (UTF7-IMAP).
+     * @param boolean $subscribe  True to subscribe, false to unsubscribe.
+     */
+    abstract protected function _subscribeMailbox($mailbox, $subscribe);
+
+    /**
+     * Obtain a list of mailboxes matching a pattern.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @todo RFC 5258 extensions
+     *
+     * @param string $pattern  The mailbox search pattern (see RFC 3501
+     *                         [6.3.8] for the format). Either in UTF7-IMAP or
+     *                         UTF-8.
+     * @param integer $mode    Which mailboxes to return.  Either
+     *                         Horde_Imap_Client::MBOX_SUBSCRIBED,
+     *                         Horde_Imap_Client::MBOX_UNSUBSCRIBED, or
+     *                         Horde_Imap_Client::MBOX_ALL.
+     * @param array $options   Additional options:
+     * <pre>
+     * 'attributes' - (boolean) If true, return attribute information under
+     *                the 'attributes' key. The attributes will be returned
+     *                in an array with each attribute in lowercase.
+     *                DEFAULT: Do not return this information.
+     * 'utf8' - (boolean) True to return mailbox names in UTF-8.
+     *          DEFAULT: Names are returned in UTF7-IMAP.
+     * 'delimiter' - (boolean) If true, return delimiter information under
+     *               the 'delimiter' key.
+     *               DEFAULT: Do not return this information.
+     * 'flat' - (boolean) If true, return a flat list of mailbox names only.
+     *          Overrides both the 'attributes' and 'delimiter' options.
+     *          DEFAULT: Do not return flat list.
+     * 'sort' - (boolean) If true, return a sorted list of mailboxes?
+     *          DEFAULT: Do not sort the list.
+     * 'sort_delimiter' - (string) If 'sort' is true, this is the delimiter
+     *                    used to sort the mailboxes.
+     *                    DEFAULT: '.'
+     * </pre>
+     *
+     * @return array  If 'flat' option is true, the array values are the list
+     *                of mailboxes.  Otherwise, the array values are arrays
+     *                with the following keys: 'mailbox', 'attributes' (only
+     *                if 'attributes' option is true), and 'delimiter' (only
+     *                if 'delimiter' option is true).
+     */
+    public function listMailboxes($pattern, $mode = self::MBOX_ALL,
+                                  $options = array())
+    {
+        $ret = $this->_listMailboxes(Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($pattern), $mode, $options);
+
+        if (!empty($options['sort'])) {
+            require_once dirname(__FILE__) . '/Sort.php';
+            Horde_Imap_Client_Sort::sortMailboxes($ret, array('delimiter' => empty($options['sort_delimiter']) ? '.' : $options['sort_delimiter'], 'index' => false, 'keysort' => empty($options['flat'])));
+        }
+
+        return $ret;
+    }
+
+    /**
+     * Obtain a list of mailboxes matching a pattern.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $pattern  The mailbox search pattern (UTF7-IMAP).
+     * @param integer $mode    Which mailboxes to return.
+     * @param array $options   Additional options.
+     *
+     * @return array  See self::listMailboxes().
+     */
+    abstract protected function _listMailboxes($pattern, $mode, $options);
+
+    /**
+     * Obtain status information for a mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox  The mailbox to query. Either in UTF7-IMAP or
+     *                         or UTF-8.
+     * @param string $flags    A bitmask of information requested from the
+     *                         server. Allowed flags:
+     * <pre>
+     * Flag: Horde_Imap_Client::STATUS_MESSAGES
+     *   Return key: 'messages'
+     *   Return format: (integer) The number of messages in the mailbox.
+     *
+     * Flag: Horde_Imap_Client::STATUS_RECENT
+     *   Return key: 'recent'
+     *   Return format: (integer) The number of messages with the '\Recent'
+     *                  flag set
+     *
+     * Flag: Horde_Imap_Client::STATUS_UIDNEXT
+     *   Return key: 'uidnext'
+     *   Return format: (integer) The next UID to be assigned in the mailbox.
+     *
+     * Flag: Horde_Imap_Client::STATUS_UIDVALIDITY
+     *   Return key: 'uidvalidity'
+     *   Return format: (integer) The unique identifier validity of the
+     *                  mailbox.
+     *
+     * Flag: Horde_Imap_Client::STATUS_UNSEEN
+     *   Return key: 'unseen'
+     *   Return format: (integer) The number of messages which do not have
+     *                  the '\Seen' flag set.
+     *
+     * Flag: Horde_Imap_Client::STATUS_FIRSTUNSEEN
+     *   Return key: 'firstunseen'
+     *   Return format: (integer) The sequence number of the first unseen
+     *                  message in the mailbox.
+     *
+     * Flag: Horde_Imap_Client::STATUS_FLAGS
+     *   Return key: 'flags'
+     *   Return format: (array) The list of defined flags in the mailbox (all
+     *                  flags are in lowercase).
+     *
+     * Flag: Horde_Imap_Client::STATUS_PERMFLAGS
+     *   Return key: 'permflags'
+     *   Return format: (array) The list of flags that a client can change
+     *                  permanently (all flags are in lowercase).
+     *
+     * Flag: Horde_Imap_Client::STATUS_HIGHESTMODSEQ
+     *   Return key: 'highestmodseq'
+     *   Return format: (mixed) If the server supports the CONDSTORE IMAP
+     *                  extension, this will be the highest mod-sequence value
+     *                  of all messages in the mailbox or null if the mailbox
+     *                  does not support mod-sequences. Else, this value will
+     *                  be undefined.
+     *
+     * Flag: Horde_Imap_Client::STATUS_UIDNOTSTICKY
+     *   Return key: 'uidnotsticky'
+     *   Return format: (boolean) If the server supports the UIDPLUS IMAP
+     *                  extension, and the queried mailbox does not support
+     *                  persistent UIDs, this value will be true. In all
+     *                  other cases, this value will be false.
+     *
+     * Flag: Horde_Imap_Client::STATUS_ALL (DEFAULT)
+     *   A shortcut to return 'messages', 'recent', 'uidnext', 'uidvalidity',
+     *   and 'unseen'.
+     * </pre>
+     *
+     * @return array  An array with the requested keys (see above).
+     */
+    public function status($mailbox, $flags = self::STATUS_ALL)
+    {
+        if ($flags & self::STATUS_ALL) {
+            $flags |= self::STATUS_MESSAGES | self::STATUS_RECENT | self::STATUS_UNSEEN | self::STATUS_UIDNEXT | self::STATUS_UIDVALIDITY;
+        }
+
+        return $this->_status(Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($mailbox), $flags);
+    }
+
+    /**
+     * Obtain status information for a mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox  The mailbox to query (UTF7-IMAP).
+     * @param string $flags    A bitmask of information requested from the
+     *                         server.
+     *
+     * @return array  See Horde_Imap_Client_Base::status().
+     */
+    abstract protected function _status($mailbox, $flags);
+
+    /**
+     * Append message(s) to a mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox  The mailbox to append the message(s) to. Either
+     *                         in UTF7-IMAP or UTF-8.
+     * @param array $data      The message data to append, along with
+     *                         additional options. An array of arrays with
+     *                         each embedded array having the following
+     *                         entries:
+     * <pre>
+     * 'data' - (mixed) The data to append. Either a string or a stream
+     *          resource.
+     *          DEFAULT: NONE (entry is MANDATORY)
+     * 'flags' - (array) An array of flags/keywords to set on the appended
+     *           message.
+     *           DEFAULT: Only the '\Recent' flag is set.
+     * 'internaldate' - (DateTime object) The internaldate to set for the
+     *                  appended message.
+     *                  DEFAULT: internaldate will be the same date as when
+     *                  the message was appended.
+     * 'messageid' - (string) For servers/drivers that support the UIDPLUS
+     *               IMAP extension, the UID of the appended message(s) can be
+     *               determined automatically. If this extension is not
+     *               available, the message-id of each message is needed to
+     *               determine the UID. If UIDPLUS is not available, and this
+     *               option is not defined, append() will return true only.
+     *               DEFAULT: If UIDPLUS is supported, or this string is
+     *               provided, appended ID is returned. Else, append() will
+     *               return true.
+     * </pre>
+     * @param array $options  Additonal options:
+     * <pre>
+     * 'create' - (boolean) Try to create $mailbox if it does not exist?
+     *             DEFAULT: No.
+     * </pre>
+     *
+     * @return mixed  An array of the UIDs of the appended messages (if server
+     *                supports UIDPLUS extension or 'messageid' is defined)
+     *                or true.
+     */
+    public function append($mailbox, $data, $options = array())
+    {
+        $mailbox = Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($mailbox);
+
+        $ret = $this->_append($mailbox, $data, $options);
+        if (is_array($ret)) {
+            return $ret;
+        }
+
+        $msgid = false;
+        $uids = array();
+
+        while (list(,$val) = each($data)) {
+            if (empty($val['messageid'])) {
+                $uids[] = null;
+            } else {
+                $msgid = true;
+                $search_query = new Horde_Imap_Client_Search_Query();
+                $search_query->headerText('Message-ID', $val['messageid']);
+                $uidsearch = $this->search($mailbox, $search_query);
+                $uids[] = reset($uidsearch['match']);
+            }
+        }
+
+        return $msgid ? $uids : true;
+    }
+
+    /**
+     * Append message(s) to a mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox  The mailbox to append the message(s) to
+     *                         (UTF7-IMAP).
+     * @param array $data      The message data.
+     * @param array $options   Additional options.
+     *
+     * @return mixed  An array of the UIDs of the appended messages (if server
+     *                supports UIDPLUS extension) or true.
+     */
+    abstract protected function _append($mailbox, $data, $options);
+
+    /**
+     * Request a checkpoint of the currently selected mailbox (RFC 3501
+     * [6.4.1]).
+     * Throws a Horde_Imap_Client_Exception on error.
+     */
+    public function check()
+    {
+        // CHECK only useful if we are already authenticated.
+        if ($this->_isAuthenticated) {
+            $this->_check();
+        }
+    }
+
+    /**
+     * Request a checkpoint of the currently selected mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     */
+    abstract protected function _check();
+
+    /**
+     * Close the connection to the currently selected mailbox, optionally
+     * expunging all deleted messages (RFC 3501 [6.4.2]).
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param array $options  Additional options:
+     * <pre>
+     * 'expunge' - (boolean) Expunge all messages flagged as deleted?
+     *             DEFAULT: No
+     * </pre>
+     */
+    public function close($options = array())
+    {
+        if (is_null($this->_selected)) {
+            return;
+        }
+
+        /* If we are caching, search for deleted messages. */
+        if (!empty($options['expunge']) && $this->_initCacheOb()) {
+            $search_query = new Horde_Imap_Client_Search_Query();
+            $search_query->flag('\\deleted', true);
+            $search_res = $this->search($this->_selected, $search_query);
+        } else {
+            $search_res = null;
+        }
+
+        $this->_close($options);
+        $this->_selected = null;
+        $this->_mode = 0;
+
+        if (!is_null($search_res)) {
+            $this->_cacheOb->deleteMsgs($this->_selected, $search_res['match']);
+        }
+    }
+
+    /**
+     * Close the connection to the currently selected mailbox, optionally
+     * expunging all deleted messages (RFC 3501 [6.4.2]).
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param array $options  Additional options.
+     */
+    abstract protected function _close($options);
+
+    /**
+     * Expunge deleted messages from the given mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox  The mailbox to expunge. Either in UTF7-IMAP
+     *                         or UTF-8.
+     * @param array $options   Additional options:
+     * <pre>
+     * 'ids' - (array) A list of messages to expunge, but only if they
+     *         are also flagged as deleted. By default, this array is
+     *         assumed to contain UIDs (see 'sequence').
+     *         DEFAULT: All messages marked as deleted will be expunged.
+     * 'sequence' - (boolean) If true, 'ids' is an array of sequence numbers.
+     *              DEFAULT: 'sequence' is an array of UIDs.
+     * </pre>
+     */
+    public function expunge($mailbox, $options = array())
+    {
+        $this->openMailbox($mailbox, self::OPEN_READWRITE);
+        $this->_expunge($options);
+    }
+
+    /**
+     * Expunge all deleted messages from the given mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param array $options  Additional options.
+     */
+    abstract protected function _expunge($options);
+
+    /**
+     * Search a mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox  The mailbox to search. Either in UTF7-IMAP
+     *                         or UTF-8.
+     * @param object $query    The search query (a
+     *                         Horde_Imap_Client_Search_Query object).
+     *                         Defaults to an ALL search.
+     * @param array $options   Additional options:
+     * <pre>
+     * 'results' - (array) The data to return. Consists of zero or more of the
+     *                     following flags:
+     * <pre>
+     * Horde_Imap_Client::SORT_RESULTS_COUNT
+     * Horde_Imap_Client::SORT_RESULTS_MATCH (DEFAULT)
+     * Horde_Imap_Client::SORT_RESULTS_MAX
+     * Horde_Imap_Client::SORT_RESULTS_MIN
+     * Horde_Imap_Client::SORT_RESULTS_SAVE - (This option is currently meant
+     *   for internal use only)
+     * </pre>
+     * 'reverse' - (boolean) Sort the entire returned list of messages in
+     *             reverse (i.e. descending) order.
+     *             DEFAULT: Sorted in ascending order.
+     * 'sequence' - (boolean) If true, returns an array of sequence numbers.
+     *              DEFAULT: Returns an array of UIDs
+     * 'sort' - (array) Sort the returned list of messages. Multiple sort
+     *          criteria can be specified. The following sort criteria
+     *          are available:
+     * <pre>
+     * Horde_Imap_Client::SORT_ARRIVAL
+     * Horde_Imap_Client::SORT_CC
+     * Horde_Imap_Client::SORT_DATE
+     * Horde_Imap_Client::SORT_FROM
+     * Horde_Imap_Client::SORT_SIZE
+     * Horde_Imap_Client::SORT_SUBJECT
+     * Horde_Imap_Client::SORT_TO.
+     * </pre>
+     *          Additionally, any sort criteria can be sorted in reverse order
+     *          (instead of the default ascending order) by adding a
+     *          Horde_Imap_Client::SORT_REVERSE element to the array directly
+     *          before adding the sort element. Note that if you want the
+     *          entire list to be sorted in reverse order, use the 'reverse'
+     *          option instead. If this option is set, the 'results' option
+     *          is ignored.
+     *          DEFAULT: Arrival sort (Horde_Imap_Client::SORT_ARRIVAL)
+     * </pre>
+     *
+     * @return array  An array with the following keys:
+     * <pre>
+     * 'count' - (integer) The number of messages that match the search
+     *           criteria.
+     *           Always returned.
+     * 'match' - OPTIONAL (array) The UIDs (default) or message sequence
+     *           numbers (if 'sequence' is true) that match $criteria.
+     *           Returned if 'sort' is false and
+     *           Horde_Imap_Client::SORT_RESULTS_MATCH is set.
+     * 'max' - (integer) The UID (default) or message sequence number (if
+     *         'sequence is true) of the highest message that satisifies
+     *         $criteria. Returns null if no matches found.
+     *         Returned if Horde_Imap_Client::SORT_RESULTS_MAX is set.
+     * 'min' - (integer) The UID (default) or message sequence number (if
+     *         'sequence is true) of the lowest message that satisifies
+     *         $criteria. Returns null if no matches found.
+     *         Returned if Horde_Imap_Client::SORT_RESULTS_MIN is set.
+     * 'modseq' - (integer) The highest mod-sequence for all messages being
+     *            returned.
+     *            Returned if 'sort' is false, the search query includes a
+     *            modseq command, and the server supports the CONDSTORE IMAP
+     *            extension.
+     * 'save' - (boolean) Whether the search results were saved. This value is
+     *          meant for internal use only. Returned if 'sort' is false and
+     *          Horde_Imap_Client::SORT_RESULTS_SAVE is set.
+     * 'sort' - (array) The sorted UIDs (default) or message sequence numbers
+     *          (if 'sequence' is true) that match $criteria.
+     *          Returned if 'sort' is true.
+     * </pre>
+     */
+    public function search($mailbox, $query = null, $options = array())
+    {
+        $this->openMailbox($mailbox, self::OPEN_AUTO);
+
+        if (empty($options['results'])) {
+            $options['results'] = array(
+                self::SORT_RESULTS_MATCH,
+                self::SORT_RESULTS_COUNT
+            );
+        }
+
+        // Default to an ALL search.
+        if (is_null($query)) {
+            $query = new Horde_Imap_Client_Search_Query();
+        }
+
+        $options['_query'] = $query->build();
+
+        /* Optimization - if query is just for a count of either RECENT or
+         * ALL messages, we can send status information instead. Can't
+         * optimize with unseen queries because we may cause an infinite loop
+         * between here and the status() call. */
+        if ((count($options['results']) == 1) &&
+            (reset($options['results']) == self::SORT_RESULTS_COUNT)) {
+            switch ($options['_query']['query']) {
+            case 'ALL':
+                $ret = $this->status($this->_selected, self::STATUS_MESSAGES);
+                return array('count' => $ret['messages']);
+
+            case 'RECENT':
+                $ret = $this->status($this->_selected, self::STATUS_RECENT);
+                return array('count' => $ret['recent']);
+            }
+        }
+
+        $ret = $this->_search($query, $options);
+
+        if (!empty($options['reverse'])) {
+            if (empty($options['sort'])) {
+                $ret['match'] = array_reverse($ret['match']);
+            } else {
+                $ret['sort'] = array_reverse($ret['sort']);
+            }
+        }
+
+        return $ret;
+    }
+
+    /**
+     * Search a mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param object $query   The search query.
+     * @param array $options  Additional options. The '_query' key contains
+     *                        the value of $query->build().
+     *
+     * @return array  An array of UIDs (default) or an array of message
+     *                sequence numbers (if 'sequence' is true).
+     */
+    abstract protected function _search($query, $options);
+
+    /**
+     * Set the comparator to use for searching/sorting (RFC 5255).
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $comparator  The comparator string (see RFC 4790 [3.1] -
+     *                            "collation-id" - for format). The reserved
+     *                            string 'default' can be used to select
+     *                            the default comparator.
+     */
+    public function setComparator($comparator = null)
+    {
+        $comp = is_null($comparator) ? (empty($this->_params['comparator']) ? null : $this->_params['comparator']) : $comparator;
+        if (is_null($comp)) {
+            return;
+        }
+
+        $i18n = $this->queryCapability('I18NLEVEL');
+        if (empty($i18n) || (max($i18n) < 2)) {
+            throw new Horde_Imap_Client_Exception('The IMAP server does not support changing SEARCH/SORT comparators.', Horde_Imap_Client_Exception::NOSUPPORTIMAPEXT);
+        }
+
+        $this->_setComparator($comp);
+    }
+
+    /**
+     * Set the comparator to use for searching/sorting (RFC 5255).
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $comparator  The comparator string (see RFC 4790 [3.1] -
+     *                            "collation-id" - for format). The reserved
+     *                            string 'default' can be used to select
+     *                            the default comparator.
+     */
+    abstract protected function _setComparator($comparator);
+
+    /**
+     * Get the comparator used for searching/sorting (RFC 5255).
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @return mixed  Null if the default comparator is being used, or an
+     *                array of comparator information (see RFC 5255 [4.8]).
+     */
+    public function getComparator()
+    {
+        $i18n = $this->queryCapability('I18NLEVEL');
+        if (empty($i18n) || (max($i18n) < 2)) {
+            return null;
+        }
+
+        return $this->_getComparator();
+    }
+
+    /**
+     * Get the comparator used for searching/sorting (RFC 5255).
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @return mixed  Null if the default comparator is being used, or an
+     *                array of comparator information (see RFC 5255 [4.8]).
+     */
+    abstract protected function _getComparator();
+
+    /**
+     * Thread sort a given list of messages (RFC 5256).
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox  The mailbox to search. Either in UTF7-IMAP
+     *                         or UTF-8.
+     * @param array $options   Additional options:
+     * <pre>
+     * 'criteria' - (mixed) The following thread criteria are available:
+     *              Horde_Imap_Client::THREAD_ORDEREDSUBJECT, and
+     *              Horde_Imap_Client::THREAD_REFERENCES. Additionally, other
+     *              algorithms can be explicitly specified by passing the IMAP
+     *              thread algorithm in as a string.
+     * 'search' - (object) The search query (a
+     *            Horde_Imap_Client_Search_Query object).
+     *            DEFAULT: All messages in mailbox included in thread sort.
+     * 'sequence' - (boolean) If true, each message is stored and referred to
+     *              by its message sequence number.
+     *              DEFAULT: Stored/referred to by UID.
+     * </pre>
+     *
+     * @return Horde_Imap_Client_Thread  A Horde_Imap_Client_Thread object.
+     */
+    public function thread($mailbox, $options = array())
+    {
+        $this->openMailbox($mailbox, self::OPEN_AUTO);
+
+        $ret = $this->_thread($options);
+        return new Horde_Imap_Client_Thread($ret, empty($options['sequence']) ? 'uid' : 'sequence');
+    }
+
+    /**
+     * Thread sort a given list of messages (RFC 5256).
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param array $options  Additional options.
+     *
+     * @return array  An array with the following values, one per message,
+     *                with the key being either the UID (default) or the
+     *                message sequence number (if 'sequence' is true). Values
+     *                of each entry:
+     * <pre>
+     * 'base' - (integer) The UID of the base message. Is null if this is the
+     *          only message in the thread.
+     * 'last' - (boolean) Is this the last message in a subthread?
+     * 'level' - (integer) The thread level of this message (1 = base).
+     * 'uid' - (integer) The UID of the message.
+     * </pre>
+     */
+    abstract protected function _thread($options);
+
+    /**
+     * Fetch message data (see RFC 3501 [6.4.5]).
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox  The mailbox to fetch messages from. Either in
+     *                         UTF7-IMAP or UTF-8.
+     * @param array $criteria  The fetch criteria. Contains the following:
+     * <pre>
+     * Key: Horde_Imap_Client::FETCH_FULLMSG
+     *   Desc: Returns the full text of the message.
+     *         ONLY ONE of these entries should be defined.
+     *   Value: (array) The following options are available:
+     *     'length' - (integer) If 'start' is defined, the length of the
+     *                substring to return.
+     *                DEFAULT: The entire text is returned.
+     *     'peek' - (boolean) If set, does not set the '\Seen' flag on the
+     *              message.
+     *              DEFAULT: The seen flag is set.
+     *     'start' - (integer) If a portion of the full text is desired to be
+     *               returned, the starting position is identified here.
+     *               DEFAULT: The entire text is returned.
+     *   Return key: 'fullmsg'
+     *   Return format: (string) The full text of the entire message (or the
+     *                  portion of the text delineated by the 'start'/'length'
+     *                  parameters).
+     *
+     * Key: Horde_Imap_Client::FETCH_HEADERTEXT
+     *   Desc: Returns the header text. Header text is defined only for the
+     *         base RFC 2822 message or message/rfc822 parts. Attempting to
+     *         retireve the body text from other parts will result in a
+     *         thrown exception.
+     *         MORE THAN ONE of these entries can be defined. Each entry will
+     *         be a separate array contained in the value field.
+     *         Each entry should have a unique 'id' value.
+     *   Value: (array) One array for each request. Each array may contain
+     *          the following options:
+     *     'id' - (string) The MIME ID to obtain the header text for.
+     *            DEFAULT: The header text for the entire message (MIME ID: 0)
+     *            will be returned.
+     *     'length' - (integer) If 'start' is defined, the length of the
+     *                substring to return.
+     *                DEFAULT: The entire text is returned.
+     *     'parse' - (boolean) If true, and the Horde MIME library is
+     *               available, parse the header text into a MIME_Headers
+     *               object.
+     *               DEFAULT: The full header text is returned.
+     *     'peek' - (boolean) If set, does not set the '\Seen' flag on the
+     *              message.
+     *              DEFAULT: The seen flag is set.
+     *     'start' - (integer) If a portion of the full text is desired to be
+     *               returned, the starting position is identified here.
+     *               DEFAULT: The entire text is returned.
+     *   Return key: 'headertext'
+     *   Return format: (mixed) If 'parse' is true, a MIME_Headers object.
+     *                  Else, an array of header text entries. Keys are the
+     *                  the 'id', values are the message header text strings
+     *                  (or the portion of the text delineated by the
+     *                  'start'/'length' parameters).
+     *
+     * Key: Horde_Imap_Client::FETCH_BODYTEXT
+     *   Desc: Returns the body text. Body text is defined only for the
+     *         base RFC 2822 message or message/rfc822 parts. Attempting to
+     *         retireve the body text from other parts will result in a
+     *         thrown exception.
+     *         MORE THAN ONE of these entries can be defined. Each entry will
+     *         be a separate array contained in the value field.
+     *         Each entry should have a unique 'id' value.
+     *   Value: (array) One array for each request. Each array may contain
+     *          the following options:
+     *     'id' - (string) The MIME ID to obtain the body text for.
+     *            DEFAULT: The body text for the entire message (MIME ID: 0)
+     *            will be returned.
+     *     'length' - (integer) If 'start' is defined, the length of the
+     *                substring to return.
+     *                DEFAULT: The entire text is returned.
+     *     'peek' - (boolean) If set, does not set the '\Seen' flag on the
+     *              message.
+     *              DEFAULT: The seen flag is set.
+     *     'start' - (integer) If a portion of the full text is desired to be
+     *               returned, the starting position is identified here.
+     *               DEFAULT: The entire text is returned.
+     *   Return key: 'bodytext'
+     *   Return format: (array) An array of body text entries. Keys are the
+     *                  the 'id', values are the message body text strings
+     *                  (or the portion of the text delineated by the
+     *                  'start'/'length' parameters).
+     *
+     * Key: Horde_Imap_Client::FETCH_MIMEHEADER
+     *   Desc: Returns the MIME header text. MIME header text is defined only
+     *         for non RFC 2822 messages and non message/rfc822 parts.
+     *         Attempting to retrieve the MIME header from other parts will
+     *         result in a thrown exception.
+     *         MORE THAN ONE of these entries can be defined. Each entry will
+     *         be a separate array contained in the value field.
+     *         Each entry should have a unique 'id' value.
+     *   Value: (array) One array for each request. Each array may contain
+     *          the following options:
+     *     'id' - (string) The MIME ID to obtain the MIME header text for.
+     *            DEFAULT: NONE
+     *     'length' - (integer) If 'start' is defined, the length of the
+     *                substring to return.
+     *                DEFAULT: The entire text is returned.
+     *     'peek' - (boolean) If set, does not set the '\Seen' flag on the
+     *              message.
+     *              DEFAULT: The seen flag is set.
+     *     'start' - (integer) If a portion of the full text is desired to be
+     *               returned, the starting position is identified here.
+     *               DEFAULT: The entire text is returned.
+     *   Return key: 'mimeheader'
+     *   Return format: (array) An array of MIME header text entries. Keys are
+     *                  the 'id', values are the MIME header text strings
+     *                  (or the portion of the text delineated by the
+     *                  'start'/'length' parameters).
+     *
+     * Key: Horde_Imap_Client::FETCH_BODYPART
+     *   Desc: Returns the body part data for a given MIME ID.
+     *         MORE THAN ONE of these entries can be defined. Each entry will
+     *         be a separate array contained in the value field.
+     *         Each entry should have a unique 'id' value.
+     *   Value: (array) One array for each request. Each array may contain
+     *          the following options:
+     *     'decode' - (boolean) Attempt to server-side decode the bodypart
+     *                data if it is MIME transfer encoded. If it can be done,
+     *                the 'bodypartdecode' key will be set with one of two
+     *                values: '8bit' or 'binary'.
+     *                DEFAULT: The raw data.
+     *     'id' - (string) The MIME ID to obtain the body part text for.
+     *            DEFAULT: NONE
+     *     'length' - (integer) If 'start' is defined, the length of the
+     *                substring to return.
+     *                DEFAULT: The entire data is returned.
+     *     'peek' - (boolean) If set, does not set the '\Seen' flag on the
+     *              message.
+     *              DEFAULT: The seen flag is set.
+     *     'start' - (integer) If a portion of the full data is desired to be
+     *               returned, the starting position is identified here.
+     *               DEFAULT: The entire data is returned.
+     *   Return key: 'bodypart' (and possibly 'bodypartdecode')
+     *   Return format: (array) An array of body part data entries. Keys are
+     *                  the 'id', values are the body part data (or the
+     *                  portion of the data delineated by the 'start'/'length'
+     *                  parameters).
+     *
+     * Key: Horde_Imap_Client::FETCH_BODYPARTSIZE
+     *   Desc: Returns the decoded body part size for a given MIME ID.
+     *         MORE THAN ONE of these entries can be defined. Each entry will
+     *         be a separate array contained in the value field.
+     *         Each entry should have a unique 'id' value.
+     *   Value: (array) One array for each request. Each array may contain
+     *          the following options:
+     *     'id' - (string) The MIME ID to obtain the body part size for.
+     *            DEFAULT: NONE
+     *   Return key: 'bodypartsize' (if supported by server)
+     *   Return format: (integer) The body part size in bytes. If the server
+     *                  does not support the functionality, 'bodypartsize'
+     *                  will not be set.
+     *
+     * Key: Horde_Imap_Client::FETCH_HEADERS
+     *   Desc: Returns RFC 2822 header text that matches a search string.
+     *         This header search work only with the base RFC 2822 message or
+     *         message/rfc822 parts.
+     *         MORE THAN ONE of these entries can be defined. Each entry will
+     *         be a separate array contained in the value field.
+     *         Each entry should have a unique 'label' value.
+     *   Value: (array) One array for each request. Each array may contain
+     *          the following options:
+     *     'headers' - (array) The headers to search for (case-insensitive).
+     *                 DEFAULT: NONE (MANDATORY)
+     *     'id' - (string) The MIME ID to search.
+     *            DEFAULT: The base message part (MIME ID: 0)
+     *     'label' - (string) A unique label associated with this particular
+     *               search. This is how the results are stored.
+     *               DEFAULT: NONE (MANDATORY entry or exception will be
+     *               thrown)
+     *     'length' - (integer) If 'start' is defined, the length of the
+     *                substring to return.
+     *                DEFAULT: The entire text is returned.
+     *     'notsearch' - (boolean) Do a 'NOT' search on the headers.
+     *                   DEFAULT: false
+     *     'parse' - (boolean) If true, and the Horde MIME library is
+     *               available, parse the returned headers into a
+     *               MIME_Headers object.
+     *               DEFAULT: The full header text is returned.
+     *     'peek' - (boolean) If set, does not set the '\Seen' flag on the
+     *              message.
+     *              DEFAULT: The seen flag is set.
+     *     'start' - (integer) If a portion of the full text is desired to be
+     *               returned, the starting position is identified here.
+     *               DEFAULT: The entire text is returned.
+     *   Return key: 'headers'
+     *   Return format: (mixed) If parse is true, a MIME_Headers object.
+     *                  Else, an array of header search entries. Keys are
+     *                  the 'label'. If 'parse' is false, values are the
+     *                  matched header text. If 'parse' is true,
+     *                  values are an array with the header names as keys
+     *                  (case-insensitive) and the header values as the
+     *                  values. Both returns are subject to the search result
+     *                  being truncated due to the 'start'/'length'
+     *                  parameters.
+     *
+     * Key: Horde_Imap_Client::FETCH_STRUCTURE
+     *   Desc: Returns MIME structure information
+     *         ONLY ONE of these entries should be defined per fetch request.
+     *   Value: (array) The following options are available:
+     *     'noext' - (boolean) Don't return information on extensions
+     *               DEFAULT: Will return information on extensions
+     *     'parse' - (boolean) If true, and the Horde MIME library is
+     *               available, parse the returned structure into a
+     *               MIME_Message object.
+     *               DEFAULT: The array representation is returned.
+     *   Return key: 'structure' [CACHEABLE]
+     *   Return format: (mixed) If 'parse' is true, a MIME_Structure object.
+     *                          Else, an array with the following information:
+     *
+     *     'type' - (string) The MIME type
+     *     'subtype' - (string) The MIME subtype
+     *
+     *     The returned array MAY contain the following information:
+     *     'disposition' - (string) The disposition type of the part (e.g.
+     *                     'attachment', 'inline').
+     *     'dparameters' - (array) Attribute/value pairs from the part's
+     *                     Content-Disposition header.
+     *     'language' - (array) A list of body language values.
+     *     'location' - (string) The body content URI.
+     *
+     *     Depending on the MIME type of the part, the array will also contain
+     *     further information. If labeled as [OPTIONAL], the array MAY
+     *     contain this information, but only if 'noext' is false and the
+     *     server returned the requested information. Else, the value is not
+     *     set.
+     *
+     *     multipart/* parts:
+     *     ==================
+     *     'parts' - (array) An array of subparts (follows the same format as
+     *               the base structure array).
+     *     'parameters' - [OPTIONAL] (array) Attribute/value pairs from the
+     *                    part's Content-Type header.
+     *
+     *     All other parts:
+     *     ================
+     *     'parameters' - (array) Attribute/value pairs from the part's
+     *                    Content-Type header.
+     *     'id' - (string) The part's Content-ID value.
+     *     'description' - (string) The part's Content-Description value.
+     *     'encoding' - (string) The part's Content-Transfer-Encoding value.
+     *     'size' - (integer) - The part's size in bytes.
+     *     'envelope' - [ONLY message/rfc822] (array) See 'envelope' response.
+     *     'structure' - [ONLY message/rfc822] (array) See 'structure'
+     *                   response.
+     *     'lines' - [ONLY message/rfc822 and text/*] (integer) The size of
+     *               the body in text lines.
+     *     'md5' - [OPTIONAL] (string) The part's MD5 value.
+     *
+     * Key: Horde_Imap_Client::FETCH_ENVELOPE
+     *   Desc: Envelope header data
+     *         ONLY ONE of these entries should be defined per fetch request.
+     *   Value: NONE
+     *   Return key: 'envelope' [CACHEABLE]
+     *   Return format: (array) This array has 9 elements: 'date', 'subject',
+     *     'from', 'sender', 'reply-to', 'to', 'cc', 'bcc', 'in-reply-to', and
+     *     'message-id'. For 'date', 'subject', 'in-reply-to', and
+     *     'message-id', the values will be a string or null if it doesn't
+     *     exist. For the other keys, the value will be an array of arrays (or
+     *     an empty array if the header does not exist). Each of these
+     *     underlying arrays corresponds to a single address and contains 4
+     *     keys: 'personal', 'adl', 'mailbox', and 'host'. These keys will
+     *     only be set if the server returned information.
+     *
+     * Key: Horde_Imap_Client::FETCH_FLAGS
+     *   Desc: Flags set for the message
+     *         ONLY ONE of these entries should be defined per fetch request.
+     *   Value: NONE
+     *   Return key: 'flags' [CACHEABLE - if CONSTORE IMAP extension is
+     *                        supported on the server]
+     *   Return format: (array) Each flag will be in a separate array entry.
+     *     The flags will be entirely in lowercase.
+     *
+     * Key: Horde_Imap_Client::FETCH_DATE
+     *   Desc: The internal (IMAP) date of the message
+     *         ONLY ONE of these entries should be defined per fetch request.
+     *   Value: NONE
+     *   Return key: 'date' [CACHEABLE]
+     *   Return format: (DateTime object) Returns a PHP DateTime object.
+     *
+     * Key: Horde_Imap_Client::FETCH_SIZE
+     *   Desc: The size (in bytes) of the message
+     *         ONLY ONE of these entries should be defined per fetch request.
+     *   Value: NONE
+     *   Return key: 'size' [CACHEABLE]
+     *   Return format: (integer) The size of the message.
+     *
+     * Key: Horde_Imap_Client::FETCH_UID
+     *   Desc: The Unique ID of the message.
+     *         ONLY ONE of these entries should be defined per fetch request.
+     *   Value: NONE
+     *   Returned key: 'uid'
+     *   Return format: (integer) The unique ID of the message.
+     *
+     * Key: Horde_Imap_Client::FETCH_SEQ
+     *   Desc: The sequence number of the message.
+     *         ONLY ONE of these entries should be defined per fetch request.
+     *   Value: NONE
+     *   Return key: 'seq'
+     *   Return format: (integer) The sequence number of the message.
+     *
+     * Key: Horde_Imap_Client::FETCH_MODSEQ
+     *   Desc: The mod-sequence value for the message.
+     *         The server must support the CONDSTORE IMAP extension to use
+     *         this criteria. Additionally, the mailbox must support mod-
+     *         sequences or an exception will be thrown.
+     *         ONLY ONE of these entries should be defined per fetch request.
+     *   Value: NONE
+     *   Returned key: 'modseq'
+     *   Return format: (integer) The mod-sequence value of the message, or
+     *                  undefined if the server does not support CONDSTORE.
+     * </pre>
+     * @param array $options    Additional options:
+     * <pre>
+     * 'changedsince' - (integer) Only return messages that have a
+     *                  mod-sequence larger than this value. This option
+     *                  requires the CONDSTORE IMAP extension (if not present,
+     *                  this value is ignored). Additionally, the mailbox
+     *                  must support mod-sequences or an exception will be
+     *                  thrown. If valid, this option implicity adds the
+     *                  Horde_Imap_Client::FETCH_MODSEQ fetch criteria to
+     *                  the fetch command.
+     *                  DEFAULT: Mod-sequence values are ignored.
+     * 'ids' - (array) A list of messages to fetch data from.
+     *         DEFAULT: All messages in $mailbox will be fetched.
+     * 'sequence' - (boolean) If true, 'ids' is an array of sequence numbers.
+     *              DEFAULT: 'ids' is an array of UIDs.
+     * 'vanished' - (boolean) Only return messages from the UID set parameter
+     *              that have been expunged and whose associated mod-sequence
+     *              is larger than the specified mod-sequence. This option
+     *              requires the QRESYNC IMAP extension, requires
+     *              'changedsince' to be set, and requires 'sequence' to
+     *              be false.
+     *              DEFAULT: Vanished search ignored.
+     * </pre>
+     *
+     * @return array  An array of fetch results. The array consists of
+     *                keys that correspond to 'ids', and values that
+     *                contain the array of fetched information as requested
+     *                in criteria.
+     */
+    public function fetch($mailbox, $criteria, $options = array())
+    {
+        $cache_array = $get_fields = $new_criteria = $ret = array();
+        $cf = $this->_initCacheOb() ? $this->_params['cache']['fields'] : array();
+        $qresync = isset($this->_init['enabled']['QRESYNC']);
+        $seq = !empty($options['sequence']);
+
+        /* The 'vanished' modifier requires QRESYNC, 'changedsince', and
+         * !'sequence'. */
+        if (!empty($options['vanished']) &&
+            (!$qresync ||
+             $seq ||
+             empty($options['changedsince']))) {
+            throw new Horde_Imap_Client_Exception('The vanished FETCH modifier is missing a pre-requisite.');
+        }
+
+        /* The 'changedsince' modifier implicitly adds the MODSEQ FETCH item.
+         * (RFC 4551 [3.3.1]). A UID SEARCH will always return UID
+         * information (RFC 3501 [6.4.8]). Don't add to criteria because it
+         * simply creates a longer FETCH command. */
+
+        /* If using cache, we store by UID so we need to return UIDs. */
+        if ($seq && !empty($cf)) {
+            $criteria[self::FETCH_UID] = true;
+        }
+
+        $this->openMailbox($mailbox, self::OPEN_AUTO);
+
+        /* We need the UIDVALIDITY for the current mailbox. */
+        $status_res = $this->status($this->_selected, self::STATUS_HIGHESTMODSEQ | self::STATUS_UIDVALIDITY);
+
+        /* Determine if caching is available and if anything in $criteria is
+         * cacheable. Do some sanity checking on criteria also. */
+        foreach ($criteria as $k => $v) {
+            $cache_field = null;
+
+            switch ($k) {
+            case self::FETCH_STRUCTURE:
+                /* Don't cache if 'noext' is present. It will probably be a
+                 * rare event anyway. */
+                if (empty($v['noext']) && isset($cf[$k])) {
+                    /* Structure can be cached two ways - via MIME_Message or
+                     * by internal array format. */
+                    $cache_field = empty($v['parse']) ? 'HICstructa' : 'HICstructm';
+                    $fetch_field = 'structure';
+                }
+                break;
+
+            case self::FETCH_BODYPARTSIZE:
+                if (!$this->queryCapability('BINARY')) {
+                    unset($criteria[$k]);
+                }
+                break;
+
+            case self::FETCH_ENVELOPE:
+                if (isset($cf[$k])) {
+                    $cache_field = 'HICenv';
+                    $fetch_field = 'envelope';
+                }
+                break;
+
+            case self::FETCH_FLAGS:
+                if (isset($cf[$k])) {
+                    /* QRESYNC would have already done syncing on mailbox
+                     * open, so no need to do again. */
+                    if (!$qresync) {
+                        /* Grab all flags updated since the cached modseq
+                         * val. */
+                        $metadata = $this->_cacheOb->getMetaData($this->_selected, array('HICmodseq'));
+                        if (isset($metadata['HICmodseq']) &&
+                            ($metadata['HICmodseq'] != $status_res['highestmodseq'])) {
+                            $uids = $this->_cacheOb->get($this->_selected, array(), array(), $status_res['uidvalidity']);
+                            if (!empty($uids)) {
+                                $this->_fetch(array(self::FETCH_FLAGS => true), array('changedsince' => $metadata['HICmodseq'], 'ids' => $uids));
+                            }
+                            $this->_cacheOb->setMetaData($mailbox, array('HICmodseq' => $status_res['highestmodseq']));
+                        }
+                    }
+
+                    $cache_field = 'HICflags';
+                    $fetch_field = 'flags';
+                }
+                break;
+
+            case self::FETCH_DATE:
+                if (isset($cf[$k])) {
+                    $cache_field = 'HICdate';
+                    $fetch_field = 'date';
+                }
+                break;
+
+            case self::FETCH_SIZE:
+                if (isset($cf[$k])) {
+                    $cache_field = 'HICsize';
+                    $fetch_field = 'size';
+                }
+                break;
+
+            case self::FETCH_MODSEQ:
+                if (!isset($this->_init['enabled']['CONDSTORE'])) {
+                    unset($criteria[$k]);
+                }
+                break;
+            }
+
+            if (!is_null($cache_field)) {
+                $cache_array[$k] = array(
+                    'c' => $cache_field,
+                    'f' => $fetch_field
+                );
+                $get_fields[] = $cache_field;
+            }
+        }
+
+        /* If nothing is cacheable, we can do a straight search. */
+        if (empty($cache_array)) {
+            return $this->_fetch($criteria, $options);
+        }
+
+        /* If given sequence numbers, we need to switch to UIDs for caching
+         * purposes. Also, we need UID #'s now if searching the entire
+         * mailbox. */
+        if ($seq || empty($options['ids'])) {
+            $res_seq = $this->_getSeqUIDLookup(empty($options['ids']) ? null : $options['ids'], $seq);
+            $uids = $res_seq['uids'];
+        } else {
+            $uids = $options['ids'];
+        }
+
+        /* Get the cached values. */
+        try {
+            $data = $this->_cacheOb->get($this->_selected, $uids, $get_fields, $status_res['uidvalidity']);
+        } catch (Horde_Imap_Client_Exception $e) {
+            if ($e->getCode() != Horde_Imap_Client_Exception::CACHEUIDINVALID) {
+                throw $e;
+            }
+            $data = array();
+        }
+
+        // Build a list of what we still need.
+        foreach ($uids as $val) {
+            $crit = $criteria;
+            $id = $seq ? $res_seq['lookup'][$val] : $val;
+            $ret[$id] = array('uid' => $id);
+
+            foreach ($cache_array as $key => $cval) {
+                // Retrieved from cache so store in return array
+                if (isset($data[$val][$cval['c']])) {
+                    $ret[$id][$cval['f']] = $data[$val][$cval['c']];
+                    unset($crit[$key]);
+                }
+            }
+
+            if (!$seq) {
+                unset($crit[self::FETCH_UID]);
+            }
+
+            if (!empty($crit)) {
+                $sig = md5(serialize(array_values($crit)));
+                if (isset($new_criteria[$sig])) {
+                    $new_criteria[$sig]['i'][] = $id;
+                } else {
+                    $new_criteria[$sig] = array('c' => $crit, 'i' => array($id));
+                }
+            }
+        }
+
+        if (!empty($new_criteria)) {
+            $opts = $options;
+            foreach ($new_criteria as $val) {
+                $opts['ids'] = $val['i'];
+                $fetch_res = $this->_fetch($val['c'], $opts);
+                reset($fetch_res);
+                while (list($k, $v) = each($fetch_res)) {
+                    reset($v);
+                    while (list($k2, $v2) = each($v)) {
+                        $ret[$k][$k2] = $v2;
+                    }
+                }
+            }
+        }
+
+        return $ret;
+    }
+
+    /**
+     * Fetch message data.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param array $criteria  The fetch criteria.
+     * @param array $options   Additional options.
+     *
+     * @return array  See self::fetch().
+     */
+    abstract protected function _fetch($criteria, $options);
+
+    /**
+     * Store message flag data (see RFC 3501 [6.4.6]).
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox  The mailbox containing the messages to modify.
+     *                         Either in UTF7-IMAP or UTF-8.
+     * @param array $options   Additional options:
+     * <pre>
+     * 'add' - (array) An array of flags to add.
+     *         DEFAULT: No flags added.
+     * 'ids' - (array) The list of messages to modify.
+     *         DEFAULT: All messages in $mailbox will be modified.
+     * 'remove' - (array) An array of flags to remove.
+     *            DEFAULT: No flags removed.
+     * 'replace' - (array) Replace the current flags with this set
+     *             of flags. Overrides both the 'add' and 'remove' options.
+     *             DEFAULT: No replace is performed.
+     * 'sequence' - (boolean) If true, 'ids' is an array of sequence numbers.
+     *              DEFAULT: 'ids' is an array of UIDs.
+     * 'unchangedsince' - (integer) Only changes flags if the mod-sequence ID
+     *                    of the message is equal or less than this value.
+     *                    Requires the CONDSTORE IMAP extension on the server.
+     *                    Also requires the mailbox to support mod-sequences.
+     *                    Will throw an exception if either condition is not
+     *                    met.
+     *                    DEFAULT: mod-sequence is ignored when applying
+     *                             changes
+     * </pre>
+     *
+     * @return array  If 'unchangedsince' is set, this is a list of UIDs or
+     *                sequence numbers (if 'sequence' is true) that failed
+     *                the 'unchangedsince' test.  Else, an empty array.
+     */
+    public function store($mailbox, $options = array())
+    {
+        $this->openMailbox($mailbox, self::OPEN_READWRITE);
+
+        if (!empty($options['unchangedsince']) &&
+            !isset($this->_init['enabled']['CONDSTORE'])) {
+            throw new Horde_Imap_Client_Exception('Server does not support the CONDSTORE extension.', Horde_Imap_Client_Exception::NOSUPPORTIMAPEXT);
+        }
+
+        return $this->_store($options);
+    }
+
+    /**
+     * Store message flag data.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param array $options  Additional options.
+     *
+     * @return array  See Horde_Imap_Client::store().
+     */
+    abstract protected function _store($options);
+
+    /**
+     * Copy messages to another mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $source   The source mailbox. Either in UTF7-IMAP
+     *                         or UTF-8.
+     * @param string $dest     The destination mailbox. Either in UTF7-IMAP
+     *                         or UTF-8.
+     * @param array $options   Additional options:
+     * <pre>
+     * 'create' - (boolean) Try to create $dest if it does not exist?
+     *            DEFAULT: No.
+     * 'ids' - (array) The list of messages to copy.
+     *         DEFAULT: All messages in $mailbox will be copied.
+     * 'move' - (boolean) If true, delete the original messages.
+     *          DEFAULT: Original messages are not deleted.
+     * 'sequence' - (boolean) If true, 'ids' is an array of sequence numbers.
+     *              DEFAULT: 'ids' is an array of UIDs.
+     * </pre>
+     *
+     * @return mixed  An array mapping old UIDs (keys) to new UIDs (values) on
+     *                success (if the IMAP server and/or driver support the
+     *                UIDPLUS extension) or true.
+     */
+    public function copy($source, $dest, $options = array())
+    {
+        $this->openMailbox($source, empty($options['move']) ? self::OPEN_AUTO : self::OPEN_READWRITE);
+        return $this->_copy(Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($dest), $options);
+    }
+
+    /**
+     * Copy messages to another mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $dest    The destination mailbox (UTF7-IMAP).
+     * @param array $options  Additional options.
+     *
+     * @return mixed  An array mapping old UIDs (keys) to new UIDs (values) on
+     *                success (if the IMAP server and/or driver support the
+     *                UIDPLUS extension) or true.
+     */
+    abstract protected function _copy($dest, $options);
+
+    /**
+     * Set quota limits. The server must support the IMAP QUOTA extension
+     * (RFC 2087).
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $root    The quota root. Either in UTF7-IMAP or UTF-8.
+     * @param array $options  Additional options:
+     * <pre>
+     * 'messages' - (integer) The limit to set on the number of messages
+     *              allowed.
+     *              DEFAULT: No limit set.
+     * 'storage' - (integer) The limit (in units of 1 KB) to set for the
+     *             storage size.
+     *             DEFAULT: No limit set.
+     * </pre>
+     */
+    public function setQuota($root, $options = array())
+    {
+        if (!$this->queryCapability('QUOTA')) {
+            throw new Horde_Imap_Client_Exception('Server does not support the QUOTA extension.', Horde_Imap_Client_Exception::NOSUPPORTIMAPEXT);
+        }
+
+        if (isset($options['messages']) || isset($options['storage'])) {
+            $this->_setQuota(Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($root), $options);
+        }
+    }
+
+    /**
+     * Set quota limits.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $root    The quota root (UTF7-IMAP).
+     * @param array $options  Additional options.
+     *
+     * @return boolean  True on success.
+     */
+    abstract protected function _setQuota($root, $options);
+
+    /**
+     * Get quota limits. The server must support the IMAP QUOTA extension
+     * (RFC 2087).
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $root  The quota root. Either in UTF7-IMAP or UTF-8.
+     *
+     * @return mixed  An array with these possible keys: 'messages' and
+     *                'storage'; each key holds an array with 2 values:
+     *                'limit' and 'usage'.
+     */
+    public function getQuota($root)
+    {
+        if (!$this->queryCapability('QUOTA')) {
+            throw new Horde_Imap_Client_Exception('Server does not support the QUOTA extension.', Horde_Imap_Client_Exception::NOSUPPORTIMAPEXT);
+        }
+
+        return $this->_getQuota(Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($root));
+    }
+
+    /**
+     * Get quota limits.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $root  The quota root (UTF7-IMAP).
+     *
+     * @return mixed  An array with these possible keys: 'messages' and
+     *                'storage'; each key holds an array with 2 values:
+     *                'limit' and 'usage'.
+     */
+    abstract protected function _getQuota($root);
+
+    /**
+     * Get quota limits for a mailbox. The server must support the IMAP QUOTA
+     * extension (RFC 2087).
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox  A mailbox. Either in UTF7-IMAP or UTF-8.
+     *
+     * @return mixed  An array with the keys being the quota roots. Each key
+     *                holds an array with two possible keys: 'messages' and
+     *                'storage'; each of these keys holds an array with 2
+     *                values: 'limit' and 'usage'.
+     */
+    public function getQuotaRoot($mailbox)
+    {
+        if (!$this->queryCapability('QUOTA')) {
+            throw new Horde_Imap_Client_Exception('Server does not support the QUOTA extension.', Horde_Imap_Client_Exception::NOSUPPORTIMAPEXT);
+        }
+
+        return $this->_getQuotaRoot(Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($mailbox));
+    }
+
+    /**
+     * Get quota limits for a mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox  A mailbox (UTF7-IMAP).
+     *
+     * @return mixed  An array with the keys being the quota roots. Each key
+     *                holds an array with two possible keys: 'messages' and
+     *                'storage'; each of these keys holds an array with 2
+     *                values: 'limit' and 'usage'.
+     */
+    abstract protected function _getQuotaRoot($mailbox);
+
+    /**
+     * Get the ACL rights for a given mailbox. The server must support the
+     * IMAP ACL extension (RFC 2086/4314).
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox  A mailbox. Either in UTF7-IMAP or UTF-8.
+     *
+     * @return array  An array with identifiers as the keys and an array of
+     *                rights as the values.
+     */
+    public function getACL($mailbox)
+    {
+        return $this->_getACL(Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($mailbox));
+    }
+
+    /**
+     * Get ACL rights for a given mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox  A mailbox (UTF7-IMAP).
+     *
+     * @return array  An array with identifiers as the keys and an array of
+     *                rights as the values.
+     */
+    abstract protected function _getACL($mailbox);
+
+    /**
+     * Set ACL rights for a given mailbox/identifier.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox     A mailbox. Either in UTF7-IMAP or UTF-8.
+     * @param string $identifier  The identifier to alter. Either in UTF7-IMAP
+     *                            or UTF-8.
+     * @param array $options      Additional options:
+     * <pre>
+     * 'remove' - (boolean) If true, removes all rights for $identifier.
+     *            DEFAULT: Rights in 'rights' are added.
+     * 'rights' - (string) The rights to alter.
+     *            DEFAULT: No rights are altered.
+     * </pre>
+     */
+    public function setACL($mailbox, $identifier, $options)
+    {
+        if (!$this->queryCapability('ACL')) {
+            throw new Horde_Imap_Client_Exception('Server does not support the ACL extension.', Horde_Imap_Client_Exception::NOSUPPORTIMAPEXT);
+        }
+
+        return $this->_setACL(Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($mailbox), Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($identifier), $options);
+    }
+
+    /**
+     * Set ACL rights for a given mailbox/identifier.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox     A mailbox (UTF7-IMAP).
+     * @param string $identifier  The identifier to alter (UTF7-IMAP).
+     * @param array $options      Additional options.
+     */
+    abstract protected function _setACL($mailbox, $identifier, $options);
+
+    /**
+     * List the ACL rights for a given mailbox/identifier. The server must
+     * support the IMAP ACL extension (RFC 2086/4314).
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox     A mailbox. Either in UTF7-IMAP or UTF-8.
+     * @param string $identifier  The identifier to alter. Either in UTF7-IMAP
+     *                            or UTF-8.
+     *
+     * @return array  An array with two elements: 'required' (a list of
+     *                required rights) and 'optional' (a list of rights the
+     *                identifier can be granted in the mailbox; these rights
+     *                may be grouped together to indicate that they are tied
+     *                to each other).
+     */
+    public function listACLRights($mailbox, $identifier)
+    {
+        if (!$this->queryCapability('ACL')) {
+            throw new Horde_Imap_Client_Exception('Server does not support the ACL extension.', Horde_Imap_Client_Exception::NOSUPPORTIMAPEXT);
+        }
+
+        return $this->_listACLRights(Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($mailbox), Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($identifier));
+    }
+
+    /**
+     * Get ACL rights for a given mailbox/identifier.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox     A mailbox (UTF7-IMAP).
+     * @param string $identifier  The identifier to alter (UTF7-IMAP).
+     *
+     * @return array  An array of rights (keys: 'required' and 'optional').
+     */
+    abstract protected function _listACLRights($mailbox, $identifier);
+
+    /**
+     * Get the ACL rights for the current user for a given mailbox. The
+     * server must support the IMAP ACL extension (RFC 2086/4314).
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox  A mailbox. Either in UTF7-IMAP or UTF-8.
+     *
+     * @return array  An array of rights.
+     */
+    public function getMyACLRights($mailbox)
+    {
+        if (!$this->queryCapability('ACL')) {
+            throw new Horde_Imap_Client_Exception('Server does not support the ACL extension.', Horde_Imap_Client_Exception::NOSUPPORTIMAPEXT);
+        }
+
+        return $this->_getMyACLRights(Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($mailbox));
+    }
+
+    /**
+     * Get the ACL rights for the current user for a given mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox  A mailbox (UTF7-IMAP).
+     *
+     * @return array  An array of rights.
+     */
+    abstract protected function _getMyACLRights($mailbox);
+
+    /* Utility functions. */
+
+    /**
+     * Returns UIDs for an ALL search, or for a sequence number -> UID lookup.
+     *
+     * @param mixed $ids    If null, return all UIDs for the mailbox. If an
+     *                      array, only look up these values.
+     * @param boolean $seq  Are $ids sequence numbers?
+     *
+     * @return array  An array with 2 possible entries:
+     * <pre>
+     * 'lookup' - (array) If $ids is not null, the mapping of sequence
+     *            numbers (keys) to UIDs (values).
+     * 'uids' - (array) The list of UIDs.
+     * </pre>
+     */
+    protected function _getSeqUIDLookup($ids, $seq)
+    {
+        $search = new Horde_Imap_Client_Search_Query();
+        $search->sequence($ids, $seq);
+        $res = $this->search($this->_selected, $search, array('sort' => array(self::SORT_ARRIVAL)));
+        $ret = array('uids' => $res['sort']);
+        if ($seq) {
+            if (empty($ids)) {
+                $ids = range(1, count($ret['uids']));
+            } else {
+                sort($ids, SORT_NUMERIC);
+            }
+            $ret['lookup'] = array_combine($ret['uids'], $ids);
+        }
+
+        return $ret;
+    }
+
+    /**
+     * Store FETCH data in cache.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param array $data      The data array.
+     * @param array $options   Additional options:
+     * <pre>
+     * 'mailbox' - (string) The mailbox to update.
+     *             DEFAULT: The selected mailbox.
+     * 'seq' - (boolean) Is data stored with sequence numbers?
+     *             DEFAULT: Data stored with UIDs.
+     * 'uidvalid' - (integer) The UID Validity number.
+     *              DEFAULT: UIDVALIDITY discovered via a status() call.
+     * </pre>
+     */
+    protected function _updateCache($data, $options = array())
+    {
+        if (!$this->_initCacheOb()) {
+            return;
+        }
+
+        if (!empty($options['seq'])) {
+            $seq_res = $this->_getSeqUIDLookup(array_keys($data));
+        }
+
+        $cf = $this->_params['cache']['fields'];
+        $is_flags = false;
+        $highestmodseq = $tocache = array();
+        $mailbox = empty($options['mailbox']) ? $this->_selected : $options['mailbox'];
+
+        if (empty($options['uidvalid'])) {
+            $status_res = $this->status($mailbox, self::STATUS_HIGHESTMODSEQ | self::STATUS_UIDVALIDITY);
+            $uidvalid = $status_res['uidvalidity'];
+            if (isset($status_res['highestmodseq'])) {
+                $highestmodseq[] = $status_res['highestmodseq'];
+            }
+        } else {
+            $uidvalid = $options['uidvalid'];
+        }
+
+        reset($data);
+        while (list($k, $v) = each($data)) {
+            $tmp = array();
+            $id = empty($options['seq']) ? $k : $seq_res['lookup'][$k];
+
+            reset($v);
+            while (list($label, $val) = each($v)) {
+                switch ($label) {
+                case 'structure':
+                    if (isset($cf[self::FETCH_STRUCTURE])) {
+                        $tmp[is_array($val) ? 'HICstructa' : 'HICstructm'] = $val;
+                    }
+                    break;
+
+                case 'envelope':
+                    if (isset($cf[self::FETCH_ENVELOPE])) {
+                        $tmp['HICenv'] = $val;
+                    }
+                    break;
+
+                case 'flags':
+                    if (isset($cf[self::FETCH_FLAGS])) {
+                        /* A FLAGS FETCH can only occur if we are in the
+                         * mailbox. So either HIGHESTMODSEQ has already been
+                         * updated or the flag FETCHs will provide the new
+                         * HIGHESTMODSEQ value.  In either case, we are
+                         * guaranteed that all cache information is correctly
+                         * updated (in the former case, we reached here via
+                         * an 'changedsince' FETCH and in the latter case, we
+                         * are in EXAMINE/SELECT mode and will catch all flag
+                         * changes). */
+                        if (isset($v['modseq'])) {
+                            $highestmodseq[] = $v['modseq'];
+                        }
+                        $tmp['HICflags'] = $val;
+                        $is_flags = true;
+                    }
+                    break;
+
+                case 'date':
+                    if (isset($cf[self::FETCH_DATE])) {
+                        $tmp['HICdate'] = $val;
+                    }
+                    break;
+
+                case 'size':
+                    if (isset($cf[self::FETCH_SIZE])) {
+                        $tmp['HICsize'] = $val;
+                    }
+                    break;
+                }
+            }
+
+            if (!empty($tmp)) {
+                $tocache[$id] = $tmp;
+            }
+        }
+
+        try {
+            $this->_cacheOb->set($mailbox, $tocache, $uidvalid);
+            if ($is_flags) {
+                $this->_cacheOb->setMetaData($mailbox, array('HICmodseq' => max($highestmodseq)));
+            }
+        } catch (Horde_Imap_Client_Exception $e) {
+            if ($e->getCode() != Horde_Imap_Client_Exception::CACHEUIDINVALID) {
+                throw $e;
+            }
+        }
+    }
+}
+
+/*
+ * Abstraction of the IMAP4rev1 search criteria (see RFC 3501 [6.4.4]).  This
+ * class allows translation between abstracted search criteria and a
+ * generated IMAP search criteria string suitable for sending to a remote
+ * IMAP server.
+ */
+class Horde_Imap_Client_Search_Query
+{
+    /* Constants for dateSearch() */
+    const DATE_BEFORE = 'BEFORE';
+    const DATE_ON = 'ON';
+    const DATE_SINCE = 'SINCE';
+
+    /* Constants for intervalSearch() */
+    const INTERVAL_OLDER = 'OLDER';
+    const INTERVAL_YOUNGER = 'YOUNGER';
+
+    /**
+     * The charset of the search strings.  All text strings must be in
+     * this charset.
+     *
+     * @var string
+     */
+    protected $_charset = 'US-ASCII';
+
+    /**
+     * The list of defined system flags (see RFC 3501 [2.3.2]).
+     *
+     * @var array
+     */
+    protected $_systemflags = array(
+        'ANSWERED', 'DELETED', 'DRAFT', 'FLAGGED', 'RECENT', 'SEEN'
+    );
+
+    /**
+     * The list of 'system' headers that have a specific search query.
+     *
+     * @var array
+     */
+    protected $_systemheaders = array(
+        'BCC', 'CC', 'FROM', 'SUBJECT', 'TO'
+    );
+
+    /**
+     * The list of search params.
+     *
+     * @var array
+     */
+    protected $_search = array();
+
+    /**
+     * List of extensions needed for advanced queries.
+     *
+     * @var array
+     */
+    protected $_exts = array();
+
+    /**
+     * Sets the charset of the search text.
+     *
+     * @param string $charset  The charset to use for the search.
+     */
+    public function charset($charset)
+    {
+        $this->_charset = strtoupper($charset);
+    }
+
+    /**
+     * Builds an IMAP4rev1 compliant search string.
+     *
+     * @return array  An array with 3 elements:
+     * <pre>
+     * 'charset' - (string) The charset of the search string.
+     * 'imap4' - (boolean) True if the search uses IMAP4 criteria (as opposed
+     *           to IMAP2 search criteria)
+     * 'query' - (string) The IMAP search string
+     * </pre>
+     */
+    public function build()
+    {
+        $cmds = array();
+        $imap4 = false;
+        $ptr = &$this->_search;
+
+        if (isset($ptr['new'])) {
+            if ($ptr['new']) {
+                $cmds[] = 'NEW';
+                unset($ptr['flag']['UNSEEN']);
+            } else {
+                $cmds[] = 'OLD';
+            }
+            unset($ptr['flag']['RECENT']);
+        }
+
+        if (!empty($ptr['flag'])) {
+            foreach ($ptr['flag'] as $key => $val) {
+                if ($key == 'draft') {
+                    // DRAFT flag was not in IMAP2
+                    $imap4 = true;
+                }
+
+                $tmp = '';
+                if (!$val['set']) {
+                    // This is a 'NOT' search.  All system flags but \Recent
+                    // have 'UN' equivalents.
+                    if ($key == 'RECENT') {
+                        $tmp = 'NOT ';
+                        // NOT searches were not in IMAP2
+                        $imap4 = true;
+                    } else {
+                        $tmp = 'UN';
+                    }
+                }
+
+                $cmds[] = $tmp . ($val['type'] == 'keyword' ? 'KEYWORD ' : '') . $key;
+            }
+        }
+
+        if (!empty($ptr['header'])) {
+            foreach ($ptr['header'] as $val) {
+                $tmp = '';
+                if ($val['not']) {
+                    $tmp = 'NOT ';
+                    // NOT searches were not in IMAP2
+                    $imap4 = true;
+                }
+
+                if (!in_array($val['header'], $this->_systemheaders)) {
+                    // HEADER searches were not in IMAP2
+                    $tmp .= 'HEADER ';
+                    $imap4 = true;
+                }
+                $cmds[] = $tmp . $val['header'] . ' ' . Horde_Imap_Client::escape($val['text']);
+            }
+        }
+
+        if (!empty($ptr['text'])) {
+            foreach ($ptr['text'] as $val) {
+                $tmp = '';
+                if ($val['not']) {
+                    $tmp = 'NOT ';
+                    // NOT searches were not in IMAP2
+                    $imap4 = true;
+                }
+                $cmds[] = $tmp . $val['type'] . ' ' . Horde_Imap_Client::escape($val['text']);
+            }
+        }
+
+        if (!empty($ptr['size'])) {
+            foreach ($ptr['size'] as $key => $val) {
+                $cmds[] = ($val['not'] ? 'NOT ' : '' ) . $key . ' ' . $val['size'];
+                // LARGER/SMALLER searches were not in IMAP2
+                $imap4 = true;
+            }
+        }
+
+        if (isset($ptr['sequence'])) {
+            $cmds[] = ($ptr['sequence']['not'] ? 'NOT ' : '') . ($ptr['sequence']['sequence'] ? '' : 'UID ') . $ptr['sequence']['ids'];
+
+            // sequence searches were not in IMAP2
+            $imap4 = true;
+        }
+
+        if (!empty($ptr['date'])) {
+            foreach ($ptr['date'] as $key => $val) {
+                $tmp = '';
+                if ($val['not']) {
+                    $tmp = 'NOT ';
+                    // NOT searches were not in IMAP2
+                    $imap4 = true;
+                }
+
+                if ($key == 'header') {
+                    $tmp .= 'SENT';
+                    // 'SENT*' searches were not in IMAP2
+                    $imap4 = true;
+                }
+                $cmds[] = $tmp . $val['range'] . ' ' . $val['date'];
+            }
+        }
+
+        if (!empty($ptr['within'])) {
+            $imap4 = true;
+            $this->_exts['WITHIN'] = true;
+
+            foreach ($ptr['within'] as $key => $val) {
+                $cmds[] = ($val['not'] ? 'NOT ' : '') . $key . ' ' . $val['interval'];
+            }
+        }
+
+        if (!empty($ptr['modseq'])) {
+            $imap4 = true;
+            $this->_exts['CONDSTORE'] = true;
+            $cmds[] = ($ptr['modseq']['not'] ? 'NOT ' : '') .
+                'MODSEQ ' .
+                (is_null($ptr['modseq']['name'])
+                    ? ''
+                    : Horde_Imap_Client::escape($ptr['modseq']['name']) . ' ' . $ptr['modseq']['type'] . ' ') .
+                $ptr['modseq']['value'];
+        }
+
+        if (isset($ptr['prevsearch'])) {
+            $imap4 = true;
+            $this->_exts['SEARCHRES'] = true;
+            $cmds[] = ($ptr['prevsearch'] ? '' : 'NOT ') . '$';
+        }
+
+        $query = '';
+
+        // Add OR'ed queries
+        if (!empty($ptr['or'])) {
+            foreach ($ptr['or'] as $key => $val) {
+                // OR queries were not in IMAP 2
+                $imap4 = true;
+
+                if ($key == 0) {
+                    $query = '(' . $query . ')';
+                }
+
+                $ret = $val->build();
+                $query = 'OR (' . $ret['query'] . ') ' . $query;
+            }
+        }
+
+        // Add AND'ed queries
+        if (!empty($ptr['and'])) {
+            foreach ($ptr['and'] as $key => $val) {
+                $ret = $val->build();
+                $query .= ' ' . $ret['query'];
+            }
+        }
+
+        // Default search is 'ALL'
+        if (empty($cmds)) {
+            $query .= empty($query) ? 'ALL' : '';
+        } else {
+            $query .= implode(' ', $cmds);
+        }
+
+        return array(
+            'charset' => $this->_charset,
+            'imap4' => $imap4,
+            'query' => trim($query)
+        );
+    }
+
+    /**
+     * Return the list of any IMAP extensions needed to perform the query.
+     *
+     * @return array  The list of extensions (CAPABILITY responses) needed to
+     *                perform the query.
+     */
+    public function extensionsNeeded()
+    {
+        return $this->_exts;
+    }
+
+    /**
+     * Search for a flag/keywords.
+     *
+     * @param string $name  The flag or keyword name.
+     * @param boolean $set  If true, search for messages that have the flag
+     *                      set.  If false, search for messages that do not
+     *                      have the flag set.
+     */
+    public function flag($name, $set = true)
+    {
+        $name = strtoupper(ltrim($name, '\\'));
+        if (!isset($this->_search['flag'])) {
+            $this->_search['flag'] = array();
+        }
+        $this->_search['flag'][$name] = array(
+            'set' => $set,
+            'type' => in_array($name, $this->_systemflags) ? 'flag' : 'keyword'
+        );
+    }
+
+    /**
+     * Search for either new messages (messages that have the '\Recent' flag
+     * but not the '\Seen' flag) or old messages (messages that do not have
+     * the '\Recent' flag).  If new messages are searched, this will clear
+     * any '\Recent' or '\Unseen' flag searches.  If old messages are searched,
+     * this will clear any '\Recent' flag search.
+     *
+     * @param boolean $newmsgs  If true, searches for new messages.  Else,
+     *                          search for old messages.
+     */
+    public function newMsgs($newmsgs = true)
+    {
+        $this->_search['new'] = $newmsgs;
+    }
+
+    /**
+     * Search for text in the header of a message.
+     *
+     * @param string $header  The header field.
+     * @param string $text    The search text.
+     * @param boolean $not    If true, do a 'NOT' search of $text.
+     */
+    public function headerText($header, $text, $not = false)
+    {
+        if (!isset($this->_search['header'])) {
+            $this->_search['header'] = array();
+        }
+        $this->_search['header'][] = array(
+            'header' => strtoupper($header),
+            'text' => $text,
+            'not' => $not
+        );
+    }
+
+    /**
+     * Search for text in either the entire message, or just the body.
+     *
+     * @param string $text      The search text.
+     * @param string $bodyonly  If true, only search in the body of the
+     *                          message. If false, also search in the headers.
+     * @param boolean $not      If true, do a 'NOT' search of $text.
+     */
+    public function text($text, $bodyonly = true, $not = false)
+    {
+        if (!isset($this->_search['text'])) {
+            $this->_search['text'] = array();
+        }
+        $this->_search['text'][] = array(
+            'text' => $text,
+            'not' => $not,
+            'type' => $bodyonly ? 'BODY' : 'TEXT'
+        );
+    }
+
+    /**
+     * Search for messages smaller/larger than a certain size.
+     *
+     * @param integer $size    The size (in bytes).
+     * @param boolean $larger  Search for messages larger than $size?
+     * @param boolean $not     If true, do a 'NOT' search of $text.
+     */
+    public function size($size, $larger = false, $not = false)
+    {
+        if (!isset($this->_search['size'])) {
+            $this->_search['size'] = array();
+        }
+        $this->_search['size'][$larger ? 'LARGER' : 'SMALLER'] = array(
+            'size' => (float)$size,
+            'not' => $not
+        );
+    }
+
+    /**
+     * Search for messages within a given message range. Only one message
+     * range can be specified per query.
+     *
+     * @param array $ids         The list of messages to search.
+     * @param boolean $sequence  By default, $ids is assumed to be UIDs. If
+     *                           this param is true, $ids are taken to be
+     *                           message sequence numbers instead.
+     * @param boolean $not       If true, do a 'NOT' search of the sequence.
+     */
+    public function sequence($ids, $sequence = false, $not = false)
+    {
+        if (empty($ids)) {
+            $ids = '1:*';
+        } else {
+            $ids = Horde_Imap_Client::toSequenceString($ids);
+        }
+        $this->_search['sequence'] = array(
+            'ids' => $ids,
+            'not' => $not,
+            'sequence' => $sequence
+        );
+    }
+
+    /**
+     * Search for messages within a date range. Only one internal date and
+     * one RFC 2822 date can be specified per query.
+     *
+     * @param integer $month   Month (from 1-12).
+     * @param integer $day     Day of month (from 1-31).
+     * @param integer $year    Year (4-digit year).
+     * @param string $range    Either:
+     * <pre>
+     * Horde_Imap_Client_Search_Query::DATE_BEFORE,
+     * Horde_Imap_Client_Search_Query::DATE_ON, or
+     * Horde_Imap_Client_Search_Query::DATE_SINCE.
+     * </pre>
+     * @param boolean $header  If true, search using the date in the message
+     *                         headers. If false, search using the internal
+     *                         IMAP date (usually arrival time).
+     * @param boolean $not     If true, do a 'NOT' search of the range.
+     */
+    public function dateSearch($month, $day, $year, $range, $header = true,
+                        $not = false)
+    {
+        $type = $header ? 'header' : 'internal';
+        if (!isset($this->_search['date'])) {
+            $this->_search['date'] = array();
+        }
+        $this->_search['date'][$header ? 'header' : 'internal'] = array(
+            'date' => date("d-M-y", mktime(0, 0, 0, $month, $day, $year)),
+            'range' => $range,
+            'not' => $not
+        );
+    }
+
+    /**
+     * Search for messages within a given interval. Only one interval of each
+     * type can be specified per search query. The IMAP server must support
+     * the WITHIN extension (RFC 5032) for this query to be used.
+     *
+     * @param integer $interval  Seconds from the present.
+     * @param string $range      Either:
+     * <pre>
+     * Horde_Imap_Client_Search_Query::INTERVAL_OLDER, or
+     * Horde_Imap_Client_Search_Query::INTERVAL_YOUNGER
+     * </pre>
+     * @param boolean $not       If true, do a 'NOT' search.
+     */
+    public function intervalSearch($interval, $range, $not = false)
+    {
+        if (!isset($this->_search['within'])) {
+            $this->_search['within'] = array();
+        }
+        $this->_search['within'][$range] = array(
+            'interval' => $interval,
+            'not' => $not
+        );
+    }
+
+    /**
+     * AND queries - the contents of this query will be AND'ed (in its
+     * entirety) with the contents of each of the queries passed in.  All
+     * AND'd queries must share the same charset as this query.
+     *
+     * @param array $queries  An array of queries to AND with this one.  Each
+     *                        query is a Horde_Imap_Client_Search_Query
+     *                        object.
+     */
+    public function andSearch($queries)
+    {
+        if (!isset($this->_search['and'])) {
+            $this->_search['and'] = array();
+        }
+        $this->_search['and'] = array_merge($this->_search['and'], $queries);
+    }
+
+    /**
+     * OR a query - the contents of this query will be OR'ed (in its entirety)
+     * with the contents of each of the queries passed in.  All OR'd queries
+     * must share the same charset as this query.  All contents of any single
+     * query will be AND'ed together.
+     *
+     * @param array $queries  An array of queries to OR with this one.  Each
+     *                        query is a Horde_Imap_Client_Search_Query
+     *                        object.
+     */
+    public function orSearch($queries)
+    {
+        if (!isset($this->_search['or'])) {
+            $this->_search['or'] = array();
+        }
+        $this->_search['or'] = array_merge($this->_search['or'], $queries);
+    }
+
+    /**
+     * Search for messages modified since a specific moment. The IMAP server
+     * must support the CONDSTORE extension (RFC 4551) for this query to be
+     * used.
+     *
+     * @param integer $value  The mod-sequence value.
+     * @param string $name    The entry-name string.
+     * @param string $type    Either 'shared', 'priv', or 'all'. Defaults to
+     *                        'all'
+     * @param boolean $not    If true, do a 'NOT' search.
+     */
+    public function modseq($value, $name = null, $type = null, $not = false)
+    {
+        if (!is_null($type)) {
+            $type = strtolower($type);
+            if (!in_array($type, array('shared', 'priv', 'all'))) {
+                $type = 'all';
+            }
+        }
+
+        $this->_search['modseq'] = array(
+            'value' => $value,
+            'name' => $name,
+            'not' => $not,
+            'type' => (!is_null($name) && is_null($type)) ? 'all' : $type
+        );
+    }
+
+    /**
+     * Use the results from the previous SEARCH command. The IMAP server must
+     * support the SEARCHRES extension (RFC 5032) for this query to be used.
+     *
+     * @param boolean $not  If true, don't match the previous query.
+     */
+    public function previousSearch($not = false)
+    {
+        $this->_search['prevsearch'] = $not;
+    }
+}
+
+/*
+ * A class allowing easy access to threaded sort results from
+ * Horde_Imap_Client::thread().
+ */
+class Horde_Imap_Client_Thread
+{
+    /**
+     * Internal thread data structure.
+     *
+     * @var array
+     */
+    protected $_thread = array();
+
+    /**
+     * The index type.
+     *
+     * @var string
+     */
+    protected $_type;
+
+    /**
+     * Constructor.
+     *
+     * @param array $data   The data as returned by
+     *                      Horde_Imap_Client_Base::_thread().
+     * @param string $type  Either 'uid' or 'sequence'.
+     */
+    function __construct($data, $type)
+    {
+        $this->_thread = $data;
+        $this->_type = $type;
+    }
+
+    /**
+     * Return the raw thread data array.
+     *
+     * @return array  See Horde_Imap_Client_Base::_thread().
+     */
+    public function getRawData()
+    {
+        return $this->_thread;
+    }
+
+    /**
+     * Gets the indention level for an index.
+     *
+     * @param integer $index  The index.
+     *
+     * @return mixed  Returns the thread indent level if $index found.
+     *                Returns false on failure.
+     */
+    public function getThreadIndent($index)
+    {
+        return isset($this->_thread[$index]['level'])
+            ? $this->_thread[$index]['level']
+            : false;
+    }
+
+    /**
+     * Gets the base thread index for an index.
+     *
+     * @param integer $index  The index.
+     *
+     * @return mixed  Returns the base index if $index is part of a thread.
+     *                Returns false on failure.
+     */
+    public function getThreadBase($index)
+    {
+        return !empty($this->_thread[$index]['base'])
+            ? $this->_thread[$index]['base']
+            : false;
+    }
+
+    /**
+     * Is this index the last in the current level?
+     *
+     * @param integer $index  The index.
+     *
+     * @return boolean  Returns true if $index is the last element in the
+     *                  current thread level.
+     *                  Returns false if not, or on failure.
+     */
+    public function lastInLevel($index)
+    {
+        return !empty($this->_thread[$index]['last'])
+            ? $this->_thread[$index]['last']
+            : false;
+    }
+
+    /**
+     * Return the sorted list of messages indices.
+     *
+     * @param boolean $new  True for newest first, false for oldest first.
+     *
+     * @return array  The sorted list of messages.
+     */
+    public function messageList($new)
+    {
+        return ($new) ? array_reverse(array_keys($this->_thread)) : array_keys($this->_thread);
+    }
+
+    /**
+     * Returns the list of messages in the current thread.
+     *
+     * @param integer $index  The index of the current message.
+     *
+     * @return array  A list of message indices.
+     */
+    public function getThread($index)
+    {
+        /* Find the beginning of the thread. */
+        if (($begin = $this->getThreadBase($index)) === false) {
+            return array($index);
+        }
+
+        /* Work forward from the first thread element to find the end of the
+         * thread. */
+        $in_thread = false;
+        $thread_list = array();
+        reset($this->_thread);
+        while (list($k, $v) = each($this->_thread)) {
+            if ($k == $begin) {
+                $in_thread = true;
+            } elseif ($in_thread && ($v['base'] != $begin)) {
+                break;
+            }
+
+            if ($in_thread) {
+                $thread_list[] = $k;
+            }
+        }
+
+        return $thread_list;
+    }
+}
diff --git a/framework/Imap_Client/lib/Horde/Imap/Client/Cache.php b/framework/Imap_Client/lib/Horde/Imap/Client/Cache.php
new file mode 100644 (file)
index 0000000..1f71905
--- /dev/null
@@ -0,0 +1,612 @@
+<?php
+
+require_once 'Horde/Cache.php';
+require_once 'Horde/Serialize.php';
+
+/**
+ * Horde_Imap_Client_Cache:: provides an interface to cache various data
+ * retrieved from the IMAP server.
+ *
+ * Requires Horde_Cache and Horde_Serialize packages.
+ *
+ * REQUIRED Parameters:
+ * ====================
+ * 'driver' - (string) The Horde_Cache driver to use.
+ * 'driver_params' - (string) The params to pass to the Horde_Cache driver.
+ * 'hostspec' - (string) The IMAP hostspec.
+ * 'username' - (string) The IMAP username.
+ *
+ * Optional Parameters:
+ * ====================
+ * 'compress' - (string) Compression to use on the cached data.
+ *              Either false, 'gzip' or 'lzf'.
+ *              DEFAULT: No compression
+ * 'debug' - (resource) If set, will output debug information to the stream
+ *           identified.
+ *           DEFAULT: No debug output
+ * 'lifetime' - (integer) The lifetime of the cache data (in seconds).
+ *              DEFAULT: 1 week (604800 secs)
+ * 'slicesize' - (integer) The slicesize to use.
+ *               DEFAULT: 50
+ *
+ * $Horde: framework/Imap_Client/lib/Horde/Imap/Client/Cache.php,v 1.19 2008/10/28 21:54:40 slusarz Exp $
+ *
+ * Copyright 2005-2008 The Horde Project (http://www.horde.org/)
+ *
+ * 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@horde.org>
+ * @category Horde
+ * @package  Horde_Imap_Client
+ */
+class Horde_Imap_Client_Cache
+{
+    /**
+     * The configuration params.
+     *
+     * @var array
+     */
+    protected $_params = array();
+
+    /**
+     * The Horde_Cache object.
+     *
+     * @var Horde_Cache
+     */
+    protected $_cacheOb;
+
+    /**
+     * The list of items to save on shutdown.
+     *
+     * @var array
+     */
+    protected $_save = array();
+
+    /**
+     * The working data for the current pageload.  All changes take place to
+     * this data.
+     *
+     * @var array
+     */
+    protected $_data = array();
+
+    /**
+     * The list of cache slices loaded.
+     *
+     * @var array
+     */
+    protected $_loaded = array();
+
+    /**
+     * The mapping of UIDs to slices.
+     *
+     * @var array
+     */
+    protected $_slicemap = array();
+
+    /**
+     * Return a reference to a concrete Horde_Imap_Client_Cache instance.
+     *
+     * This method must be invoked as:
+     *   $var = &IMP_MessageCache::singleton();
+     *
+     * @param array $params  The configuration parameters.
+     *
+     * @return Horde_Imap_Client_Cache  The global instance.
+     */
+    static public function &singleton($params = array())
+    {
+        static $instance = array();
+
+        $sig = md5(serialize($params));
+
+        if (!isset($instance[$sig])) {
+            $instance[$sig] = new Horde_Imap_Client_Cache($params);
+        }
+
+        return $instance[$sig];
+    }
+
+    /**
+     * Constructor.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param array $params  The configuration parameters.
+     */
+    function __construct($params = array())
+    {
+        if (empty($params['driver']) ||
+            empty($params['driver_params']) ||
+            empty($params['username']) ||
+            empty($params['hostspec'])) {
+            throw new Horde_Imap_Client_Exception('Missing required parameters to Horde_Imap_Client_Cache.');
+        }
+
+        /* Initialize the Cache object. */
+        $this->_cacheOb = &Horde_Cache::singleton($params['driver'], $params['driver_params']);
+        if (is_a($this->_cacheOb, 'PEAR_Error')) {
+            throw new Horde_Imap_Client_Exception($this->_cacheOb->getMessage());
+        }
+
+        $compress = null;
+        if (!empty($params['compress'])) {
+            switch ($params['compress']) {
+            case 'gzip':
+                if (Horde_Serialize::hasCapability(SERIALIZE_GZ_COMPRESS)) {
+                    $compress = SERIALIZE_GZ_COMPRESS;
+                }
+                break;
+
+            case 'lzf':
+                if (Horde_Serialize::hasCapability(SERIALIZE_LZF)) {
+                    $compress = SERIALIZE_LZF;
+                }
+                break;
+            }
+
+            if (is_null($compress)) {
+                throw new Horde_Imap_Client_Exception('Horde_Cache does not support the compression type given.');
+            }
+        }
+
+        $this->_params = array(
+            'compress' => $compress,
+            'debug' => empty($params['debug']) ? false : $params['debug'],
+            'hostspec' => $params['hostspec'],
+            'lifetime' => empty($params['lifetime']) ? 604800 : intval($params['lifetime']),
+            'slicesize' => empty($params['slicesize']) ? 50 : intval($params['slicesize']),
+            'username' => $params['username']
+        );
+    }
+
+    /**
+     * Saves items to the cache at shutdown.
+     */
+    function __destruct()
+    {
+        $compress = $this->_params['compress'];
+        $lifetime = $this->_params['lifetime'];
+
+        foreach ($this->_save as $mbox => $uids) {
+            $dptr = &$this->_data[$mbox];
+            $sptr = &$this->_slicemap[$mbox];
+
+            /* Get the list of slices to save. */
+            foreach (array_intersect_key($sptr['slice'], array_flip($uids)) as $slice) {
+                $data = array();
+
+                /* Get the list of IDs to save. */
+                foreach (array_keys($sptr['slice'], $slice) as $uid) {
+                    /* Compress individual UID entries. We will worry about
+                     * error checking when decompressing (cache data will
+                     * automatically be invalidated then). */
+                    if (isset($dptr[$uid])) {
+                        $data[$uid] = ($compress && is_array($dptr[$uid])) ? Horde_Serialize::serialize($dptr[$uid], array(SERIALIZE_BASIC, $compress)) : $dptr[$uid];
+                    }
+                }
+
+                $cid = $this->_getCID($mbox, $slice);
+                if (empty($data)) {
+                    // If empty, we can expire the cache.
+                    $this->_cacheOb->expire($cid);
+                } else {
+                    $this->_cacheOb->set($cid, Horde_Serialize::serialize($data, SERIALIZE_BASIC), $lifetime);
+                }
+            }
+
+            // Save the slicemap
+            $this->_cacheOb->set($this->_getCID($mbox, 'slicemap'), Horde_Serialize::serialize($sptr, SERIALIZE_BASIC), $lifetime);
+        }
+    }
+
+    /**
+     * Create the unique ID used to store the data in the cache.
+     *
+     * @param string $mailbox  The mailbox to cache.
+     * @param string $slice    The cache slice.
+     *
+     * @return string  The cache ID (CID).
+     */
+    protected function _getCID($mailbox, $slice)
+    {
+        /* Cache ID = "prefix | username | mailbox | hostspec | slice" */
+        return 'horde_imap_client|' . $this->_params['username'] . '|' . $mailbox . '|' . $this->_params['hostspec'] . '|' . $slice;
+    }
+
+    /**
+     * Get information from the cache.
+     * Throws a Horde_Imap_Cache_Exception on error.
+     *
+     * @param string $mailbox    An IMAP mailbox string.
+     * @param array $uids        The list of message UIDs to retrieve
+     *                           information for. If empty, returns the list
+     *                           of cached UIDs.
+     * @param array $fields      An array of fields to retrieve.
+     * @param integer $uidvalid  The IMAP uidvalidity value of the mailbox.
+     *
+     * @return array  An array of arrays with the UID of the message as the
+     *                key (if found) and the fields as values (will be
+     *                undefined if not found). If $uids is empty, returns the
+     *                full list of cached UIDs.
+     */
+    public function get($mailbox, $uids = array(), $fields = array(),
+                        $uidvalid = null)
+    {
+        if (empty($uids)) {
+            $this->_loadSliceMap($mailbox, $uidvalid);
+            return array_keys($this->_slicemap[$mailbox]['slice']);
+        }
+
+        $ret_array = array();
+
+        $this->_loadUIDs($mailbox, $uids, $uidvalid);
+        if (!empty($this->_data[$mailbox])) {
+            $fields = array_flip($fields);
+            $ptr = &$this->_data[$mailbox];
+
+            foreach ($uids as $val) {
+                if (isset($ptr[$val])) {
+                    $ret_array[$val] = array_intersect_key($ptr[$val], $fields);
+                }
+            }
+
+            if ($this->_params['debug']) {
+                fwrite($this->_params['debug'], 'Horde_Imap_Client_Cache: Retrieved from cache (mailbox: ' . $mailbox . '; UIDs: ' . implode(',', array_keys($ret_array)) . ")\n");
+            }
+        }
+
+        return $ret_array;
+    }
+
+    /**
+     * Store information in cache.
+     *
+     * @param string $mailbox    An IMAP mailbox string.
+     * @param array $data        The list of data to save. The keys are the
+     *                           UIDs, the values are an array of information
+     *                           to save. If empty, do a check to make sure
+     *                           the uidvalidity is still valid.
+     * @param integer $uidvalid  The IMAP uidvalidity value of the mailbox.
+     */
+    public function set($mailbox, $data, $uidvalid = null)
+    {
+        $save = array_keys($data);
+        if (empty($save)) {
+            $this->_loadSliceMap($mailbox, $uidvalid);
+        } else {
+            try {
+                $this->_loadUIDs($mailbox, $save, $uidvalid);
+            } catch (Horde_Imap_Client_Exception $e) {
+                // Ignore invalidity - just start building the new cache
+            }
+
+            $d = &$this->_data[$mailbox];
+
+            reset($data);
+            while (list($k, $v) = each($data)) {
+                reset($v);
+                while (list($k2, $v2) = each($v)) {
+                    $d[$k][$k2] = $v2;
+                }
+            }
+
+            $this->_save[$mailbox] = isset($this->_save[$mailbox]) ? array_merge($this->_save[$mailbox], $save) : $save;
+
+            /* Need to select slices now because we may need list of cached
+             * UIDs before we save. */
+            $slices = $this->_getCacheSlices($mailbox, $save, true);
+
+            if ($this->_params['debug']) {
+                fwrite($this->_params['debug'], 'Horde_Imap_Client_Cache: Stored in cache (mailbox: ' . $mailbox . '; UIDs: ' . implode(',', $save) . ")\n");
+            }
+        }
+    }
+
+    /**
+     * Get metadata information for a mailbox.
+     *
+     * @param string $mailbox  An IMAP mailbox string.
+     * @param array $entries   An array of entries to return. If empty,
+     *                         returns all metadata.
+     *
+     * @return array  The requested metadata. Requested entries that do not
+     *                exist will be undefined. The following entries are
+     *                defaults and always present:
+     * <pre>
+     * 'uidvalid' - (integer) The UIDVALIDITY of the mailbox.
+     * </pre>
+     */
+    public function getMetaData($mailbox, $entries = array())
+    {
+        $this->_loadSliceMap($mailbox);
+        return empty($entries)
+            ? $this->_slicemap[$mailbox]['data']
+            : array_intersect_key($this->_slicemap[$mailbox]['data'], array_flip($entries));
+    }
+
+    /**
+     * Set metadata information for a mailbox.
+     *
+     * @param string $mailbox  An IMAP mailbox string.
+     * @param array $data      The list of data to save. The keys are the
+     *                         metadata IDs, the values are the associated
+     *                         data. The following labels are reserved:
+     *                         'uidvalid'.
+     */
+    public function setMetaData($mailbox, $data = array())
+    {
+        if (!empty($data)) {
+            unset($data['uidvalid']);
+            $this->_loadSliceMap($mailbox);
+            $this->_slicemap[$mailbox]['data'] = array_merge($this->_slicemap[$mailbox]['data'], $data);
+            if (!isset($this->_save[$mailbox])) {
+                $this->_save[$mailbox] = array();
+            }
+        }
+    }
+
+    /**
+     * Delete messages in the cache.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox  An IMAP mailbox string.
+     * @param array $uids      The list of message UIDs to delete.
+     */
+    public function deleteMsgs($mailbox, $uids)
+    {
+        if (empty($uids)) {
+            return;
+        }
+
+        $this->_loadSliceMap($mailbox);
+
+        $save = array();
+        $slicemap = &$this->_slicemap[$mailbox];
+        $todelete = &$slicemap['delete'];
+
+        foreach ($uids as $id) {
+            if (isset($slicemap['slice'][$id])) {
+                if (isset($this->_data[$mailbox][$id])) {
+                    $save[] = $id;
+                    unset($this->_data[$mailbox][$id]);
+                } else {
+                    $slice = $slicemap['slice'][$id];
+                    if (!isset($todelete[$slice])) {
+                        $todelete[$slice] = array();
+                    }
+                    $todelete[$slice][] = $id;
+                }
+                unset($this->_save[$mailbox][$id], $slicemap['slice'][$id]);
+            }
+        }
+
+        if (!empty($save)) {
+            if ($this->_params['debug']) {
+                fwrite($this->_params['debug'], 'Horde_Imap_Client_Cache: Deleted messages from cache (mailbox: ' . $mailbox . '; UIDs: ' . implode(',', $save) . ")\n");
+            }
+
+            $this->_save[$mailbox] = isset($this->_save[$mailbox]) ? array_merge($this->_save[$mailbox], $save) : $save;
+        } elseif (!isset($this->_save[$mailbox])) {
+            $this->_save[$mailbox] = array();
+        }
+    }
+
+    /**
+     * Delete a mailbox from the cache.
+     *
+     * @param string $mbox  The mailbox to delete.
+     */
+    public function deleteMailbox($mbox)
+    {
+        $this->_loadSliceMap($mbox);
+        foreach (array_keys(array_flip($this->_slicemap[$mbox]['slice'])) as $slice) {
+            $this->_cacheOb->expire($this->_getCID($mbox, $slice));
+        }
+        $this->_cacheOb->expire($this->_getCID($mbox, 'slicemap'));
+        unset($this->_data[$mbox], $this->_loaded[$mbox], $this->_save[$mbox], $this->_slicemap[$mbox]);
+
+        if ($this->_params['debug']) {
+            fwrite($this->_params['debug'], 'Horde_Imap_Client_Cache: Deleted mailbox from cache (mailbox: ' . $mbox . ")\n");
+        }
+    }
+
+    /**
+     * Load the given mailbox by regenerating from the cache.
+     * Throws a Horde_Imap_Client_Exception on error (only if $uidvalid is
+     * set).
+     *
+     * @param string $mailbox    The mailbox to load.
+     * @param array $uids        The UIDs to load.
+     * @param integer $uidvalid  The IMAP uidvalidity value of the mailbox.
+     */
+    protected function _loadMailbox($mailbox, $uids, $uidvalid = null)
+    {
+        if (!isset($this->_data[$mailbox])) {
+            $this->_data[$mailbox] = array();
+        }
+
+        $this->_loadSliceMap($mailbox, $uidvalid);
+
+        foreach (array_keys(array_flip($this->_getCacheSlices($mailbox, $uids))) as $val) {
+            $this->_loadSlice($mailbox, $val);
+        }
+    }
+
+    /**
+     * Load a cache slice into memory.
+     *
+     * @param string $mailbox  The mailbox to load.
+     * @param integer $slice   The slice to load.
+     */
+    protected function _loadSlice($mailbox, $slice)
+    {
+        /* Get the unique cache identifier for this mailbox. */
+        $cache_id = $this->_getCID($mailbox, $slice);
+
+        if (!empty($this->_loaded[$cache_id])) {
+            return;
+        }
+        $this->_loaded[$cache_id] = true;
+
+        /* Attempt to grab data from the cache. */
+        if (($data = $this->_cacheOb->get($cache_id, $this->_params['lifetime'])) === false) {
+            return;
+        }
+
+        $data = Horde_Serialize::unserialize($data, SERIALIZE_BASIC);
+        if (!is_array($data)) {
+            return;
+        }
+
+        /* Remove old entries. */
+        $ptr = &$this->_slicemap[$mailbox];
+        if (isset($ptr['delete'][$slice])) {
+            $data = array_diff_key($data, $ptr['delete'][$slice]);
+            if ($this->_params['debug']) {
+                fwrite($this->_params['debug'], 'Horde_Imap_Client_Cache: Deleted messages from cache (mailbox: ' . $mailbox . '; UIDs: ' . implode(',', $ptr['delete'][$slice]) . ")\n");
+            }
+            unset($ptr['delete'][$slice]);
+
+            /* Check if slice has less than 5 entries. */
+            $save = array();
+            if ((count($data) < 5) &&
+                ($slice != intval($ptr['count'] / $this->_params['slicesize']))) {
+                $save = array_keys($data);
+                $ptr['slice'] = array_diff_key($ptr['slice'], $save);
+            }
+
+            if (!isset($this->_save[$mailbox])) {
+                $this->_save[$mailbox] = array();
+            }
+            if (!empty($save)) {
+                $this->_save[$mailbox] = array_merge($this->_save[$mailbox], $save);
+            }
+        }
+
+        $this->_data[$mailbox] += $data;
+    }
+
+    /**
+     * Given a list of UIDs, determine the slices that need to be loaded.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox  The mailbox.
+     * @param array $uids      A list of UIDs.
+     * @param boolean $set     Set the slice information in $_slicemap?
+     *
+     * @return array  UIDs as the keys, the slice number as the value.
+     */
+    protected function _getCacheSlices($mailbox, $uids, $set = false)
+    {
+        $this->_loadSliceMap($mailbox);
+
+        $lookup = array();
+        $ptr = &$this->_slicemap[$mailbox];
+        $slicesize = $this->_params['slicesize'];
+
+        if (!empty($uids)) {
+            if ($set) {
+                $pcount = &$ptr['count'];
+            } else {
+                $pcount = $ptr['count'];
+            }
+
+            foreach ($uids as $val) {
+                if (isset($ptr['slice'][$val])) {
+                    $slice = $ptr['slice'][$val];
+                } else {
+                    $slice = intval($pcount++ / $slicesize);
+                    if ($set) {
+                        $ptr['slice'][$val] = $slice;
+                    }
+                }
+                $lookup[$val] = $slice;
+            }
+        }
+
+        return $lookup;
+    }
+
+    /**
+     * Given a list of UIDs, unpacks the messages from stored cache data and
+     * returns the list of UIDs that exist in the cache.
+     *
+     * @param string $mailbox    The mailbox.
+     * @param array $uids        The list of UIDs to load.
+     * @param integer $uidvalid  The IMAP uidvalidity value of the mailbox.
+     */
+    protected function _loadUIDs($mailbox, $uids, $uidvalid)
+    {
+        $this->_loadMailbox($mailbox, $uids, $uidvalid);
+        if (empty($this->_data[$mailbox])) {
+            return;
+        }
+
+        $compress = $this->_params['compress'];
+        $ptr = &$this->_data[$mailbox]['data'];
+        $todelete = array();
+
+        foreach ($uids as $val) {
+            if (isset($ptr[$val]) && !is_array($ptr[$val])) {
+                $success = false;
+                if (!is_null($compress)) {
+                    $res = Horde_Serialize::unserialize($ptr[$val], array($compress, SERIALIZE_BASIC));
+                    if (!is_a($res, 'PEAR_Error')) {
+                        $ptr[$val] = $res;
+                        $success = true;
+                    }
+                }
+                if (!$success) {
+                    $todelete[] = $val;
+                }
+            }
+        }
+
+        if (!empty($todelete)) {
+            $this->deleteMsgs($mailbox, $todelete);
+        }
+    }
+
+    /**
+     * Load the slicemap for a given mailbox.  The slicemap contains
+     * the uidvalidity information, the UIDs->slice lookup table, and any
+     * metadata that needs to be saved for the mailbox.
+     *
+     * @param string $mailbox    The mailbox.
+     * @param integer $uidvalid  The IMAP uidvalidity value of the mailbox.
+     */
+    protected function _loadSliceMap($mailbox, $uidvalid = null)
+    {
+        if (!isset($this->_slicemap[$mailbox])) {
+            if (($data = $this->_cacheOb->get($this->_getCID($mailbox, 'slicemap'), $this->_params['lifetime'])) !== false) {
+                $slice = Horde_Serialize::unserialize($data, SERIALIZE_BASIC);
+                if (is_array($slice)) {
+                    $this->_slicemap[$mailbox] = $slice;
+                }
+            }
+        }
+
+        if (isset($this->_slicemap[$mailbox])) {
+            $ptr = &$this->_slicemap[$mailbox]['data']['uidvalid'];
+            if (is_null($ptr)) {
+                $ptr = $uidvalid;
+            } elseif (!is_null($uidvalid) && ($ptr != $uidvalid)) {
+                $this->deleteMailbox($mailbox);
+                throw new Horde_Imap_Client_Exception('UIDs have been invalidated', Horde_Imap_Client_Exception::CACHEUIDINVALID);
+            }
+        } else {
+            $this->_slicemap[$mailbox] = array(
+                // Tracking count for purposes of determining slices
+                'count' => 0,
+                // Metadata storage
+                // By default includes UIDVALIDITY of mailbox.
+                'data' => array('uidvalid' => $uidvalid),
+                // UIDs to delete
+                'delete' => array(),
+                // The slice list.
+                'slice' => array()
+            );
+        }
+    }
+}
diff --git a/framework/Imap_Client/lib/Horde/Imap/Client/Cclient-pop3.php b/framework/Imap_Client/lib/Horde/Imap/Client/Cclient-pop3.php
new file mode 100644 (file)
index 0000000..70f1c4a
--- /dev/null
@@ -0,0 +1,439 @@
+<?php
+
+require_once dirname(__FILE__) . '/Cclient.php';
+
+/**
+ * Horde_Imap_Client_Cclient_pop3 provides an interface to a POP3 server (RFC
+ * 1939) via the PHP imap (c-client) module.  This driver is an abstraction
+ * layer allowing POP3 commands to be used based on its IMAP equivalents.
+ *
+ * PHP IMAP module: http://www.php.net/imap
+ *
+ * No additional paramaters from those defined in Horde_Imap_Client_Cclient.
+ *
+ * Copyright 2008 The Horde Project (http://www.horde.org/)
+ *
+ * $Horde: framework/Imap_Client/lib/Horde/Imap/Client/Cclient-pop3.php,v 1.6 2008/10/09 21:06:22 slusarz Exp $
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author   Michael Slusarz <slusarz@curecanti.org>
+ * @category Horde
+ * @package  Horde_Imap_Client
+ */
+class Horde_Imap_Client_Cclient_pop3 extends Horde_Imap_Client_Cclient
+{
+    /**
+     * Constructs a new Horde_Imap_Client_Cclient object.
+     *
+     * @param array $params  A hash containing configuration parameters.
+     */
+    public function __construct($params)
+    {
+        $this->_service = 'pop3';
+        if (!isset($params['port'])) {
+            $params['port'] = ($params['secure'] == 'ssl') ? 995 : 110;
+        }
+        parent::__construct($params);
+    }
+
+    /**
+     * Get CAPABILITY info from the IMAP server.
+     * Throws a Horde_Imap_Client_Exception on error.
+     */
+    protected function _capability()
+    {
+        throw new Horde_Imap_Client_Exception('IMAP CAPABILITY command not supported on POP3 servers.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED);
+    }
+
+    /**
+     * Get the NAMESPACE information from the IMAP server.
+     * Throws a Horde_Imap_Client_Exception on error.
+     */
+    protected function _getNamespaces()
+    {
+        throw new Horde_Imap_Client_Exception('IMAP namespaces not supported on POP3 servers.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED);
+    }
+
+    /**
+     * Send ID information to the IMAP server (RFC 2971).
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param array $info  The information to send to the server.
+     */
+    protected function _sendID($info)
+    {
+        throw new Horde_Imap_Client_Exception('IMAP ID command not supported on POP3 servers.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED);
+    }
+
+    /**
+     * Return ID information from the IMAP server (RFC 2971).
+     * Throws a Horde_Imap_Client_Exception on error.
+     */
+    protected function _getID()
+    {
+        throw new Horde_Imap_Client_Exception('IMAP ID command not supported on POP3 servers.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED);
+    }
+
+    /**
+     * Sets the preferred language for server response messages (RFC 5255).
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param array $info  The preferred list of languages.
+     *
+     * @return string  The language accepted by the server, or null if the
+     *                 default language is used.
+     */
+    protected function _setLanguage($langs)
+    {
+        throw new Horde_Imap_Client_Exception('IMAP LANGUAGE extension not supported on POP3 servers.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED);
+    }
+
+    /**
+     * Gets the preferred language for server response messages (RFC 5255).
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param array $list  If true, return the list of available languages.
+     */
+    protected function _getLanguage($list)
+    {
+        throw new Horde_Imap_Client_Exception('IMAP LANGUAGE extension not supported on POP3 servers.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED);
+    }
+
+    /**
+     * Open a mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox  The mailbox to open (UTF7-IMAP).
+     * @param integer $mode    The access mode.
+     */
+    protected function _openMailbox($mailbox, $mode)
+    {
+        if (strcasecmp($mailbox, 'INBOX') !== 0) {
+            throw new Horde_Imap_Client_Exception('Mailboxes other than INBOX not supported on POP3 servers.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED);
+        }
+    }
+
+    /**
+     * Create a mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox  The mailbox to create (UTF7-IMAP).
+     */
+    protected function _createMailbox($mailbox)
+    {
+        throw new Horde_Imap_Client_Exception('Creating mailboxes not supported on POP3 servers.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED);
+    }
+
+    /**
+     * Delete a mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox  The mailbox to delete (UTF7-IMAP).
+     */
+    protected function _deleteMailbox($mailbox)
+    {
+        throw new Horde_Imap_Client_Exception('Deleting mailboxes not supported on POP3 servers.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED);
+    }
+
+    /**
+     * Rename a mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $old     The old mailbox name (UTF7-IMAP).
+     * @param string $new     The new mailbox name (UTF7-IMAP).
+     */
+    protected function _renameMailbox($old, $new)
+    {
+        throw new Horde_Imap_Client_Exception('Renaming mailboxes not supported on POP3 servers.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED);
+    }
+
+    /**
+     * Manage subscription status for a mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox     The mailbox to [un]subscribe to (UTF7-IMAP).
+     * @param boolean $subscribe  True to subscribe, false to unsubscribe.
+     */
+    protected function _subscribeMailbox($mailbox, $subscribe)
+    {
+        throw new Horde_Imap_Client_Exception('Mailboxes other than INBOX not supported on POP3 servers.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED);
+    }
+
+    /**
+     * Unsubscribe to a mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox  The mailbox to unsubscribe to (UTF7-IMAP).
+     */
+    protected function _unsubscribeMailbox($mailbox)
+    {
+        throw new Horde_Imap_Client_Exception('Mailboxes other than INBOX not supported on POP3 servers.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED);
+    }
+
+    /**
+     * Obtain a list of mailboxes matching a pattern.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $pattern  The mailbox search pattern.
+     * @param integer $mode    Which mailboxes to return.
+     * @param array $options   Additional options.
+     *
+     * @return array  See Horde_Imap_Client_Base::listMailboxes().
+     */
+    protected function _listMailboxes($pattern, $mode, $options)
+    {
+        $tmp = array('mailbox' => 'INBOX');
+
+        if (!empty($options['attributes'])) {
+            $tmp['attributes'] = array();
+        }
+        if (!empty($options['delimiter'])) {
+            $tmp['delimiter'] = '';
+        }
+
+        return array('INBOX' => $tmp);
+    }
+
+    /**
+     * Obtain status information for a mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox  The mailbox to query (UTF7-IMAP).
+     * @param string $flags    A bitmask of information requested from the
+     *                         server.
+     *
+     * @return array  See Horde_Imap_Client_Base::status().
+     */
+    protected function _status($mailbox, $flags)
+    {
+        if (strcasecmp($mailbox, 'INBOX') !== 0) {
+            throw new Horde_Imap_Client_Exception('Mailboxes other than INBOX not supported on POP3 servers.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED);
+        }
+
+        // This driver only supports the base flags given by c-client.
+        if (($flags & Horde_Imap_Client::STATUS_FIRSTUNSEEN) ||
+            ($flags & Horde_Imap_Client::STATUS_FLAGS) ||
+            ($flags & Horde_Imap_Client::STATUS_PERMFLAGS)) {
+            throw new Horde_Imap_Client_Exception('Improper status request on POP3 server.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED);
+        }
+
+        return parent::_status($mailbox, $flags);
+    }
+
+    /**
+     * Search a mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param object $query   The search string.
+     * @param array $options  Additional options.
+     *
+     * @return array  An array of UIDs (default) or an array of message
+     *                sequence numbers (if 'sequence' is true).
+     */
+    protected function _search($query, $options)
+    {
+        // POP 3 supports c-client search criteria only.
+        $search_query = $query->build();
+
+        /* If more than 1 sort criteria given, or if SORT_REVERSE is given
+         * as a sort criteria, or search query uses IMAP4 criteria, use the
+         * Socket client instead. */
+        if ($search_query['imap4'] ||
+            (!empty($options['sort']) &&
+             ((count($options['sort']) > 1) ||
+             in_array(self::SORT_REVERSE, $options['sort'])))) {
+            throw new Horde_Imap_Client_Exception('Unsupported search criteria on POP3 server.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED);
+        }
+
+        return parent::_search($query, $options);
+    }
+
+   /**
+     * Set the comparator to use for searching/sorting (RFC 5255).
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $comparator  The comparator string (see RFC 4790 [3.1] -
+     *                            "collation-id" - for format). The reserved
+     *                            string 'default' can be used to select
+     *                            the default comparator.
+     */
+    protected function _setComparator($comparator)
+    {
+        throw new Horde_Imap_Client_Exception('Search comparators not supported on POP3 server.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED);
+    }
+
+    /**
+     * Get the comparator used for searching/sorting (RFC 5255).
+     * Throws a Horde_Imap_Client_Exception on error.
+     */
+    protected function _getComparator()
+    {
+        throw new Horde_Imap_Client_Exception('Search comparators not supported on POP3 server.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED);
+    }
+
+    /**
+     * Thread sort a given list of messages (RFC 5256).
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param array $options  Additional options.
+     *
+     * @return array  See Horde_Imap_Client_Base::thread().
+     */
+    protected function _thread($options)
+    {
+        /* This driver only supports Horde_Imap_Client::THREAD_REFERENCES
+         * and does not support defining search criteria. */
+        if (!empty($options['search']) ||
+            (!empty($options['criteria']) &&
+             $options['criteria'] != self::THREAD_REFERENCES)) {
+            throw new Horde_Imap_Client_Exception('Unsupported threading criteria on POP3 server.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED);
+        }
+
+        return parent::_thread($options);
+    }
+
+    /**
+     * Append a message to the mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param array $mailbox   The mailboxes to append the messages to
+     *                         (UTF7-IMAP).
+     * @param array $data      The message data.
+     * @param array $options   Additional options.
+     */
+    protected function _append($mailbox, $data, $options)
+    {
+        throw new Horde_Imap_Client_Exception('Appending messages not supported on POP3 servers.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED);
+    }
+
+    /**
+     * Fetch message data.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param array $criteria  The fetch criteria.
+     * @param array $options   Additional options.
+     *
+     * @return array  See self::fetch().
+     */
+    protected function _fetch($criteria, $options)
+    {
+        // No support for FETCH_MIMEHEADER or FETCH_HEADERS
+        $nosupport = array(self::FETCH_MIMEHEADER, self::FETCH_HEADERS);
+
+        reset($criteria);
+        while (list($val,) = each($criteria)) {
+            if (in_array($val, $nosupport)) {
+                throw new Horde_Imap_Client_Exception('Fetch criteria provided not supported on POP3 servers.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED);
+            }
+        }
+
+        return parent::_fetch($criteria, $options);
+    }
+
+    /**
+     * Store message flag data.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param array $options  Additional options.
+     */
+    protected function _store($options)
+    {
+        throw new Horde_Imap_Client_Exception('Flagging messages not supported on POP3 servers.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED);
+    }
+
+    /**
+     * Copy messages to another mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $dest    The destination mailbox (UTF7-IMAP).
+     * @param array $options  Additional options.
+     */
+    protected function _copy($dest, $options)
+    {
+        throw new Horde_Imap_Client_Exception('Copying messages not supported on POP3 servers.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED);
+    }
+
+    /**
+     * Set quota limits.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $root    The quota root (UTF7-IMAP).
+     * @param array $options  Additional options.
+     */
+    protected function _setQuota($root, $options)
+    {
+        throw new Horde_Imap_Client_Exception('IMAP quotas not supported on POP3 servers.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED);
+    }
+
+    /**
+     * Get quota limits.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $root  The quota root (UTF7-IMAP).
+     */
+    protected function _getQuota($root)
+    {
+        throw new Horde_Imap_Client_Exception('IMAP quotas not supported on POP3 servers.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED);
+    }
+
+    /**
+     * Get quota limits for a mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox  A mailbox (UTF7-IMAP).
+     */
+    protected function _getQuotaRoot($mailbox)
+    {
+        throw new Horde_Imap_Client_Exception('IMAP quotas not supported on POP3 servers.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED);
+    }
+
+    /**
+     * Set ACL rights for a given mailbox/identifier.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox     A mailbox (UTF7-IMAP).
+     * @param string $identifier  The identifier to alter (UTF7-IMAP).
+     * @param array $options      Additional options.
+     */
+    protected function _setACL($mailbox, $identifier, $options)
+    {
+        throw new Horde_Imap_Client_Exception('IMAP ACLs not supported on POP3 servers.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED);
+    }
+
+    /**
+     * Get ACL rights for a given mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox  A mailbox (UTF7-IMAP).
+     */
+    protected function _getACL($mailbox)
+    {
+        throw new Horde_Imap_Client_Exception('IMAP ACLs not supported on POP3 servers.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED);
+    }
+
+    /**
+     * Get ACL rights for a given mailbox/identifier.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox     A mailbox (UTF7-IMAP).
+     * @param string $identifier  The identifier (UTF7-IMAP).
+     */
+    protected function _listACLRights($mailbox, $identifier)
+    {
+        throw new Horde_Imap_Client_Exception('IMAP ACLs not supported on POP3 servers.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED);
+    }
+
+    /**
+     * Get the ACL rights for the current user for a given mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox  A mailbox (UTF7-IMAP).
+     */
+    protected function _getMyACLRights($mailbox)
+    {
+        throw new Horde_Imap_Client_Exception('IMAP ACLs not supported on POP3 servers.', Horde_Imap_Client_Exception::POP3_NOTSUPPORTED);
+    }
+
+}
diff --git a/framework/Imap_Client/lib/Horde/Imap/Client/Cclient.php b/framework/Imap_Client/lib/Horde/Imap/Client/Cclient.php
new file mode 100644 (file)
index 0000000..e01a5d0
--- /dev/null
@@ -0,0 +1,1655 @@
+<?php
+/**
+ * Horde_Imap_Client_Cclient provides an interface to an IMAP server using the
+ * PHP imap (c-client) module.
+ *
+ * PHP IMAP module: http://www.php.net/imap
+ *
+ * Optional Parameters:
+ *   retries - (integer) Connection retries.
+ *             DEFAULT: 3
+ *   timeout - (array) Timeout value (in seconds) for various actions. Unlinke
+ *             the base Horde_Imap_Client class, this driver supports an
+ *             array of timeout entries as follows:
+ *               'open', 'read', 'write', 'close'
+ *             If timeout is a string, the same timeout will be used for all
+ *             values.
+ *             DEFAULT: C-client default values
+ *   validate_cert - (boolean)  If using tls or ssl connections, validate the
+ *                   certificate?
+ *                   DEFAULT: Don't validate
+ *
+ * Copyright 2008 The Horde Project (http://www.horde.org/)
+ *
+ * $Horde: framework/Imap_Client/lib/Horde/Imap/Client/Cclient.php,v 1.58 2008/10/28 21:54:40 slusarz Exp $
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author   Michael Slusarz <slusarz@curecanti.org>
+ * @category Horde
+ * @package  Horde_Imap_Client
+ */
+class Horde_Imap_Client_Cclient extends Horde_Imap_Client_Base
+{
+    /**
+     * The Horde_Imap_Client_Socket object needed to obtain server info.
+     *
+     * @var Horde_Imap_Client_Socket
+     */
+    protected $_socket;
+
+    /**
+     * The IMAP resource stream.
+     *
+     * @var resource
+     */
+    protected $_stream = null;
+
+    /**
+     * The IMAP c-client connection string.
+     *
+     * @var string
+     */
+    protected $_cstring;
+
+    /**
+     * The service to connect to via c-client
+     *
+     * @var string
+     */
+    protected $_service = 'imap';
+
+    /**
+     * The IMAP flags supported in this driver.
+     *
+     * @var array
+     */
+    protected $_supportedFlags = array(
+        'seen', 'answered', 'flagged', 'deleted', 'recent', 'draft'
+    );
+
+    /**
+     * The c-client code -> MIME type conversion table.
+     *
+     * @var array
+     */
+    protected $_mimeTypes = array(
+        TYPETEXT => 'text',
+        TYPEMULTIPART => 'multipart',
+        TYPEMESSAGE => 'message',
+        TYPEAPPLICATION => 'application',
+        TYPEAUDIO => 'audio',
+        TYPEIMAGE => 'image',
+        TYPEVIDEO => 'video',
+        TYPEMODEL => 'model',
+        TYPEOTHER => 'other'
+    );
+
+    /**
+     * The c-client code -> MIME encodings conversion table.
+     *
+     * @var array
+     */
+    protected $_mimeEncodings = array(
+        ENC7BIT => '7bit',
+        ENC8BIT => '8bit',
+        ENCBINARY => 'binary',
+        ENCBASE64 => 'base64',
+        ENCQUOTEDPRINTABLE => 'quoted-printable',
+        ENCOTHER => 'unknown'
+    );
+
+    /**
+     * Constructs a new Horde_Imap_Client_Cclient object.
+     *
+     * @param array $params  A hash containing configuration parameters.
+     */
+    public function __construct($params)
+    {
+        if (!isset($params['retries'])) {
+            $params['retries'] = 3;
+        }
+        parent::__construct($params);
+    }
+
+    /**
+     * Do cleanup prior to serialization and provide a list of variables
+     * to serialize.
+     */
+    function __sleep()
+    {
+        $this->logout();
+        parent::__sleep();
+        return array_diff(array_keys(get_class_vars(__CLASS__)), array('encryptKey'));
+    }
+
+    /**
+     * Get CAPABILITY info from the IMAP server.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @return array  The capability array.
+     */
+    protected function _capability()
+    {
+        $cap = $this->_getSocket()->capability();
+
+        /* No need to support these extensions here - the wrapping required
+         * to make this work is probably just as resource intensive as what
+         * we are trying to avoid. */
+        unset($cap['CONDSTORE'], $cap['QRESYNC']);
+
+        return $cap;
+    }
+
+    /**
+     * Send a NOOP command.
+     * Throws a Horde_Imap_Client_Exception on error.
+     */
+    protected function _noop()
+    {
+        // Already guaranteed to be logged in here.
+
+        $old_error = error_reporting(0);
+        $res = imap_ping($this->_stream);
+        error_reporting($old_error);
+
+        if ($res === false) {
+            throw new Horde_Imap_Client_Exception('Received error from IMAP server when sending a NOOP command: ' . imap_last_error());
+        }
+    }
+
+    /**
+     * Get the NAMESPACE information from the IMAP server.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @return array  An array of namespace information.
+     */
+    protected function _getNamespaces()
+    {
+        return $this->_getSocket()->getNamespaces();
+    }
+
+    /**
+     * Return a list of alerts that MUST be presented to the user.
+     *
+     * @return array  An array of alert messages.
+     */
+    public function alerts()
+    {
+        // TODO: check for [ALERT]?
+        return imap_alerts();
+    }
+
+    /**
+     * Login to the IMAP server.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @return boolean  Return true if global login tasks should be run.
+     */
+    protected function _login()
+    {
+        $i = -1;
+        $res = false;
+
+        if (!empty($this->_params['secure']) && !extension_loaded('openssl')) {
+            throw new Horde_Imap_Client_Exception('Secure connections require the PHP openssl extension: http://php.net/openssl.');
+        }
+
+        $mask = ($this->_service == 'pop3') ? 0 : OP_HALFOPEN;
+
+        $old_error = error_reporting(0);
+        if (version_compare(PHP_VERSION, '5.2.1') != -1) {
+            $res = imap_open($this->_connString(), $this->_params['username'], $this->_params['password'], $mask, $this->_params['retries']);
+        } else {
+            while (($res === false) &&
+                   !strstr(strtolower(imap_last_error()), 'login failure') &&
+                   (++$i < $this->_params['retries'])) {
+                if ($i != 0) {
+                    sleep(1);
+                }
+                $res = imap_open($this->_connString(), $this->_params['username'], $this->_params['password'], $mask);
+            }
+        }
+        error_reporting($old_error);
+
+        if ($res === false) {
+            throw new Horde_Imap_Client_Exception('Could not authenticate to IMAP server: ' . imap_last_error());
+        }
+
+        $this->_stream = $res;
+        $this->_isSecure = !empty($this->_params['secure']);
+
+        $this->setLanguage();
+
+        if (!empty($this->_params['timeout'])) {
+            $timeout = array(
+                'open' => IMAP_OPENTIMEOUT,
+                'read' => IMAP_READTIMEOUT,
+                'write' => IMAP_WRITETIMEOUT,
+                'close' => IMAP_CLOSETIMEOUT
+            );
+
+            foreach ($timeout as $key => $val) {
+                if (isset($this->_params['timeout'][$key])) {
+                    imap_timeout($val, $this->_params['timeout'][$key]);
+                }
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Log out of the IMAP session.
+     */
+    protected function _logout()
+    {
+        if (!is_null($this->_stream)) {
+            imap_close($this->_stream);
+            $this->_stream = null;
+            if (isset($this->_socket)) {
+                $this->_socket->logout();
+            }
+        }
+    }
+
+    /**
+     * Send ID information to the IMAP server (RFC 2971).
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param array $info  The information to send to the server.
+     */
+    protected function _sendID($info)
+    {
+        $this->_getSocket()->sendID($info);
+    }
+
+    /**
+     * Return ID information from the IMAP server (RFC 2971).
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @return array  An array of information returned, with the keys as the
+     *                'field' and the values as the 'value'.
+     */
+    protected function _getID()
+    {
+        return $this->_getSocket()->getID();
+    }
+
+    /**
+     * Sets the preferred language for server response messages (RFC 5255).
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param array $info  The preferred list of languages.
+     *
+     * @return string  The language accepted by the server, or null if the
+     *                 default language is used.
+     */
+    protected function _setLanguage($langs)
+    {
+        return $this->_getSocket()->setLanguage($langs);
+    }
+
+    /**
+     * Gets the preferred language for server response messages (RFC 5255).
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param array $list  If true, return the list of available languages.
+     *
+     * @return mixed  If $list is true, the list of languages available on the
+     *                server (may be empty). If false, the language used by
+     *                the server, or null if the default language is used.
+     */
+    protected function _getLanguage($list)
+    {
+        return $this->_getSocket()->getLanguage($list);
+    }
+
+    /**
+     * Open a mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox  The mailbox to open (UTF7-IMAP).
+     * @param integer $mode    The access mode.
+     */
+    protected function _openMailbox($mailbox, $mode)
+    {
+        $this->login();
+        $flag = ($mode == self::OPEN_READONLY) ? OP_READONLY : 0;
+
+        $old_error = error_reporting(0);
+        if (version_compare(PHP_VERSION, '5.2.1') != -1) {
+            $res = imap_reopen($this->_stream, $this->_connString($mailbox), $flag, $this->_params['retries']);
+        } else {
+            $res = imap_reopen($this->_stream, $this->_connString($mailbox), $flag);
+        }
+        error_reporting($old_error);
+
+        if ($res === false) {
+            throw new Horde_Imap_Client_Exception('Could not open mailbox "' . $mailbox . '": ' . imap_last_error());
+        }
+    }
+
+    /**
+     * Create a mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox  The mailbox to create (UTF7-IMAP).
+     */
+    protected function _createMailbox($mailbox)
+    {
+        $this->login();
+
+        $old_error = error_reporting(0);
+        $res = imap_createmailbox($this->_stream, $this->_connString($mailbox));
+        error_reporting($old_error);
+
+        if ($res === false) {
+            throw new Horde_Imap_Client_Exception('Could not create mailbox "' . $mailbox . '": ' . imap_last_error());
+        }
+    }
+
+    /**
+     * Delete a mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox  The mailbox to delete (UTF7-IMAP).
+     */
+    protected function _deleteMailbox($mailbox)
+    {
+        $this->login();
+
+        $old_error = error_reporting(0);
+        $res = imap_deletemailbox($this->_stream, $this->_connString($mailbox));
+        error_reporting($old_error);
+
+        if ($res === false) {
+            throw new Horde_Imap_Client_Exception('Could not delete mailbox "' . $mailbox . '": ' . imap_last_error());
+        }
+    }
+
+    /**
+     * Rename a mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $old     The old mailbox name (UTF7-IMAP).
+     * @param string $new     The new mailbox name (UTF7-IMAP).
+     */
+    protected function _renameMailbox($old, $new)
+    {
+        $this->login();
+
+        $old_error = error_reporting(0);
+        $res = imap_renamemailbox($this->_stream, $this->_connString($old), $this->_connString($new));
+        error_reporting($old_error);
+
+        if ($res === false) {
+            throw new Horde_Imap_Client_Exception('Could not rename mailbox "' . $old . '": ' . imap_last_error());
+        }
+    }
+
+    /**
+     * Manage subscription status for a mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox     The mailbox to [un]subscribe to (UTF7-IMAP).
+     * @param boolean $subscribe  True to subscribe, false to unsubscribe.
+     */
+    protected function _subscribeMailbox($mailbox, $subscribe)
+    {
+        $this->login();
+
+        $old_error = error_reporting(0);
+        if ($subscribe) {
+            $res = imap_subscribe($this->_stream, $this->_connString($mailbox));
+        } else {
+            $res = imap_unsubscribe($this->_stream, $this->_connString($mailbox));
+        }
+        error_reporting($old_error);
+
+        if ($res === false) {
+            throw new Horde_Imap_Client_Exception('Could not ' . ($subscribe ? 'subscribe' : 'unsubscribe') . ' to mailbox "' . $mailbox . '": ' . imap_last_error());
+        }
+    }
+
+    /**
+     * Obtain a list of mailboxes matching a pattern.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $pattern  The mailbox search pattern.
+     * @param integer $mode    Which mailboxes to return.
+     * @param array $options   Additional options.
+     * <pre>
+     * For the 'attributes' option, this driver will return only these
+     * attributes:
+     *   '\noinferiors', '\noselect', '\marked', '\unmarked', '\referral',
+     *   '\haschildren', '\hasnochildren'
+     * </pre>
+     *
+     * @return array  See Horde_Imap_Client_Base::listMailboxes().
+     */
+    protected function _listMailboxes($pattern, $mode, $options)
+    {
+        $this->login();
+
+        switch ($mode) {
+        case self::MBOX_ALL:
+            if (!empty($options['flat'])) {
+                $mboxes = $this->_getMailboxList($pattern, $mode);
+                return (empty($options['utf8'])) ? $mboxes : array_map(array('Horde_Imap_Client_Utf7imap', 'Utf7ImapToUtf8'), $mboxes);
+            }
+            $check = false;
+            break;
+
+        case self::MBOX_SUBSCRIBED:
+        case self::MBOX_UNSUBSCRIBED:
+            $sub = $this->_getMailboxList($pattern, self::MBOX_SUBSCRIBED);
+            if (!empty($options['flat'])) {
+                if (!empty($options['utf8'])) {
+                    $sub = array_map(array('Horde_Imap_Client_Utf7imap', 'Utf7ImapToUtf8'), $sub);
+                }
+                if ($mode == self::MBOX_SUBSCRIBED) {
+                    return $sub;
+                }
+
+                $mboxes = $this->_getMailboxList($pattern, self::MBOX_ALL);
+                if (!empty($options['utf8'])) {
+                    $sub = array_map(array('Horde_Imap_Client_Utf7imap', 'Utf7ImapToUtf8'), $sub);
+                }
+                return array_values(array_diff($mboxes, $sub));
+            }
+            $sub = array_flip($sub);
+            $check = true;
+        }
+
+        $attr = array(
+            LATT_NOINFERIORS => '\\noinferiors',
+            LATT_NOSELECT => '\\noselect',
+            LATT_MARKED => '\\marked',
+            LATT_UNMARKED => '\\unmarked',
+            LATT_REFERRAL => '\\referral',
+            LATT_HASCHILDREN => '\\haschildren',
+            LATT_HASNOCHILDREN => '\\hasnochildren'
+        );
+
+        $old_error = error_reporting(0);
+        $res = imap_getmailboxes($this->_stream, $this->_connString(), $pattern);
+        error_reporting($old_error);
+
+        $mboxes = array();
+        while (list(,$val) = each($res)) {
+            $mbox = substr($val->name, strpos($val->name, '}') + 1);
+
+            if ($check &&
+                ((($mode == self::MBOX_UNSUBSCRIBED) &&
+                  isset($sub[$mbox])) ||
+                 (($mode == self::MBOX_SUBSCRIBED) &&
+                  !isset($sub[$mbox])))) {
+                continue;
+            }
+
+            if (!empty($options['utf8'])) {
+                $mbox = Horde_Imap_Client_Utf7imap::Utf7ImapToUtf8($mbox);
+            }
+
+            $tmp = array('mailbox' => $mbox);
+            if (!empty($options['attributes'])) {
+                $tmp['attributes'] = array();
+                foreach ($attr as $k => $a) {
+                    if ($val->attributes & $k) {
+                        $tmp['attributes'][] = $a;
+                    }
+                }
+            }
+            if (!empty($options['delimiter'])) {
+                $tmp['delimiter'] = $val->delimiter;
+            }
+            $mboxes[$mbox] = $tmp;
+        }
+
+        return $mboxes;
+    }
+
+    /**
+     * Obtain a list of mailboxes matching a pattern.
+     *
+     * @param string $pattern  The mailbox search pattern.
+     * @param integer $mode    Which mailboxes to return.  Either
+     *                         Horde_Imap_Client::MBOX_SUBSCRIBED or
+     *                         Horde_Imap_Client::MBOX_ALL.
+     *
+     * @return array  A list of mailboxes in UTF7-IMAP format.
+     */
+    protected function _getMailboxList($pattern, $mode)
+    {
+        $mboxes = array();
+
+        $old_error = error_reporting(0);
+        if ($mode != self::MBOX_ALL) {
+            $res = imap_list($this->_stream, $this->_connString(), $pattern);
+        } else {
+            $res = imap_lsub($this->_stream, $this->_connString(), $pattern);
+        }
+        error_reporting($old_error);
+
+        if (is_array($res)) {
+            while (list(,$val) = each($res)) {
+                $mboxes[] = substr($val, strpos($val, '}') + 1);
+            }
+        }
+
+        return $mboxes;
+    }
+
+    /**
+     * Obtain status information for a mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox  The mailbox to query (UTF7-IMAP).
+     * @param string $flags    A bitmask of information requested from the
+     *                         server.
+     *
+     * @return array  See Horde_Imap_Client_Base::status().
+     */
+    protected function _status($mailbox, $flags)
+    {
+        $this->login();
+
+        /* If FLAGS/PERMFLAGS/HIGHESTMODSEQ/UIDNOTSTICKY are needed, we must
+         * use the Socket driver. */
+        if (($flags & self::STATUS_FLAGS) ||
+            ($flags & self::STATUS_PERMFLAGS) ||
+            ($flags & self::STATUS_HIGHESTMODSEQ) ||
+            ($flags & self::STATUS_UIDNOTSTICKY)) {
+            return $this->_getSocket()->status($mailbox, $flags);
+        }
+
+        $items = array(
+            self::STATUS_MESSAGES => SA_MESSAGES,
+            self::STATUS_RECENT => SA_RECENT,
+            self::STATUS_UIDNEXT => SA_UIDNEXT,
+            self::STATUS_UIDVALIDITY => SA_UIDVALIDITY,
+            self::STATUS_UNSEEN => SA_UNSEEN
+        );
+
+        $c_flag = 0;
+        $res = null;
+
+        foreach ($items as $key => $val) {
+            if ($key & $flags) {
+                $c_flag |= $val;
+            }
+        }
+
+        if (!empty($c_flag)) {
+            $res = imap_status($this->_stream, $this->_connString($mailbox), $c_flag);
+            if (!is_object($res)) {
+                $res = null;
+            }
+        }
+
+        if ($flags & self::STATUS_FIRSTUNSEEN) {
+            $search_query = new Horde_Imap_Client_Search_Query();
+            $search_query->flag('\\unseen', false);
+            $search = $this->search($mailbox, $search_query, array('results' => array(self::SORT_RESULTS_MIN), 'sequence' => true));
+
+            if (is_null($res)) {
+                return array('firstunseen' => $search['min']);
+            }
+            $res->firstunseen = reset($search);
+        }
+
+        if (is_null($res)) {
+            return array();
+        } else {
+            unset($res->flags);
+            return (array)$res;
+        }
+    }
+
+    /**
+     * Append message(s) to a mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox  The mailbox to append the message(s) to
+     *                         (UTF7-IMAP).
+     * @param array $data      The message data.
+     * @param array $options   Additional options.
+     *
+     * @return mixed  Returns true.
+     */
+    protected function _append($mailbox, $data, $options)
+    {
+        $this->login();
+
+        /* This driver does not support flags other than those defined in the
+         * IMAP4 spec, and does not support 'internaldate'. If either of these
+         * conditions exist, use the Socket driver instead. */
+        while (list(,$val) = each($data)) {
+            if (isset($val['internaldate']) ||
+                (!empty($val['flags']) &&
+                 $this->_nonSupportedFlags($val['flags']))) {
+                return $this->_getSocket()->append($mailbox, $data);
+            }
+        }
+
+        while (list(,$val) = each($data)) {
+            $old_error = error_reporting(0);
+            $text = is_resource($val['data']) ? stream_get_contents($val['data']) : $val['data'];
+            $res = imap_append($this->_stream, $this->_connString($mailbox), $this->removeBareNewlines($text), empty($val['flags']) ? null : implode(' ', $val['flags']));
+            error_reporting($old_error);
+
+            if ($res === false) {
+                if (!empty($options['create'])) {
+                    $this->createMailbox($mailbox);
+                    unset($options['create']);
+                    return $this->_append($mailbox, $data, $options);
+                }
+                throw new Horde_Imap_Client_Exception('Could not append message to IMAP server: ' . imap_last_error());
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Request a checkpoint of the currently selected mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     */
+    protected function _check()
+    {
+        // Already guaranteed to be logged in here.
+
+        $old_error = error_reporting(0);
+        $res = imap_check($this->_stream);
+        error_reporting($old_error);
+
+        if ($res === false) {
+            throw new Horde_Imap_Client_Exception('Received error from IMAP server when sending a CHECK command: ' . imap_last_error());
+        }
+    }
+
+    /**
+     * Close the connection to the currently selected mailbox, optionally
+     * expunging all deleted messages (RFC 3501 [6.4.2]).
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param array $options  Additional options.
+     */
+    protected function _close($options)
+    {
+        if (!empty($options['expunge'])) {
+            $this->expunge($this->_selected);
+        }
+        $this->openMailbox($this->_selected, self::OPEN_READONLY);
+    }
+
+    /**
+     * Expunge deleted messages from the given mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param array $options  Additional options.
+     */
+    protected function _expunge($options)
+    {
+        // Already guaranteed to be logged in here.
+
+        if (empty($options['ids'])) {
+            $old_error = error_reporting(0);
+            imap_expunge($this->_stream);
+            error_reporting($old_error);
+            return;
+        }
+
+        $use_seq = !empty($options['sequence']);
+
+        // Need to temporarily unflag all messages marked as deleted but not
+        // a part of requested UIDs to delete.
+        $search_query = new Horde_Imap_Client_Search_Query();
+        $search_query->flag('\\deleted');
+        $ids = $this->search($this->_selected, $search_query, array('sequence' => $use_seq));
+        if (!empty($ids['match'])) {
+            $unflag = array_diff($ids['match'], $options['ids']);
+            if (!empty($unflag)) {
+                $this->store($this->_selected, array('ids' => $unflag, 'remove' => array('\\deleted'), 'sequence' => $use_seq));
+            }
+
+            /* If we are using a cache, we need to get the list of
+             * messages that will be expunged. */
+            if ($this->_initCacheOb()) {
+                if ($use_seq) {
+                    $res = $this->search($this->_selected, $search_query);
+                    $expunged = $res['match'];
+                } else {
+                    $expunged = array_intersect($ids['match'], $options['ids']);
+                }
+
+                if (!empty($expunged)) {
+                    $this->_cacheOb->deleteMsgs($this->_selected, $expunged);
+                }
+            }
+        }
+
+        $old_error = error_reporting(0);
+        imap_expunge($this->_stream);
+        error_reporting($old_error);
+
+        if (!empty($unflag)) {
+            $this->store($this->_selected, array('add' => array('\\deleted'), 'ids' => $unflag, 'sequence' => $use_seq));
+        }
+    }
+
+    /**
+     * Search a mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param object $query   The search string.
+     * @param array $options  Additional options. The '_query' key contains
+     *                        the value of $query->build().
+     *
+     * @return array  An array of UIDs (default) or an array of message
+     *                sequence numbers (if 'sequence' is true).
+     */
+    protected function _search($query, $options)
+    {
+        // Already guaranteed to be logged in here.
+
+        /* If more than 1 sort criteria given, or if SORT_REVERSE is given
+         * as a sort criteria, or search query uses IMAP4 criteria, use the
+         * Socket client instead. */
+        if ($options['_query']['imap4'] ||
+            (!empty($options['sort']) &&
+             ((count($options['sort']) > 1) ||
+             in_array(self::SORT_REVERSE, $options['sort'])))) {
+            return $this->_getSocket()->search($this->_selected, $query, $options);
+        }
+
+        $old_error = error_reporting(0);
+        if (empty($options['sort'])) {
+            $res = imap_search($this->_stream, $options['_query']['query'], empty($options['sequence']) ? SE_UID : 0, $options['_query']['charset']);
+        } else {
+            $sort_criteria = array(
+                self::SORT_ARRIVAL => SORTARRIVAL,
+                self::SORT_CC => SORTCC,
+                self::SORT_DATE => SORTDATE,
+                self::SORT_FROM => SORTFROM,
+                self::SORT_SIZE => SORTSIZE,
+                self::SORT_SUBJECT => SORTSUBJECT,
+                self::SORT_TO => SORTTO
+            );
+
+            $res = imap_sort($this->_stream, $sort_criteria[reset($options['sort'])], 0, empty($options['sequence']) ? SE_UID : 0, $options['_query']['query'], $options['_query']['charset']);
+        }
+        $res = ($res === false) ? array() : $res;
+        error_reporting($old_error);
+
+        $ret = array();
+        foreach ($options['results'] as $val) {
+            switch ($val) {
+            case self::SORT_RESULTS_COUNT:
+                $ret['count'] = count($res);
+                break;
+
+            case self::SORT_RESULTS_MATCH:
+                $ret[empty($options['sort']) ? 'match' : 'sort'] = $res;
+                break;
+
+            case self::SORT_RESULTS_MAX:
+                $ret['max'] = empty($res) ? null : max($res);
+                break;
+
+            case self::SORT_RESULTS_MIN:
+                $ret['min'] = empty($res) ? null : min($res);
+                break;
+            }
+        }
+
+        return $ret;
+    }
+
+   /**
+     * Set the comparator to use for searching/sorting (RFC 5255).
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $comparator  The comparator string (see RFC 4790 [3.1] -
+     *                            "collation-id" - for format). The reserved
+     *                            string 'default' can be used to select
+     *                            the default comparator.
+     */
+    protected function _setComparator($comparator)
+    {
+        return $this->_getSocket()->setComparator($comparator);
+    }
+
+    /**
+     * Get the comparator used for searching/sorting (RFC 5255).
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @return mixed  Null if the default comparator is being used, or an
+     *                array of comparator information (see RFC 5255 [4.8]).
+     */
+    protected function _getComparator()
+    {
+        return $this->_getSocket()->getComparator();
+    }
+
+    /**
+     * Thread sort a given list of messages (RFC 5256).
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param array $options  Additional options.
+     *
+     * @return array  See Horde_Imap_Client_Base::_thread().
+     */
+    protected function _thread($options)
+    {
+        // Already guaranteed to be logged in here
+
+        /* This driver only supports Horde_Imap_Client::THREAD_REFERENCES
+         * and does not support defining search criteria. */
+        if (!empty($options['search']) ||
+            (!empty($options['criteria']) &&
+             $options['criteria'] != self::THREAD_REFERENCES)) {
+            return $this->_getSocket()->thread($this->_selected, $options);
+        }
+
+        $use_seq = !empty($options['sequence']);
+
+        $old_error = error_reporting(0);
+        $ob = imap_thread($this->_stream, $use_seq ? 0 : SE_UID);
+        error_reporting($old_error);
+
+        if (empty($ob)) {
+            return array();
+        }
+
+        $container = $container_base = $last_index = $thread_base = $thread_base_idx = $uid = null;
+        $lookup = $ret = array();
+        $i = $last_i = $level = 0;
+
+        reset($ob);
+        while (list($key, $val) = each($ob)) {
+            $pos = strpos($key, '.');
+            $index = substr($key, 0, $pos);
+            $type = substr($key, $pos + 1);
+
+            switch ($type) {
+            case 'num':
+                if ($val === 0) {
+                    $container = $index;
+                } else {
+                    ++$i;
+                    if (is_null($container) && empty($level)) {
+                        $thread_base = $val;
+                        $thread_base_idx = $index;
+                    }
+                    $lookup[$index] = $use_seq ? $index : $val;
+                    $ret[$val] = array('uid' => $val);
+                }
+                break;
+
+            case 'next':
+                if (!is_null($container) && ($container === $index)) {
+                    $container_base = $val;
+                } else {
+                    $ret[$lookup[$index]]['base'] = (!is_null($container))
+                        ? $lookup[$container_base]
+                        : ((!empty($level) || ($val != 0)) ? $lookup[$thread_base_idx] : null);
+                    ++$i;
+                    ++$level;
+                }
+                break;
+
+            case 'branch':
+                if ($container === $index) {
+                    $container = $container_base = null;
+                    $ret[$lookup[$last_index]]['last'] = true;
+                } else {
+                    $ret[$lookup[$index]]['level'] = $level--;
+                    $ret[$lookup[$index]]['last'] = !(!is_null($container) && empty($level));
+                    if ($index === $thread_base_idx) {
+                        $index = null;
+
+                    } elseif (!empty($level) &&
+                              !is_null($last_index) &&
+                              isset($ret[$last_index])) {
+                        $ret[$lookup[$last_index]]['last'] = ($last_i == ($i - 1));
+                    }
+                }
+                $last_index = $index;
+                $last_i = $i++;
+                break;
+            }
+        }
+
+        return $ret;
+    }
+
+    /**
+     * Fetch message data.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param array $criteria  The fetch criteria.
+     * @param array $options   Additional options.
+     *
+     * @return array  See self::fetch().
+     */
+    protected function _fetch($criteria, $options)
+    {
+        // Already guaranteed to be logged in here
+
+        $err = false;
+        $hdrinfo = $overview = null;
+
+        $old_error = error_reporting(0);
+
+        // These options are not supported by this driver.
+        if (!empty($options['changedsince']) ||
+            (reset($options['ids']) == self::USE_SEARCHRES)) {
+            return $this->_getSocket()->fetch($this->_selected, $criteria, $options);
+        }
+
+        if (empty($options['ids'])) {
+            $seq = '1:*';
+            $options['ids'] = range(1, imap_num_msg($this->_stream));
+        } else {
+            $seq = $this->toSequenceString($options['ids']);
+        }
+
+        $ret = array_combine($options['ids'], array_fill(0, count($options['ids']), array()));
+
+        foreach ($criteria as $type => $c_val) {
+            if (!is_array($c_val)) {
+                $c_val = array();
+            }
+
+            switch ($type) {
+            case self::FETCH_STRUCTURE:
+                // 'noext' has no effect in this driver
+                foreach ($options['ids'] as $id) {
+                    $structure = imap_fetchstructure($this->_stream, $id, empty($options['sequence']) ? FT_UID : 0);
+                    if (!$structure) {
+                        $err = true;
+                        break 2;
+                    }
+                    $structure = $this->_parseStructure($structure);
+                    $ret[$id]['structure'] = empty($c_val['parse']) ? $structure : Horde_MIME_Message::parseStructure($structure);
+                }
+                break;
+
+            case self::FETCH_FULLMSG:
+                foreach ($options['ids'] as $id) {
+                    $tmp = imap_fetchheader($this->_stream, $id, (empty($options['sequence']) ? FT_UID : 0) | FT_PREFETCHTEXT) .
+                           imap_body($this->_stream, $id, (empty($options['sequence']) ? FT_UID : 0) | (empty($c_val['peek']) ? 0 : FT_PEEK));
+                    if (isset($c_val['start']) && !empty($c_val['length'])) {
+                        $ret[$id]['fullmsg'] = substr($tmp, $c_val['start'], $c_val['length']);
+                    } else {
+                        $ret[$id]['fullmsg'] = $tmp;
+                    }
+                }
+                break;
+
+            case self::FETCH_HEADERTEXT:
+            case self::FETCH_BODYPART:
+                foreach ($c_val as $val) {
+                    if ($type == self::FETCH_HEADERTEXT) {
+                        $label = 'headertext';
+                        /* imap_fetchbody() can return header parts for a
+                         * given MIME part by appending '.0' (or 0 for the
+                         * main header) */
+                        if (empty($val['id'])) {
+                            $val['id'] = 0;
+                            $body_key = 0;
+                        } else {
+                            $body_key = $val['id'] . '.0';
+                        }
+                    } else {
+                        $label = 'bodypart';
+                        if (empty($val['id'])) {
+                            throw new Horde_Imap_Client_Exception('Need a MIME ID when retrieving a MIME body part.');
+                        }
+                        $body_key = $val['id'];
+                    }
+
+                    foreach ($options['ids'] as $id) {
+                        if (!isset($ret[$id][$label])) {
+                            $ret[$id][$label] = array();
+                        }
+                        $tmp = imap_fetchbody($this->_stream, $id, $header_key, empty($options['sequence']) ? FT_UID : 0);
+
+                        if (isset($val['start']) && !empty($val['length'])) {
+                            $tmp = substr($tmp, $val['start'], $val['length']);
+                        }
+
+                        if (!empty($val['parse'])) {
+                            $tmp = Horde_MIME_Headers::parseHeaders($tmp);
+                        }
+
+                        $ret[$id][$label][$val['id']] = $tmp;
+                    }
+                }
+                break;
+
+            case self::FETCH_BODYTEXT:
+                foreach ($c_val as $val) {
+                    // This is the base body.  This is easily obtained via
+                    // imap_body().
+                    $use_imapbody = empty($val['id']);
+
+                    foreach ($options['ids'] as $id) {
+                        if (!isset($ret[$id]['bodytext'])) {
+                            $ret[$id]['bodytext'] = array();
+                        }
+                        if ($use_imapbody) {
+                            $tmp = imap_body($this->_stream, $id, (empty($options['sequence']) ? FT_UID : 0) | (empty($val['peek']) ? 0 : FT_PEEK));
+                            if (isset($val['start']) && !empty($val['length'])) {
+                                $ret[$id]['bodytext'][0] = substr($tmp, $val['start'], $val['length']);
+                            } else {
+                                $ret[$id]['bodytext'][0] = $tmp;
+                            }
+                        } else {
+                            /* OY! There is no way to download just the body
+                             * of the message/rfc822 part.  The best we can do
+                             * is download the header of the part, determine
+                             * the length, and then remove that info from the
+                             * beginning of the imap_fetchbody() data. */
+                            $hdr_len = strlen(imap_fetchbody($this->_stream, $id, $val['id'] . '.0', (empty($options['sequence']) ? FT_UID : 0)));
+                            $tmp = substr(imap_fetchbody($this->_stream, $id, $val['id'], (empty($options['sequence']) ? FT_UID : 0)), $hdr_len);
+                            if (isset($val['start']) && !empty($val['length'])) {
+                                $ret[$id]['bodytext'][$val['id']] = substr($tmp, $val['start'], $val['length']);
+                            } else {
+                                $ret[$id]['bodytext'][$val['id']] = $tmp;
+                            }
+                        }
+                    }
+                }
+                break;
+
+            case self::FETCH_MIMEHEADER:
+            case self::FETCH_HEADERS:
+            case self::FETCH_MODSEQ:
+                // Can't do it. Nope. Nada. Without heavy duty parsing of the
+                // full imap_body() object, it is impossible to retrieve the
+                // MIME headers for each individual part. Ship it off to
+                // the Socket driver. Adios.
+                // This goes for header field searches also.
+                // MODSEQ isn't available in c-client either.
+                switch ($type) {
+                case self::FETCH_MIMEHEADER:
+                    $label = 'mimeheader';
+                    break;
+
+                case self::FETCH_HEADERS:
+                    $label = 'headers';
+                    break;
+
+                case self::FETCH_MODSEQ:
+                    $label = 'modseq';
+                    break;
+                }
+                $tmp = $this->_getSocket()->fetch($this->_selected, array($type => $c_val), $options);
+                foreach ($tmp as $id => $id_data) {
+                    if (!isset($ret[$id][$label])) {
+                        $ret[$id][$label] = array();
+                    }
+                    $ret[$id][$label] = array_merge($ret[$id][$label], $id_data[$label]);
+                }
+                break;
+
+            case self::FETCH_ENVELOPE:
+                if (is_null($hdrinfo)) {
+                    $hdrinfo = array();
+                    foreach ($options['ids'] as $id) {
+                        $hdrinfo[$id] = imap_headerinfo($this->_stream, empty($options['sequence']) ? imap_msgno($this->_stream, $id) : $id);
+                        if (!$hdrinfo[$id]) {
+                            $err = true;
+                            break 2;
+                        }
+                    }
+                }
+
+                $env_data = array(
+                    'date', 'subject', 'from', 'sender', 'reply_to', 'to',
+                    'cc', 'bcc', 'in_reply_to', 'message_id'
+                );
+
+                foreach ($options['ids'] as $id) {
+                    $hptr = &$hdrinfo[$id];
+                    $ret[$id]['envelope'] = array();
+                    $ptr = &$ret[$id]['envelope'];
+
+                    foreach ($env_data as $e_val) {
+                        $label = strtr($e_val, '_', '-');
+                        if (isset($hptr->$e_val)) {
+                            if (is_array($hptr->$e_val)) {
+                                $tmp = array();
+                                foreach ($hptr->$e_val as $a_val) {
+                                    $tmp[] = (array)$a_val;
+                                }
+                                $ptr[$label] = $tmp;
+                            } else {
+                                $ptr[$label] = $hptr->$e_val;
+                            }
+                        } else {
+                            $ptr[$label] = null;
+                        }
+                    }
+                }
+                break;
+
+            case self::FETCH_FLAGS:
+                if (is_null($overview)) {
+                    $overview = imap_fetch_overview($this->_stream, $seq, empty($options['sequence']) ? FT_UID : 0);
+                    if (!$overview) {
+                        $err = true;
+                        break 2;
+                    }
+                }
+
+                foreach ($options['ids'] as $id) {
+                    $tmp = array();
+                    foreach ($this->_supportedFlags as $f_val) {
+                        if ($overview[$id]->$f_val) {
+                            $tmp[] = '\\' . $f_val;
+                        }
+                    }
+                    $ret[$id]['flags'] = $tmp;
+                }
+                break;
+
+            case self::FETCH_DATE:
+                if (is_null($hdrinfo)) {
+                    $hdrinfo = array();
+                    foreach ($options['ids'] as $id) {
+                        $hdrinfo[$id] = imap_headerinfo($this->_stream, empty($options['sequence']) ? imap_msgno($this->_stream, $id) : $id);
+                        if (!$hdrinfo[$id]) {
+                            $err = true;
+                            break 2;
+                        }
+                    }
+                }
+
+                foreach ($options['ids'] as $id) {
+                    $ret[$id]['date'] = new DateTime($hdrinfo[$id]->MailDate);
+                }
+                break;
+
+            case self::FETCH_SIZE:
+                if (!is_null($hdrinfo)) {
+                    foreach ($options['ids'] as $id) {
+                        $ret[$id]['size'] = $hdrinfo[$id]->Size;
+                    }
+                } else {
+                    if (is_null($overview)) {
+                        $overview = imap_fetch_overview($this->_stream, $seq, empty($options['sequence']) ? FT_UID : 0);
+                        if (!$overview) {
+                            $err = true;
+                            break;
+                        }
+                    }
+                    foreach ($options['ids'] as $id) {
+                        $ret[$id]['size'] = $overview[$id]->size;
+                    }
+                }
+                break;
+
+            case self::FETCH_UID:
+                if (empty($options['sequence'])) {
+                    foreach ($options['ids'] as $id) {
+                        $ret[$id]['uid'] = $id;
+                    }
+                } else {
+                    if (is_null($overview)) {
+                        $overview = imap_fetch_overview($this->_stream, $seq, empty($options['sequence']) ? FT_UID : 0);
+                        if (!$overview) {
+                            $err = true;
+                            break;
+                        }
+                    }
+                    foreach ($options['ids'] as $id) {
+                        $ret[$id]['uid'] = $overview[$id]->uid;
+                    }
+                }
+                break;
+
+            case self::FETCH_SEQ:
+                if (!empty($options['sequence'])) {
+                    foreach ($options['ids'] as $id) {
+                        $ret[$id]['seq'] = $id;
+                    }
+                } else {
+                    if (is_null($overview)) {
+                        $overview = imap_fetch_overview($this->_stream, $seq, empty($options['sequence']) ? FT_UID : 0);
+                        if (!$overview) {
+                            $err = true;
+                            break;
+                        }
+                    }
+                    foreach ($options['ids'] as $id) {
+                        $ret[$id]['uid'] = $overview[$id]->msgno;
+                    }
+                }
+            }
+        }
+        error_reporting($old_error);
+
+        if ($err) {
+            throw new Horde_Imap_Client_Exception('Error when fetching messages: ' . imap_last_error());
+        }
+
+        return $ret;
+    }
+
+    /**
+     * Parse the output from imap_fetchstructure() in the format that
+     * this class returns structure data in.
+     *
+     * @param object $data  Data from imap_fetchstructure().
+     *
+     * @return array  See self::fetch() for structure return format.
+     */
+    protected function _parseStructure($data)
+    {
+        // Required entries
+        $ret = array(
+            'type' => $this->_mimeTypes[$data->type],
+            'subtype' => $data->ifsubtype ? strtolower($data->subtype) : 'x-unknown'
+        );
+
+        // Optional for multipart-parts, required for all others
+        if ($data->ifparameters) {
+            $ret['parameters'] = array();
+            foreach ($data->parameters as $val) {
+                $ret['parameters'][$val->attribute] = $val->value;
+            }
+        }
+
+        // Optional entries. 'location' and 'language' not supported
+        if ($data->ifdisposition) {
+            $ret['disposition'] = $data->disposition;
+            if ($data->ifdparameters) {
+                $ret['dparameters'] = array();
+                foreach ($data->dparameters as $val) {
+                    $ret['dparameters'][$val->attribute] = $val->value;
+                }
+            }
+        }
+
+        if ($ret['type'] == 'multipart') {
+            // multipart/* specific entries
+            $ret['parts'] = array();
+            foreach ($data->parts as $val) {
+                $ret['parts'][] = $this->_parseStructure($val);
+            }
+        } else {
+            // Required options
+            $ret['id'] = $data->ifid ? $data->id : null;
+            $ret['description'] = $data->ifdescription ? $data->description : null;
+            $ret['encoding'] = $this->_mimeEncodings[$data->encoding];
+            $ret['size'] = $data->bytes;
+
+            // Part specific options
+            if (($ret['type'] == 'message') && ($ret['subtype'] == 'rfc822')) {
+                // @todo - Doesn't seem to be an easy way to obtain the
+                // envelope information for this part.
+                $ret['envelope'] = array();
+                $ret['structure'] = $this->_parseStructure(reset($data->parts));
+                $ret['lines'] = $data->lines;
+            } elseif ($ret['type'] == 'text') {
+                $ret['lines'] = $data->lines;
+            }
+
+            // No support for 'md5' option
+        }
+
+        return $ret;
+    }
+
+    /**
+     * Store message flag data.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param array $options  Additional options.
+     */
+    protected function _store($options)
+    {
+        // Already guaranteed to be logged in here
+
+        /* This driver does not support flags other than those defined in the
+         * IMAP4 spec. If other flags exist, need to use the Socket driver
+         * instead. */
+        foreach (array('add', 'remove') as $val) {
+            if (!empty($options[$val]) &&
+                $this->_nonSupportedFlags($options[$val])) {
+                return $this->_getSocket()->store($this->_selected, $options);
+            }
+        }
+
+        // This driver does not support the 'unchangedsince' or 'replace'
+        // options, nor does it support using stored searches.
+        if (!empty($options['unchangedsince']) ||
+            !empty($options['replace']) ||
+            (reset($options['ids']) == self::USE_SEARCHRES)) {
+            // Requires Socket driver.
+            return $this->_getSocket()->store($this->_selected, $options);
+        }
+
+        $seq = empty($options['ids'])
+            ? '1:*'
+            : $this->toSequenceString($options['ids']);
+
+        $old_error = error_reporting(0);
+
+        if (!empty($options['add'])) {
+            $res = imap_setflag_full($this->_stream, $seq, implode(' ', $options['add']), empty($options['sequence']) ? ST_UID : 0);
+        }
+
+        if (($res === true) && !empty($options['remove'])) {
+            $res = imap_clearflag_full($this->_stream, $seq, implode(' ', $options['remove']), empty($options['sequence']) ? ST_UID : 0);
+        }
+
+        error_reporting($old_error);
+
+        if ($res === false) {
+            throw new Horde_Imap_Client_Exception('Error when flagging messages: ' . imap_last_error());
+        }
+
+        return array();
+    }
+
+    /**
+     * Copy messages to another mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $dest    The destination mailbox (UTF7-IMAP).
+     * @param array $options  Additional options.
+     *
+     * @return boolean  True on success (this driver does not support
+     *                  returning the UIDs).
+     */
+    protected function _copy($dest, $options)
+    {
+        // Already guaranteed to be logged in here
+
+        $opts = 0;
+        if (empty($options['sequence'])) {
+            $opts |= CP_UID;
+        }
+        if (!empty($options['move'])) {
+            $opts |= CP_MOVE;
+        }
+
+        if (reset($options['ids']) == self::USE_SEARCHRES) {
+            // Requires Socket driver.
+            return $this->_getSocket()->copy($this->_selected, $options);
+        }
+
+        $seq = empty($options['ids'])
+            ? '1:*'
+            : $this->toSequenceString($options['ids']);
+
+        $old_error = error_reporting(0);
+        $res = imap_mail_copy($this->_stream, $seq, $this->_connString($dest), $opts);
+        error_reporting($old_error);
+
+        if ($res === false) {
+            if (!empty($options['create'])) {
+                $this->createMailbox($dest);
+                unset($options['create']);
+                return $this->copy($dest, $options);
+            }
+            throw new Horde_Imap_Client_Exception('Error when copying/moving messages: ' . imap_last_error());
+        }
+
+        return true;
+    }
+
+    /**
+     * Set quota limits.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $root    The quota root (UTF7-IMAP).
+     * @param array $options  Additional options.
+     */
+    protected function _setQuota($root, $options)
+    {
+        // This driver only supports setting the 'STORAGE' quota.
+        if (isset($options['messages'])) {
+            $this->_getSocket()->setQuota($root, $options);
+            return;
+        }
+
+        $this->login();
+
+        $old_error = error_reporting(0);
+        $res = imap_set_quota($this->_stream, $root, $options['storage']);
+        error_reporting($old_error);
+
+        if ($res === false) {
+            throw new Horde_Imap_Client_Exception('Error when setting quota: ' . imap_last_error());
+        }
+    }
+
+    /**
+     * Get quota limits.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $root  The quota root (UTF7-IMAP).
+     *
+     * @return mixed  An array with these possible keys: 'messages' and
+     *                'storage'; each key holds an array with 2 values:
+     *                'limit' and 'usage'.
+     */
+    protected function _getQuota($root)
+    {
+        $this->login();
+
+        $old_error = error_reporting(0);
+        $res = imap_get_quota($this->_stream, $root);
+        error_reporting($old_error);
+
+        if ($res === false) {
+            throw new Horde_Imap_Client_Exception('Error when retrieving quota: ' . imap_last_error());
+        }
+    }
+
+    /**
+     * Get quota limits for a mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox  A mailbox (UTF7-IMAP).
+     *
+     * @return mixed  An array with the keys being the quota roots. Each key
+     *                holds an array with two possible keys: 'messages' and
+     *                'storage'; each of these keys holds an array with 2
+     *                values: 'limit' and 'usage'.
+     */
+    protected function _getQuotaRoot($mailbox)
+    {
+        $this->login();
+
+        $old_error = error_reporting(0);
+        $res = imap_get_quotaroot($this->_stream, $mailbox);
+        error_reporting($old_error);
+
+        if ($res === false) {
+            throw new Horde_Imap_Client_Exception('Error when retrieving quotaroot: ' . imap_last_error());
+        }
+
+        return array($mailbox => $ret);
+    }
+
+    /**
+     * Set ACL rights for a given mailbox/identifier.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox     A mailbox (UTF7-IMAP).
+     * @param string $identifier  The identifier to alter (UTF7-IMAP).
+     * @param array $options      Additional options.
+     */
+    protected function _setACL($mailbox, $identifier, $options)
+    {
+        $this->login();
+
+        if (empty($options['rights']) && !empty($options['remove'])) {
+            $acl = $this->listACLRights($mailbox, $identifier);
+            if (empty($acl['rights'])) {
+                return;
+            }
+            $options['rights'] = $acl['rights'];
+            $options['remove'] = true;
+        }
+
+        if (empty($options['rights'])) {
+            return;
+        }
+
+        $old_error = error_reporting(0);
+        $res = imap_setacl($this->_stream, $mailbox, $identifier, (empty($options['remove']) ? '+' : '-') . $implode('', $options['rights']));
+        error_reporting($old_error);
+
+        if ($res === false) {
+            throw new Horde_Imap_Client_Exception('Error when setting ACL: ' . imap_last_error());
+        }
+    }
+
+    /**
+     * Get ACL rights for a given mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox  A mailbox (UTF7-IMAP).
+     *
+     * @return array  An array with identifiers as the keys and an array of
+     *                rights as the values.
+     */
+    protected function _getACL($mailbox)
+    {
+        $this->login();
+
+        $acl = array();
+
+        $old_error = error_reporting(0);
+        $res = imap_getacl($this->_stream, $mailbox);
+        error_reporting($old_error);
+
+        if ($res === false) {
+            throw new Horde_Imap_Client_Exception('Error when retrieving ACLs: ' . imap_last_error());
+        }
+
+        foreach ($res as $id => $rights) {
+            $acl[$id] = array();
+            for ($i = 0, $iMax = strlen($rights); $i < $iMax; ++$i) {
+                $acl[$id][] = $rights[$i];
+            }
+        }
+
+        return $acl;
+    }
+
+    /**
+     * Get ACL rights for a given mailbox/identifier.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox     A mailbox (UTF7-IMAP).
+     * @param string $identifier  The identifier (UTF-8).
+     *
+     * @return array  An array of rights (keys: 'required' and 'optional').
+     */
+    protected function _listACLRights($mailbox, $identifier)
+    {
+        $acl = $this->getACL($mailbox);
+        // @todo - Does this return 'optional' information?
+        return isset($acl[$identifier]) ? $acl[$identifier] : array();
+    }
+
+    /**
+     * Get the ACL rights for the current user for a given mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox  A mailbox (UTF7-IMAP).
+     *
+     * @return array  An array of rights.
+     */
+    protected function _getMyACLRights($mailbox)
+    {
+        // No support in c-client for MYRIGHTS - need to call Socket driver
+        return $this->_getSocket()->getMyACLRights($mailbox);
+    }
+
+    /* Internal functions */
+
+    /**
+     * Create a Horde_Imap_Client_Socket instance pre-filled with this client's
+     * parameters.
+     *
+     * @return Horde_Imap_Client_Socket  The socket instance.
+     */
+    protected function _getSocket()
+    {
+        if (!isset($this->_socket)) {
+            $this->_socket = $this->getInstance('Socket', $this->_params);
+        }
+        return $this->_socket;
+    }
+
+    /**
+     * Generate the c-client connection string.
+     *
+     * @param string $mailbox  The mailbox to add to the connection string.
+     *
+     * @return string  The connection string.
+     */
+    protected function _connString($mailbox = '')
+    {
+        if (isset($this->_cstring)) {
+            return $this->_cstring . $mailbox;
+        }
+
+        $conn = '{' . $this->_params['hostspec'] . ':' . $this->_params['port'] . '/service=' . $this->_service;
+
+        switch ($this->_params['secure']) {
+        case 'ssl':
+            $conn .= '/ssl';
+            if (empty($this->_params['validate_cert'])) {
+                $conn .= '/novalidate-cert';
+            }
+            break;
+
+        case 'tls':
+            $conn .= '/tls';
+            if (empty($this->_params['validate_cert'])) {
+                $conn .= '/novalidate-cert';
+            }
+            break;
+
+        default:
+            $conn .= '/notls';
+            break;
+        }
+        $this->_cstring = $conn . '}';
+
+        return $this->_cstring . $mailbox;
+    }
+
+    /**
+     * Checks a flag list for non-supported c-client flags.
+     *
+     * @param array $flags  The list of flags.
+     *
+     * @return boolean  True if there is a non-supported flag in $flags.
+     */
+    protected function _nonSupportedFlags($flags)
+    {
+        // This driver does not support flags other than 'Seen', 'Answered',
+        // 'Flagged', 'Deleted', 'Recent', and 'Draft'.
+        foreach (array_map('strtolower', $flags) as $val) {
+            if (!in_array($val, $this->_supportedFlags)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+}
diff --git a/framework/Imap_Client/lib/Horde/Imap/Client/Exception.php b/framework/Imap_Client/lib/Horde/Imap/Client/Exception.php
new file mode 100644 (file)
index 0000000..f7f1858
--- /dev/null
@@ -0,0 +1,57 @@
+<?php
+/**
+ * Exception handler for the Horde_Imap_Client class.
+ *
+ * Copyright 2008 The Horde Project (http://www.horde.org/)
+ *
+ * $Horde: framework/Imap_Client/lib/Horde/Imap/Client/Exception.php,v 1.16 2008/10/23 04:53:13 slusarz Exp $
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author   Michael Slusarz <slusarz@curecanti.org>
+ * @category Horde
+ * @package  Horde_Imap_Client
+ */
+class Horde_Imap_Client_Exception extends Exception
+{
+    /* Error message codes. */
+    // Unspecified error (default)
+    const UNSPECIFIED = 0;
+
+    // The given Horde_Imap_Client driver does not exist on the system.
+    const DRIVER_NOT_FOUND = 1;
+
+    // The function called is not supported in POP3.
+    const POP3_NOTSUPPORTED = 2;
+
+    // There was an unrecoverable error in UTF7IMAP -> UTF8 conversion.
+    const UTF7IMAP_CONVERSION = 3;
+
+    // The IMAP server sent ended the connection.
+    const IMAP_DISCONNECT = 4;
+
+    // The charset used in the search query is not supported on the server.
+    const BADCHARSET = 5;
+
+    // There were errors parsing the MIME/RFC 2822 header of the part.
+    const PARSEERROR = 6;
+
+    // The server could not decode the MIME part (see RFC 3516)
+    const UNKNOWNCTE = 7;
+
+    // The server does not support the IMAP extensions needed for this
+    // operation
+    const NOSUPPORTIMAPEXT = 8;
+
+    // The comparator specified by setComparator() was not recognized by the
+    // IMAP server
+    const BADCOMPARATOR = 9;
+
+    // RFC 4551 [3.1.2] - All mailboxes are not required to support
+    // mod-sequences.
+    const MBOXNOMODSEQ = 10;
+
+    // Thrown if the cache has become invalid.
+    const CACHEUIDINVALID = 11;
+}
diff --git a/framework/Imap_Client/lib/Horde/Imap/Client/Socket.php b/framework/Imap_Client/lib/Horde/Imap/Client/Socket.php
new file mode 100644 (file)
index 0000000..91ce932
--- /dev/null
@@ -0,0 +1,3542 @@
+<?php
+/**
+ * Horde_Imap_Client_Socket:: provides an interface to an IMAP4rev1 server
+ * (RFC 3501) using PHP functions.
+ *
+ * Optional Parameters: NONE
+ *
+ * This driver implements the following IMAP-related RFCs:
+ *   RFC 2086/4314 - ACL
+ *   RFC 2087 - QUOTA
+ *   RFC 2088 - LITERAL+
+ *   RFC 2195 - AUTH=CRAM-MD5
+ *   RFC 2221 - LOGIN-REFERRALS
+ *   RFC 2342 - NAMESPACE
+ *   RFC 2595/4616 - AUTH=PLAIN
+ *   RFC 2831 - DIGEST-MD5 authentication mechanism.
+ *   RFC 2971 - ID
+ *   RFC 3501 - IMAP4rev1 specification
+ *   RFC 3502 - MULTIAPPEND
+ *   RFC 3516 - BINARY
+ *   RFC 3691 - UNSELECT
+ *   RFC 4315 - UIDPLUS
+ *   RFC 4422 - SASL Authentication (for DIGEST-MD5)
+ *   RFC 4466 - Collected extensions (updates RFCs 2088, 3501, 3502, 3516)
+ *   RFC 4551 - CONDSTORE
+ *   RFC 4731 - ESEARCH
+ *   RFC 4959 - SASL-IR
+ *   RFC 5032 - WITHIN
+ *   RFC 5161 - ENABLE
+ *   RFC 5162 - QRESYNC
+ *   RFC 5182 - SEARCHRES
+ *   RFC 5255 - LANGUAGE/I18NLEVEL
+ *   RFC 5256 - THREAD/SORT
+ *   RFC 5267 - ESORT
+ *
+ *   [NO RFC] - XIMAPPROXY
+ *              + Requires imapproxy v1.2.7-rc1 or later
+ *              + See http://lists.andrew.cmu.edu/pipermail/imapproxy-info/2008-October/000771.html and
+ *                http://lists.andrew.cmu.edu/pipermail/imapproxy-info/2008-October/000772.html
+ *
+ * TODO (or not necessary?):
+ *   RFC 2177 - IDLE (probably not necessary due to the limited connection
+ *                    time by each HTTP/PHP request)
+ *   RFC 2193 - MAILBOX-REFERRALS
+ *   RFC 4467/5092 - URLAUTH
+ *   RFC 4469 - CATENATE
+ *   RFC 4978 - COMPRESS=DEFLATE
+ *   RFC 3348/5258 - LIST-EXTENDED
+ *   RFC 5257 - ANNOTATE
+ *   RFC 5259 - CONVERT
+ *   RFC 5267 - CONTEXT
+ *
+ * Originally based on code from:
+ *   + auth.php (1.49)
+ *   + imap_general.php (1.212)
+ *   + imap_messages.php (revision 13038)
+ *   + strings.php (1.184.2.35)
+ *   from the Squirrelmail project.
+ *   Copyright (c) 1999-2007 The SquirrelMail Project Team
+ *
+ * Copyright 2005-2008 The Horde Project (http://www.horde.org/)
+ *
+ * $Horde: framework/Imap_Client/lib/Horde/Imap/Client/Socket.php,v 1.99 2008/10/29 05:13:00 slusarz Exp $
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author   Michael Slusarz <slusarz@curecanti.org>
+ * @category Horde
+ * @package  Horde_Imap_Client
+ */
+class Horde_Imap_Client_Socket extends Horde_Imap_Client_Base
+{
+    /**
+     * The unique tag to use when making an IMAP query.
+     *
+     * @var integer
+     */
+    protected $_tag = 0;
+
+    /**
+     * The socket connection to the IMAP server.
+     *
+     * @var resource
+     */
+    protected $_stream = null;
+
+    /**
+     * Temp array (destroyed at end of process).
+     *
+     * @var array
+     */
+    protected $_temp = array();
+
+    /**
+     * Destructor.
+     */
+    public function __destruct()
+    {
+        $this->_temp['logout'] = 2;
+        $this->logout();
+        parent::__destruct();
+    }
+
+    /**
+     * Do cleanup prior to serialization and provide a list of variables
+     * to serialize.
+     */
+    function __sleep()
+    {
+        $this->_temp['logout'] = 2;
+        $this->logout();
+        $this->_temp = array();
+        $this->_tag = 0;
+        parent::__sleep();
+        return array_diff(array_keys(get_class_vars(__CLASS__)), array('encryptKey'));
+    }
+
+    /**
+     * Get CAPABILITY info from the IMAP server.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @return array  The capability array.
+     */
+    protected function _capability()
+    {
+        // Need to use connect call here or else we run into loop issues
+        // because _connect() can call capability() internally.
+        $this->_connect();
+
+        // It is possible the server provided capability information on
+        // connect, so check for it now.
+        if (!isset($this->_init['capability'])) {
+            $this->_sendLine('CAPABILITY');
+        }
+
+        return $this->_init['capability'];
+    }
+
+    /**
+     * Parse a CAPABILITY Response (RFC 3501 [7.2.1]).
+     *
+     * @param array $data  The CAPABILITY data.
+     */
+    protected function _parseCapability($data)
+    {
+        $c = &$this->_init['capability'];
+        $c = array();
+
+        foreach ($data as $val) {
+            $cap_list = explode('=', $val);
+            $cap_list[0] = strtoupper($cap_list[0]);
+            if (isset($cap_list[1])) {
+                if (!isset($c[$cap_list[0]]) || !is_array($c[$cap_list[0]])) {
+                    $c[$cap_list[0]] = array();
+                }
+                $c[$cap_list[0]][] = $cap_list[1];
+            } elseif (!isset($c[$cap_list[0]])) {
+                $c[$cap_list[0]] = true;
+            }
+        }
+
+        /* RFC 5162 [1] - QRESYNC implies CONDSTORE, even if CONDSTORE is not
+         * listed as a capability. */
+        if (isset($c['QRESYNC'])) {
+            $c['CONDSTORE'] = true;
+        }
+
+        if (!empty($this->_temp['in_login'])) {
+            $this->_temp['logincapset'] = true;
+        }
+    }
+
+    /**
+     * Send a NOOP command.
+     * Throws a Horde_Imap_Client_Exception on error.
+     */
+    protected function _noop()
+    {
+        // NOOP doesn't return any specific response
+        $this->_sendLine('NOOP');
+    }
+
+    /**
+     * Get the NAMESPACE information from the IMAP server (RFC 2342).
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @return array  An array of namespace information.
+     */
+    protected function _getNamespaces()
+    {
+        $this->login();
+
+        if ($this->queryCapability('NAMESPACE')) {
+            $this->_sendLine('NAMESPACE');
+            return $this->_temp['namespace'];
+        }
+
+        return array();
+    }
+
+    /**
+     * Parse a NAMESPACE response (RFC 2342 [5] & RFC 5255 [3.4]).
+     *
+     * @param array $data  The NAMESPACE data.
+     */
+    protected function _parseNamespace($data)
+    {
+        $namespace_array = array(
+            0 => 'personal',
+            1 => 'other',
+            2 => 'shared'
+        );
+
+        $c = &$this->_temp['namespace'];
+        $c = array();
+        $lang = $this->queryCapability('LANGUAGE');
+
+        // Per RFC 2342, response from NAMESPACE command is:
+        // (PERSONAL NAMESPACES) (OTHER_USERS NAMESPACE) (SHARED NAMESPACES)
+        foreach ($namespace_array as $i => $val) {
+            if (!is_array($data[$i]) && (strtoupper($data[$i]) == 'NIL')) {
+                continue;
+            }
+            reset($data[$i]);
+            while (list(,$v) = each($data[$i])) {
+                $c[$v[0]] = array(
+                    'name' => $v[0],
+                    'delimiter' => $v[1],
+                    'type' => $val,
+                    'hidden' => false
+                );
+                // RFC 5255 [3.4] - TRANSLATION extension
+                if ($lang && (strtoupper($v[2] == 'TRANSLATION'))) {
+                    $c[$v[0]]['translation'] = reset($v[3]);
+                }
+            }
+        }
+    }
+
+    /**
+     * Return a list of alerts that MUST be presented to the user.
+     *
+     * @return array  An array of alert messages.
+     */
+    public function alerts()
+    {
+        return empty($this->_temp['alerts']) ? array() : $this->_temp['alerts'];
+    }
+
+    /**
+     * Login to the IMAP server.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @return boolean  Return true if global login tasks should be run.
+     */
+    protected function _login()
+    {
+        if (!empty($this->_temp['preauth'])) {
+            return $this->_loginTasks();
+        }
+
+        $this->_connect();
+
+        $t = &$this->_temp;
+
+        // Switch to secure channel if using TLS.
+        if (!$this->_isSecure &&
+            ($this->_params['secure'] == 'tls')) {
+            if (!$this->queryCapability('STARTTLS')) {
+                // We should never hit this - STARTTLS is required pursuant
+                // to RFC 3501 [6.2.1].
+                throw new Horde_Imap_Client_Exception('Server does not support TLS connections.', Horde_Imap_Client_Exception::NOSUPPORTIMAPEXT);
+            }
+
+            // Switch over to a TLS connection.
+            // STARTTLS returns no untagged response.
+            $this->_sendLine('STARTTLS');
+
+            $old_error = error_reporting(0);
+            $res = stream_socket_enable_crypto($this->_stream, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
+            error_reporting($old_error);
+
+            if (!$res) {
+                $this->logout();
+                throw new Horde_Imap_Client_Exception('Could not open secure TLS connection to the IMAP server.');
+            }
+
+            // Expire cached CAPABILITY information (RFC 3501 [6.2.1])
+            unset($this->_init['capability']);
+
+            // Reset language (RFC 5255 [3.1])
+            unset($this->_init['lang']);
+
+            // Set language if not using imapproxy
+            if ($this->_init['imapproxy']) {
+                $this->setLanguage();
+            }
+
+            $this->_isSecure = true;
+        }
+
+        if (empty($this->_init['authmethod'])) {
+            $first_login = true;
+            $imap_auth_mech = array();
+
+            $auth_methods = $this->queryCapability('AUTH');
+            if (!empty($auth_methods)) {
+                // Add SASL methods.
+                $imap_auth_mech = array_intersect(array('DIGEST-MD5', 'CRAM-MD5'), $auth_methods);
+
+                // Next, try 'PLAIN' authentication.
+                if (in_array('PLAIN', $auth_methods)) {
+                    $imap_auth_mech[] = 'PLAIN';
+                }
+            }
+
+            // Fall back to 'LOGIN' if available.
+            if (!$this->queryCapability('LOGINDISABLED')) {
+                $imap_auth_mech[] = 'LOGIN';
+            }
+
+            if (empty($imap_auth_mech)) {
+                throw new Horde_Imap_Client_Exception('No supported IMAP authentication method could be found.');
+            }
+
+            /* Use MD5 authentication first, if available. But no need to use
+             * use special authentication if we are already using an
+             * encrypted connection. */
+            if ($this->_isSecure) {
+                $imap_auth_mech = array_reverse($imap_auth_mech);
+            }
+        } else {
+            $first_login = false;
+            $imap_auth_mech = array($this->_init['authmethod']);
+        }
+
+        foreach ($imap_auth_mech as $method) {
+            $t['referral'] = null;
+
+            /* Set a flag indicating whether we have received a CAPABILITY
+             * response after we successfully login. Since capabilities may
+             * be different after login, this is the value we should end up
+             * caching if the object is eventually serialized. */
+            $this->_temp['in_login'] = true;
+
+            try {
+                $this->_tryLogin($method);
+                $success = true;
+                $this->_init['authmethod'] = $method;
+                unset($t['referralcount']);
+            } catch (Horde_Imap_Client_Exception $e) {
+                $success = false;
+                if (!empty($this->_init['authmethod'])) {
+                    unset($this->_init['authmethod']);
+                    return $this->login();
+                }
+            }
+
+            unset($this->_temp['in_login']);
+
+            // Check for login referral (RFC 2221) response - can happen for
+            // an OK, NO, or BYE response.
+            if (!is_null($t['referral'])) {
+                foreach (array('hostspec', 'port', 'username') as $val) {
+                    if (isset($t['referral'][$val])) {
+                        $this->_params[$val] = $t['referral'][$val];
+                    }
+                }
+
+                if (isset($t['referral']['auth'])) {
+                    $this->_init['authmethod'] = $t['referral']['auth'];
+                }
+
+                if (!isset($t['referralcount'])) {
+                    $t['referralcount'] = 0;
+                }
+
+                // RFC 2221 [3] - Don't follow more than 10 levels of referral
+                // without consulting the user.
+                if (++$t['referralcount'] < 10) {
+                    $this->logout();
+                    unset($this->_init['capability']);
+                    $this->_init['namespace'] = array();
+                    return $this->login();
+                }
+
+                unset($t['referralcount']);
+            }
+
+            if ($success) {
+                return $this->_loginTasks($first_login);
+            }
+        }
+
+        throw new Horde_Imap_Client_Exception('IMAP server denied authentication.');
+    }
+
+    /**
+     * Connects to the IMAP server.
+     * Throws a Horde_Imap_Client_Exception on error.
+     */
+    protected function _connect()
+    {
+        if (!is_null($this->_stream)) {
+            return;
+        }
+
+        if (!empty($this->_params['secure']) && !extension_loaded('openssl')) {
+            throw new Horde_Imap_Client_Exception('Secure connections require the PHP openssl extension.');
+        }
+
+        switch ($this->_params['secure']) {
+        case 'ssl':
+            $conn = 'ssl://';
+            $this->_isSecure = true;
+            break;
+
+        case 'tls':
+        default:
+            $conn = 'tcp://';
+            break;
+        }
+
+        $old_error = error_reporting(0);
+        $this->_stream = stream_socket_client($conn . $this->_params['hostspec'] . ':' . $this->_params['port'], $error_number, $error_string, $this->_params['timeout']);
+        error_reporting($old_error);
+
+        if ($this->_stream === false) {
+            $this->_stream = null;
+            $this->_isSecure = false;
+            throw new Horde_Imap_Client_Exception('Error connecting to IMAP server: [' . $error_number . '] ' . $error_string);
+        }
+
+        stream_set_timeout($this->_stream, $this->_params['timeout']);
+
+        // Get greeting information.  This is untagged so we need to specially
+        // deal with it here.  A BYE response will be caught and thrown in
+        // _getLine().
+        $ob = $this->_getLine();
+        switch ($ob['response']) {
+        case 'BAD':
+            // Server is rejecting our connection.
+            throw new Horde_Imap_Client_Exception('Server rejected connection: ' . $ob['line']);
+
+        case 'PREAUTH':
+            // The user was pre-authenticated.
+            $this->_temp['preauth'] = true;
+            break;
+
+        default:
+            $this->_temp['preauth'] = false;
+            break;
+        }
+        $this->_parseServerResponse($ob);
+
+        // Check for IMAP4rev1 support
+        if (!$this->queryCapability('IMAP4REV1')) {
+            throw new Horde_Imap_Client_Exception('This server does not support IMAP4rev1 (RFC 3501).');
+        }
+
+        // Set language if not using imapproxy
+        if (empty($this->_init['imapproxy'])) {
+            $this->_init['imapproxy'] = $this->queryCapability('XIMAPPROXY');
+            if (!$this->_init['imapproxy']) {
+                $this->setLanguage();
+            }
+        }
+
+        // If pre-authenticated, we need to do all login tasks now.
+        if ($this->_temp['preauth']) {
+            $this->login();
+        }
+    }
+
+    /**
+     * Authenticate to the IMAP server.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $method  IMAP login method.
+     */
+    protected function _tryLogin($method)
+    {
+        switch ($method) {
+        case 'CRAM-MD5':
+        case 'DIGEST-MD5':
+            $this->_sendLine('AUTHENTICATE ' . $method);
+
+            switch ($method) {
+            case 'CRAM-MD5':
+                // RFC 2195
+                $auth_sasl = Auth_SASL::factory('crammd5');
+                $response = base64_encode($auth_sasl->getResponse($this->_params['username'], $this->_params['password'], base64_decode($ob['line'])));
+                $this->_sendLine($response, array('debug' => '[CRAM-MD5 Response]', 'notag' => true));
+                break;
+
+            case 'DIGEST-MD5':
+                $auth_sasl = Auth_SASL::factory('digestmd5');
+                $response = base64_encode($auth_sasl->getResponse($this->_params['username'], $this->_params['password'], base64_decode($ob['line']), $this->_params['hostspec'], 'imap'));
+                $ob = $this->_sendLine($response, array('debug' => '[DIGEST-MD5 Response]', 'noparse' => true, 'notag' => true));
+                $response = base64_decode($ob['line']);
+                if (strpos($response, 'rspauth=') === false) {
+                    throw new Horde_Imap_Client_Exception('Unexpected response from server to Digest-MD5 response.');
+                }
+                $this->_sendLine('', array('notag' => true));
+                break;
+            }
+            break;
+
+        case 'LOGIN':
+            $this->_sendLine('LOGIN ' . $this->escape($this->_params['username']) . ' ' . $this->escape($this->_params['password']), array('debug' => '[LOGIN Command]'));
+            break;
+
+        case 'PLAIN':
+            // RFC 2595/4616 - PLAIN SASL mechanism
+            $auth = base64_encode(implode("\0", array($this->_params['username'], $this->_params['username'], $this->_params['password'])));
+            if ($this->queryCapability('SASL-IR')) {
+                // IMAP Extension for SASL Initial Client Response (RFC 4959)
+                $this->_sendLine('AUTHENTICATE PLAIN ' . $auth, array('debug' => '[SASL-IR AUTHENTICATE Command]'));
+            } else {
+                $this->_sendLine('AUTHENTICATE PLAIN');
+                $this->_sendLine($auth, array('debug' => '[AUTHENTICATE Command]', 'notag' => true));
+            }
+            break;
+        }
+    }
+
+    /**
+     * Perform login tasks.
+     *
+     * @param boolean $firstlogin  Is this the first login?
+     *
+     * @return boolean  True if global login tasks should be performed.
+     */
+    protected function _loginTasks($firstlogin = true)
+    {
+        /* If reusing an imapproxy connection, no need to do any of these
+         * login tasks again. */
+        if (!$firstlogin && !empty($this->_temp['proxyreuse'])) {
+            // If we have not yet set the language, set it now.
+            if (!isset($this->_init['lang'])) {
+                $this->setLanguage();
+            }
+            return false;
+        }
+
+        $this->_init['enabled'] = array();
+
+        /* If we logged in for first time, and server did not return
+         * capability information, we need to grab it now. */
+        if ($firstlogin && empty($this->_temp['logincapset'])) {
+            unset($this->_init['capability']);
+        }
+        $this->setLanguage();
+
+        /* Only active QRESYNC/CONDSTORE if caching is enabled. */
+        if ($this->_initCacheOb()) {
+            if ($this->queryCapability('QRESYNC')) {
+                /* QRESYNC REQUIRES ENABLE, so we just need to send one ENABLE
+                 * QRESYNC call to enable both QRESYNC && CONDSTORE. */
+                $this->_enable(array('QRESYNC'));
+                $this->_init['enabled']['CONDSTORE'] = true;
+            } elseif ($this->queryCapability('CONDSTORE')) {
+                /* CONDSTORE may be available, but ENABLE may not be. */
+                if ($this->queryCapability('ENABLE')) {
+                    $this->_enable(array('CONDSTORE'));
+                }
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Log out of the IMAP session.
+     */
+    protected function _logout()
+    {
+        if (!is_null($this->_stream)) {
+            /* $_temp['logout'] = 1 -- do explicit LOGOUT
+             * $_temp['logout'] = 2 -- immediately close connection. */
+            if (empty($this->_temp['logout']) ||
+                ($this->_temp['logout'] != 2)) {
+                $this->_temp['logout'] = 1;
+                try {
+                    $this->_sendLine('LOGOUT');
+                } catch (Horde_Imap_Client_Exception $e) {}
+            }
+            unset($this->_temp['logout']);
+            fclose($this->_stream);
+            $this->_stream = null;
+        }
+    }
+
+    /**
+     * Send ID information to the IMAP server (RFC 2971).
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param array $info  The information to send to the server.
+     */
+    protected function _sendID($info)
+    {
+        if (empty($info)) {
+            $cmd = 'NIL';
+        } else {
+            $cmd = '(';
+            foreach ($info as $key => $val) {
+                $cmd .= $this->escape(strtolower($key)) . ' ' . $this->escape($val);
+            }
+            $cmd .= ')';
+        }
+
+        $this->_sendLine('ID ' . $cmd);
+    }
+
+    /**
+     * Parse an ID response (RFC 2971 [3.2])
+     *
+     * @param array $data  The server response.
+     */
+    protected function _parseID($data)
+    {
+        $this->_temp['id'] = array();
+        $d = reset($data);
+        if (is_array($d)) {
+            for ($i = 0, $cnt = count($d); $i < $cnt; $i += 2) {
+                $this->_temp['id'][$d[$i]] = $d[$i + 1];
+            }
+        }
+    }
+
+    /**
+     * Return ID information from the IMAP server (RFC 2971).
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @return array  An array of information returned, with the keys as the
+     *                'field' and the values as the 'value'.
+     */
+    protected function _getID()
+    {
+        if (!isset($this->_temp['id'])) {
+            $this->sendID();
+        }
+        return $this->_temp['id'];
+    }
+
+    /**
+     * Sets the preferred language for server response messages (RFC 5255).
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param array $info  The preferred list of languages.
+     *
+     * @return string  The language accepted by the server, or null if the
+     *                 default language is used.
+     */
+    protected function _setLanguage($langs)
+    {
+        $cmd = array();
+        foreach ($langs as $val) {
+            $cmd[] = $this->escape($val);
+        }
+
+        try {
+            $this->_sendLine('LANGUAGE ' . implode(' ', $cmd));
+        } catch (Horde_Imap_Client_Exception $e) {
+            $this->_init['lang'] = null;
+            return null;
+        }
+
+        return $this->_init['lang'];
+    }
+
+    /**
+     * Gets the preferred language for server response messages (RFC 5255).
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param array $list  If true, return the list of available languages.
+     *
+     * @return mixed  If $list is true, the list of languages available on the
+     *                server (may be empty). If false, the language used by
+     *                the server, or null if the default language is used.
+     */
+    protected function _getLanguage($list)
+    {
+        if (!$list) {
+            return empty($this->_init['lang']) ? null : $this->_init['lang'];
+        }
+
+        if (!isset($this->_init['langavail'])) {
+            try {
+                $this->_sendLine('LANGUAGE');
+            } catch (Horde_Imap_Client_Exception $e) {
+                $this->_init['langavail'] = array();
+            }
+        }
+
+        return $this->_init['langavail'];
+    }
+
+    /**
+     * Parse a LANGUAGE response (RFC 5255 [3.3])
+     *
+     * @param array $data  The server response.
+     */
+    protected function _parseLanguage($data)
+    {
+        // Store data in $_params because it mustbe saved across page accesses
+        if (count($data[0]) == 1) {
+            // This is the language that was set.
+            $this->_init['lang'] = reset($data[0]);
+        } else {
+            // These are the languages that are available.
+            $this->_init['langavail'] = $data[0];
+        }
+    }
+
+    /**
+     * Enable an IMAP extension (see RFC 5161).
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param array $exts  The extensions to enable.
+     */
+    protected function _enable($exts)
+    {
+        // Only enable non-enabled extensions
+        $exts = array_diff($exts, array_keys($this->_init['enabled']));
+        if (!empty($exts)) {
+            $this->_sendLine('ENABLE ' . implode(' ', array_map('strtoupper', $exts)));
+        }
+    }
+
+    /**
+     * Parse an ENABLED response (RFC 5161 [3.2])
+     *
+     * @param array $data  The server response.
+     */
+    protected function _parseEnabled($data)
+    {
+        $this->_init['enabled'] = array_merge($this->_init['enabled'], array_flip($data));
+    }
+
+    /**
+     * Open a mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox  The mailbox to open (UTF7-IMAP).
+     * @param integer $mode    The access mode.
+     */
+    protected function _openMailbox($mailbox, $mode)
+    {
+        $this->login();
+
+        $condstore = false;
+        $qresync = isset($this->_init['enabled']['QRESYNC']);
+
+        /* Let the 'CLOSE' response code handle mailbox switching if QRESYNC
+         * is active. */
+        if (empty($this->_temp['mailbox']['name']) ||
+            (!$qresync && ($mailbox != $this->_temp['mailbox']['name']))) {
+            $this->_temp['mailbox'] = array('name' => $mailbox);
+            $this->_selected = $mailbox;
+        } elseif ($qresync) {
+            $this->_temp['qresyncmbox'] = $mailbox;
+        }
+
+        $cmd = (($mode == self::OPEN_READONLY) ? 'EXAMINE' : 'SELECT') . ' ' . $this->escape($mailbox);
+
+        /* If QRESYNC is available, synchronize the mailbox. */
+        if ($qresync) {
+            $metadata = $this->_cacheOb->getMetaData($mailbox, array('HICmodseq', 'uidvalid'));
+            if (isset($metadata['HICmodseq'])) {
+                $uids = $this->_cacheOb->get($mailbox);
+                if (!empty($uids)) {
+                    /* This command may cause several things to happen.
+                     * 1. UIDVALIDITY may have changed.  If so, we need
+                     * to expire the cache immediately (done below).
+                     * 2. NOMODSEQ may have been returned.  If so, we also
+                     * need to expire the cache immediately (done below).
+                     * 3. VANISHED/FETCH information was returned. These
+                     * responses will have already been handled by those
+                     * response handlers.
+                     * TODO: Use 4th parameter (useful if we keep a sequence
+                     * number->UID lookup in the future). */
+                    $cmd .= ' (QRESYNC (' . $metadata['uidvalid'] . ' ' . $metadata['HICmodseq'] . ' ' . $this->toSequenceString($uids) . '))';
+                }
+            }
+        } elseif (!isset($this->_init['enabled']['CONDSTORE']) &&
+                  $this->_initCacheOb() &&
+                  $this->queryCapability('CONDSTORE')) {
+            /* Activate CONDSTORE now if ENABLE is not available. */
+            $cmd .= ' (CONDSTORE)';
+            $condstore = true;
+        }
+
+        try {
+            $this->_sendLine($cmd);
+        } catch (Horde_Imap_Client_Exception $e) {
+            // An EXAMINE/SELECT failure with a return of 'NO' will cause the
+            // current mailbox to be unselected.
+            if ($this->_temp['parseresperr']['response'] == 'NO') {
+                $this->_selected = null;
+                $this->_mode = 0;
+            }
+            throw $e;
+        }
+
+        if ($qresync && isset($metadata['uidvalid'])) {
+            if (is_null($this->_temp['mailbox']['highestmodseq']) ||
+                ($this->_temp['mailbox']['uidvalidity'] != $metadata['uidvalid'])) {
+                $this->_cacheOb->deleteMailbox($mailbox);
+            } else {
+                /* We know the mailbox has been updated, so update the
+                 * highestmodseq metadata in the cache. */
+                $this->_cacheOb->setMetaData($mailbox, array('HICmodseq' => $this->_temp['mailbox']['highestmodseq']));
+            }
+        } elseif ($condstore) {
+            $this->_init['enabled']['CONDSTORE'] = true;
+        }
+    }
+
+    /**
+     * Create a mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox  The mailbox to create (UTF7-IMAP).
+     */
+    protected function _createMailbox($mailbox)
+    {
+        $this->login();
+
+        // CREATE returns no untagged information (RFC 3501 [6.3.3])
+        $this->_sendLine('CREATE ' . $this->escape($mailbox));
+    }
+
+    /**
+     * Delete a mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox  The mailbox to delete (UTF7-IMAP).
+     */
+    protected function _deleteMailbox($mailbox)
+    {
+        $this->login();
+
+        // Some IMAP servers will not allow a delete of a currently open
+        // mailbox.
+        if ($this->_selected == $mailbox) {
+            $this->close();
+        }
+
+        try {
+            // DELETE returns no untagged information (RFC 3501 [6.3.4])
+            $this->_sendLine('DELETE ' . $this->escape($mailbox));
+        } catch (Horde_Imap_Client_Exception $e) {
+            // Some IMAP servers won't allow a mailbox delete unless all
+            // messages in that mailbox are deleted.
+            if (!empty($this->_temp['deleteretry'])) {
+                unset($this->_temp['deleteretry']);
+                throw $e;
+            }
+
+            $this->store($mailbox, array('add' => array('\\deleted')));
+            $this->expunge($mailbox);
+
+            $this->_temp['deleteretry'] = true;
+            $this->deleteMailbox($mailbox);
+        }
+
+        unset($this->_temp['deleteretry']);
+    }
+
+    /**
+     * Rename a mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $old     The old mailbox name (UTF7-IMAP).
+     * @param string $new     The new mailbox name (UTF7-IMAP).
+     */
+    protected function _renameMailbox($old, $new)
+    {
+        $this->login();
+
+        // RENAME returns no untagged information (RFC 3501 [6.3.5])
+        $this->_sendLine('RENAME ' . $this->escape($old) . ' ' . $this->escape($new));
+    }
+
+    /**
+     * Manage subscription status for a mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox     The mailbox to [un]subscribe to (UTF7-IMAP).
+     * @param boolean $subscribe  True to subscribe, false to unsubscribe.
+     */
+    protected function _subscribeMailbox($mailbox, $subscribe)
+    {
+        $this->login();
+
+        // SUBSCRIBE/UNSUBSCRIBE returns no untagged information (RFC 3501
+        // [6.3.6 & 6.3.7])
+        $this->_sendLine(($subscribe ? '' : 'UN') . 'SUBSCRIBE ' . $this->escape($mailbox));
+    }
+
+    /**
+     * Obtain a list of mailboxes matching a pattern.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $pattern  The mailbox search pattern.
+     * @param integer $mode    Which mailboxes to return.
+     * @param array $options   Additional options.
+     *
+     * @return array  See self::listMailboxes().
+     */
+    protected function _listMailboxes($pattern, $mode, $options)
+    {
+        $this->login();
+
+        // Get the list of subscribed/unsubscribed mailboxes. Since LSUB is
+        // not guaranteed to have correct attributes, we must use LIST to
+        // ensure we receive the correct information.
+        if ($mode != self::MBOX_ALL) {
+            $subscribed = $this->_getMailboxList($pattern, self::MBOX_SUBSCRIBED, array('flat' => true));
+            // If mode is subscribed, and 'flat' option is true, we can
+            // return now.
+            if (($mode == self::MBOX_SUBSCRIBED) && !empty($options['flat'])) {
+                return $subscribed;
+            }
+        } else {
+            $subscribed = null;
+        }
+
+        return $this->_getMailboxList($pattern, $mode, $options, $subscribed);
+    }
+
+    /**
+     * Obtain a list of mailboxes matching a pattern.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $pattern    The mailbox search pattern.
+     * @param integer $mode      Which mailboxes to return.
+     * @param array $options     Additional options.
+     * @param array $subscribed  A list of subscribed mailboxes.
+     *
+     * @return array  See self::listMailboxes(().
+     */
+    protected function _getMailboxList($pattern, $mode, $options,
+                                       $subscribed = null)
+    {
+        $check = (($mode != self::MBOX_ALL) && !is_null($subscribed));
+
+        // Setup cache entry for use in _parseList()
+        $t = &$this->_temp;
+        $t['mailboxlist'] = array(
+            'check' => $check,
+            'subscribed' => $check ? array_flip($subscribed) : null,
+            'options' => $options
+        );
+        $t['listresponse'] = array();
+
+        $this->_sendLine((($mode == self::MBOX_SUBSCRIBED) ? 'LSUB' : 'LIST') . ' "" ' . $this->escape($pattern));
+
+        return (empty($options['flat'])) ? $t['listresponse'] : array_values($t['listresponse']);
+    }
+
+    /**
+     * Parse a LIST/LSUB response (RFC 3501 [7.2.2 & 7.2.3]).
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param array $data  The server response (includes type as first
+     *                     element).
+     */
+    protected function _parseList($data)
+    {
+        $ml = $this->_temp['mailboxlist'];
+        $mlo = $ml['options'];
+        $lr = &$this->_temp['listresponse'];
+
+        $mode = strtoupper($data[0]);
+        $mbox = $data[3];
+
+        /* If dealing with [un]subscribed mailboxes, check to make sure
+         * this mailbox is in the correct category. */
+        if ($ml['check'] &&
+            ((($mode == 'LIST') && isset($ml['subscribed'][$mbox])) ||
+             (($mode == 'LSUB') && !isset($ml['subscribed'][$mbox])))) {
+            return;
+        }
+
+        if (!empty($mlo['utf8'])) {
+            $mbox = Horde_Imap_Client_Utf7imap::Utf7ImapToUtf8($mbox);
+        }
+
+        if (empty($mlo['flat'])) {
+            $tmp = array('mailbox' => $mbox);
+            if (!empty($mlo['attributes'])) {
+                $tmp['attributes'] = array_map('strtolower', $data[1]);
+            }
+            if (!empty($mlo['delimiter'])) {
+                $tmp['delimiter'] = $data[2];
+            }
+            $lr[$mbox] = $tmp;
+        } else {
+            $lr[] = $mbox;
+        }
+    }
+
+    /**
+     * Obtain status information for a mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox  The mailbox to query (UTF7-IMAP).
+     * @param string $flags    A bitmask of information requested from the
+     *                         server.
+     *
+     * @return array  See Horde_Imap_Client_Base::status().
+     */
+    protected function _status($mailbox, $flags)
+    {
+        $data = $query = array();
+        $search = null;
+
+        $items = array(
+            self::STATUS_MESSAGES => 'messages',
+            self::STATUS_RECENT => 'recent',
+            self::STATUS_UIDNEXT => 'uidnext',
+            self::STATUS_UIDVALIDITY => 'uidvalidity',
+            self::STATUS_UNSEEN => 'unseen',
+            self::STATUS_FIRSTUNSEEN => 'firstunseen',
+            self::STATUS_FLAGS => 'flags',
+            self::STATUS_PERMFLAGS => 'permflags',
+            self::STATUS_UIDNOTSTICKY => 'uidnotsticky',
+        );
+
+        /* Don't include 'highestmodseq' return if server does not support it.
+         * OK to use queryCapability('CONDSTORE') here because we may not have
+         * yet sent an enabling command. */
+        if ($this->queryCapability('CONDSTORE')) {
+            $items[self::STATUS_HIGHESTMODSEQ] = 'highestmodseq';
+        }
+
+        /* If FLAGS/PERMFLAGS/UIDNOTSTICKY/FIRSTUNSEEN are needed, we must do
+         * a SELECT/EXAMINE to get this information (data will be caught in
+         * the code below). */
+        if (($flags & self::STATUS_FIRSTUNSEEN) ||
+            ($flags & self::STATUS_FLAGS) ||
+            ($flags & self::STATUS_PERMFLAGS) ||
+            ($flags & self::STATUS_UIDNOTSTICKY)) {
+            $this->openMailbox($mailbox);
+        } else {
+            $this->login();
+        }
+
+        foreach ($items as $key => $val) {
+            if ($key & $flags) {
+                if ($mailbox == $this->_selected) {
+                    if (isset($this->_temp['mailbox'][$val])) {
+                        $data[$val] = $this->_temp['mailbox'][$val];
+                    } else {
+                        if ($key == self::STATUS_UIDNOTSTICKY) {
+                            /* In the absence of uidnotsticky information, or
+                             * if UIDPLUS is not supported, we assume the UIDs
+                             * are sticky. */
+                            $data[$val] = false;
+                        } elseif (in_array($key, array(self::STATUS_FIRSTUNSEEN, self::STATUS_UNSEEN))) {
+                            /* If we already know there are no messages in the
+                             * current mailbox, we know there is no
+                             * firstunseen and unseen info also. */
+                            if (empty($this->_temp['mailbox']['messages'])) {
+                                $data[$val] = ($key == self::STATUS_FIRSTUNSEEN) ? null : 0;
+                            } else {
+                                /* RFC 3501 [6.3.1] - FIRSTUNSEEN information
+                                 * is not mandatory. If missing EXAMINE/SELECT
+                                 * we need to do a search. An UNSEEN count
+                                 * also requires a search. */
+                                if (is_null($search)) {
+                                    $search_query = new Horde_Imap_Client_Search_Query();
+                                    $search_query->flag('\\seen', false);
+                                    $search = $this->search($mailbox, $search_query, array('results' => array(($key == self::STATUS_FIRSTUNSEEN) ? self::SORT_RESULTS_MIN : self::SORT_RESULTS_COUNT), 'sequence' => true));
+                                }
+
+                                $data[$val] = $search[($key == self::STATUS_FIRSTUNSEEN) ? 'min' : 'count'];
+                            }
+                        }
+                    }
+                } else {
+                    $query[] = $val;
+                }
+            }
+        }
+
+        if (empty($query)) {
+            return $data;
+        }
+
+        $this->_temp['status'] = array();
+        $this->_sendLine('STATUS ' . $this->escape($mailbox) . ' (' . implode(' ', array_map('strtoupper', $query)) . ')');
+
+        return $this->_temp['status'];
+    }
+
+    /**
+     * Parse a STATUS response (RFC 3501 [7.2.4], RFC 4551 [3.6])
+     *
+     * @param array $data  The server response.
+     */
+    protected function _parseStatus($data)
+    {
+        for ($i = 0, $len = count($data); $i < $len; $i += 2) {
+            $item = strtolower($data[$i]);
+            $val = $data[$i + 1];
+            if (!$val && ($item == 'highestmodseq')) {
+                $val = null;
+            }
+            $this->_temp['status'][$item] = $val;
+        }
+    }
+
+    /**
+     * Append message(s) to a mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox  The mailbox to append the message(s) to
+     *                         (UTF7-IMAP).
+     * @param array $data      The message data.
+     * @param array $options   Additional options.
+     *
+     * @return mixed  An array of the UIDs of the appended messages (if server
+     *                supports UIDPLUS extension) or true.
+     */
+    protected function _append($mailbox, $data, $options)
+    {
+        $this->login();
+
+        // If the mailbox is currently selected read-only, we need to close
+        // because some IMAP implementations won't allow an append.
+        if (($this->_selected == $mailbox) &&
+            ($this->_mode == Horde_Imap_Client::OPEN_READONLY)) {
+            $this->close();
+        }
+
+        // Check for MULTIAPPEND extension (RFC 3502)
+        $multiappend = $this->queryCapability('MULTIAPPEND');
+
+        $t = &$this->_temp;
+        $t['appenduid'] = array();
+        $t['trycreate'] = null;
+        $t['uidplusmbox'] = $mailbox;
+        $cnt = count($data);
+        $i = 0;
+        $notag = false;
+        $literaldata = true;
+
+        reset($data);
+        while (list(,$m_data) = each($data)) {
+            if (!$i++ || !$multiappend) {
+                $cmd = 'APPEND ' . $this->escape($mailbox);
+            } else {
+                $cmd = '';
+                $notag = true;
+            }
+
+            if (!empty($m_data['flags'])) {
+                $cmd .= ' (' . implode(' ', $m_data['flags']) . ')';
+            }
+
+            if (!empty($m_data['internaldate'])) {
+                $cmd .= ' ' . $this->escape($m_data['internaldate']->format('j-M-Y H:i:s O'));
+            }
+
+            /* @todo There is no way I am aware of to determine the length of
+             * a stream. Having a user pass in the length of a stream is
+             * cumbersome, and they would most likely have to do just as much
+             * work to get the length of the stream as we have to do here. So
+             * for now, simply grab the contents of the stream and do a
+             * strlen() call to determine the literal size to send to the
+             * IMAP server. */
+            $text = $this->removeBareNewlines(is_resource($m_data['data']) ? stream_get_contents($m_data['data']) : $m_data['data']);
+            $datalength = strlen($text);
+
+            /* RFC 3516/4466 says we should be able to append binary data
+             * using literal8 "~{#} format", but it doesn't seem to work in
+             * all servers tried (UW-IMAP/Cyrus). However, there is no other
+             * way to append null data, so try anyway. */
+            $binary = (strpos($text, null) !== false);
+
+            /* Need to add 2 additional characters (we send CRLF at the end of
+             * a line) to literal count for multiappend messages to ensure the
+             * server will accept the next line of information, which contains
+             * the next append request. */
+            if ($multiappend) {
+                if ($i == $cnt) {
+                    $literaldata = false;
+                } else {
+                    $datalength += 2;
+                }
+            } else {
+                $literaldata = false;
+            }
+
+            try {
+                $this->_sendLine($cmd, array('binary' => $binary, 'literal' => $datalength, 'notag' => $notag));
+            } catch (Horde_Imap_Client_Exception $e) {
+                if (!empty($options['create']) && $this->_temp['trycreate']) {
+                    $this->createMailbox($mailbox);
+                    unset($options['create']);
+                    return $this->_append($mailbox, $data, $options);
+                }
+                throw $e;
+            }
+
+            // Send data.
+            $this->_sendLine($text, array('literaldata' => $literaldata, 'notag' => true));
+        }
+
+        /* If we reach this point and have data in $_temp['appenduid'],
+         * UIDPLUS (RFC 4315) has done the dirty work for us. */
+        return empty($t['appenduid']) ? true : $t['appenduid'];
+    }
+
+    /**
+     * Request a checkpoint of the currently selected mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     */
+    protected function _check()
+    {
+        // CHECK returns no untagged information (RFC 3501 [6.4.1])
+        $this->_sendLine('CHECK');
+    }
+
+    /**
+     * Close the connection to the currently selected mailbox, optionally
+     * expunging all deleted messages (RFC 3501 [6.4.2]).
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param array $options  Additional options.
+     */
+    protected function _close($options)
+    {
+        if (empty($options['expunge'])) {
+            if ($this->queryCapability('UNSELECT')) {
+                // RFC 3691 defines 'UNSELECT' for precisely this purpose
+                $this->_sendLine('UNSELECT');
+            } else {
+                // RFC 3501 [6.4.2]: to close a mailbox without expunge,
+                // select a non-existent mailbox. Selecting a null mailbox
+                // should do the trick.
+                try {
+                    $this->_sendLine('SELECT ""');
+                } catch (Horde_Imap_Client_Exception $e) {
+                    // Ignore - we are expecting a NO return.
+                }
+            }
+        } else {
+            // If caching, we need to know the UIDs being deleted, so call
+            // expunge() before calling close().
+            if ($this->_initCacheOb()) {
+                $this->expunge($this->_selected);
+            }
+
+            // CLOSE returns no untagged information (RFC 3501 [6.4.2])
+            $this->_sendLine('CLOSE');
+
+            /* Ignore HIGHESTMODSEQ information (RFC 5162 [3.4]) since the
+             * expunge() call would have already caught it. */
+        }
+
+        // Need to clear status cache since we are no longer in mailbox.
+        $this->_temp['mailbox'] = array();
+    }
+
+    /**
+     * Expunge deleted messages from the given mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param array $options  Additional options.
+     */
+    protected function _expunge($options)
+    {
+        $unflag = array();
+        $mailbox = $this->_selected;
+        $seq = !empty($options['sequence']);
+        $s_res = null;
+        $uidplus = $this->queryCapability('UIDPLUS');
+        $use_cache = $this->_initCacheOb();
+
+        if (empty($options['ids'])) {
+            $uid_string = '1:*';
+        } elseif ($uidplus) {
+            /* UID EXPUNGE command needs UIDs. */
+            if (reset($options['ids']) === self::USE_SEARCHRES) {
+                $uid_string = '$';
+            } elseif ($seq) {
+                $results = array(self::SORT_RESULTS_MATCH);
+                if ($this->queryCapability('SEARCHRES')) {
+                    $results[] = self::SORT_RESULTS_SAVE;
+                }
+                $s_res = $this->search($mailbox, null, array('results' => $results));
+                $uid_string = (in_array(self::SORT_RESULTS_SAVE, $results) && !empty($s_res['save']))
+                    ? '$'
+                    : $this->toSequenceString($s_res['match']);
+            } else {
+                $uid_string = $this->toSequenceString($options['ids']);
+            }
+        } else {
+            /* Without UIDPLUS, need to temporarily unflag all messages marked
+             * as deleted but not a part of requested IDs to delete. Use NOT
+             * searches to accomplish this goal. */
+            $search_query = new Horde_Imap_Client_Search_Query();
+            $search_query->flag('\\deleted', true);
+            if (reset($options['ids']) === self::USE_SEARCHRES) {
+                $search_query->previousSearch(true);
+            } else {
+                $search_query->sequence($options['ids'], $seq, true);
+            }
+
+            $res = $this->search($mailbox, $search_query);
+            $unflag = $res['match'];
+
+            $this->store($mailbox, array('ids' => $unflag, 'remove' => array('\\deleted')));
+        }
+
+        /* We need to get Msgno -> UID lookup table if we are caching.
+         * Apparently, there is no guarantee that if we are using QRESYNC that
+         * we will get VANISHED responses, so we need to do this. */
+        if ($use_cache && is_null($s_res)) {
+            /* Keys in $s_res['sort'] start at 0, not 1. */
+            $s_res = $this->search($mailbox, null, array('sort' => array(self::SORT_ARRIVAL)));
+        }
+
+        $tmp = &$this->_temp;
+        $tmp['expunge'] = $tmp['vanished'] = array();
+
+        /* Always use UID EXPUNGE if available. */
+        if ($uidplus) {
+            $this->_sendLine('UID EXPUNGE ' . $uid_string);
+        } elseif ($use_cache) {
+            $this->_sendLine('EXPUNGE');
+        } else {
+            /* This is faster than an EXPUNGE because the server will not
+             * return untagged EXPUNGE responses. We can only do this if
+             * we are not updating cache information. */
+            $this->close(array('expunge' => true));
+        }
+
+        if (!empty($unflag)) {
+            $this->store($mailbox, array('add' => array('\\deleted'), 'ids' => $unflag));
+        }
+
+        if ($use_cache) {
+            if (!empty($tmp['vanished'])) {
+                $i = count($tmp['vanished']);
+                $expunged = $tmp['vanished'];
+            } elseif (!empty($tmp['expunge'])) {
+                $expunged = array();
+                $i = 0;
+                $t = $s_res['sort'];
+
+                foreach ($tmp['expunge'] as $val) {
+                    $expunged[] = $t[$val - 1 + $i++];
+                }
+            }
+
+            if (!empty($expunged)) {
+                $this->_cacheOb->deleteMsgs($mailbox, $expunged);
+                $tmp['mailbox']['messages'] -= $i;
+            }
+
+            if (isset($this->_init['enabled']['QRESYNC'])) {
+                $this->_cacheOb->setMetaData($mailbox, array('HICmodseq' => $this->_temp['mailbox']['highestmodseq']));
+            }
+        }
+    }
+
+    /**
+     * Parse an EXPUNGE response (RFC 3501 [7.4.1]).
+     *
+     * @param integer $seq  The message sequence number.
+     */
+    protected function _parseExpunge($seq)
+    {
+        $this->_temp['expunge'][] = $seq;
+    }
+
+    /**
+     * Parse a VANISHED response (RFC 5162 [3.6]).
+     *
+     * @param array $data  The response data.
+     */
+    protected function _parseVanished($data)
+    {
+        /* There are two forms of VANISHED.  VANISHED (EARLIER) will be sent
+         * be sent in a FETCH (VANISHED) or SELECT/EXAMINE (QRESYNC) call.
+         * If this is the case, we can go ahead and update the cache
+         * immediately (we know we are caching or else QRESYNC would not be
+         * enabled). HIGHESTMODSEQ information will be grabbed at the end in
+         * the tagged response. */
+        if (is_array($data[0])) {
+            if (strtoupper(reset($data[0])) == 'EARLIER') {
+                $this->_cacheOb->deleteMsgs($this->_temp['mailbox']['name'], $this->fromSequenceString($data[1]));
+            }
+        } else {
+            /* The second form is just VANISHED. This is returned from an
+             * EXPUNGE command and will be processed in _expunge() (since
+             * we need to adjust message counts in the current mailbox). */
+            $this->_temp['vanished'] = $this->fromSequenceString($data[0]);
+        }
+    }
+
+    /**
+     * Search a mailbox.  This driver supports all IMAP4rev1 search criteria
+     * as defined in RFC 3501.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param object $query   The search query.
+     * @param array $options  Additional options. The '_query' key contains
+     *                        the value of $query->build().
+     *
+     * @return array  An array of UIDs (default) or an array of message
+     *                sequence numbers (if 'sequence' is true).
+     */
+    protected function _search($query, $options)
+    {
+        // Check for IMAP extensions needed
+        foreach ($query->extensionsNeeded() as $val) {
+            if (!$this->queryCapability($val)) {
+                throw new Horde_Imap_Client_Exception('IMAP Server does not support sorting extension ' . $val . '.', Horde_Imap_Client_Exception::NOSUPPORTIMAPEXT);
+            }
+
+            /* RFC 4551 [3.1] - trying to do a MODSEQ SEARCH on a mailbox that
+             * doesn't support it will return BAD. Catch that here and thrown
+             * an exception. */
+            if (($val == 'CONDSTORE') &&
+                is_null($this->_temp['mailbox']['highestmodseq']) &&
+                (strpos($options['_query']['query'], 'MODSEQ ') !== false)) {
+                throw new Horde_Imap_Client_Exception('Mailbox does not support mod-sequences.', Horde_Imap_Client_Exception::MBOXNOMODSEQ);
+            }
+        }
+
+        $cmd = '';
+        if (empty($options['sequence'])) {
+            $cmd = 'UID ';
+        }
+
+        $sort_criteria = array(
+            self::SORT_ARRIVAL => 'ARRIVAL',
+            self::SORT_CC => 'CC',
+            self::SORT_DATE => 'DATE',
+            self::SORT_FROM => 'FROM',
+            self::SORT_REVERSE => 'REVERSE',
+            self::SORT_SIZE => 'SIZE',
+            self::SORT_SUBJECT => 'SUBJECT',
+            self::SORT_TO => 'TO'
+        );
+
+        $results_criteria = array(
+            self::SORT_RESULTS_COUNT => 'COUNT',
+            self::SORT_RESULTS_MATCH => 'ALL',
+            self::SORT_RESULTS_MAX => 'MAX',
+            self::SORT_RESULTS_MIN => 'MIN',
+            self::SORT_RESULTS_SAVE => 'SAVE'
+        );
+
+        // Check if the server supports server-side sorting (RFC 5256).
+        $esearch = $server_sort = $return_sort = false;
+        if (!empty($options['sort'])) {
+            $return_sort = true;
+            $server_sort = $this->queryCapability('SORT');
+
+            /* Make sure sort options are correct. If not, default to ARRIVAL
+             * sort. */
+            if (count(array_intersect($options['sort'], array_keys($sort_criteria))) === 0) {
+                $options['sort'] = array(self::SORT_ARRIVAL);
+            }
+        }
+
+        if ($server_sort) {
+            // Check for ESORT capability (RFC 5267)
+            if ($this->queryCapability('ESORT')) {
+                $results = array();
+                foreach ($options['results'] as $val) {
+                    if (isset($results_criteria[$val]) &&
+                        ($val != self::SORT_RESULTS_SAVE)) {
+                        $results[] = $results_criteria[$val];
+                    }
+                }
+                $cmd .= 'SORT RETURN ( ' . implode(' ', $results) . ') (';
+            } else {
+                $cmd .= 'SORT (';
+            }
+
+            foreach ($options['sort'] as $val) {
+                if (isset($sort_criteria[$val])) {
+                    $cmd .= $sort_criteria[$val] . ' ';
+                }
+            }
+            $cmd = rtrim($cmd) . ') ';
+        } else {
+            // Check if the server supports ESEARCH (RFC 4731).
+            $esearch = $this->queryCapability('ESEARCH');
+
+            if ($esearch) {
+                // Always use ESEARCH if available because it returns results
+                // in a more compact sequence-set list
+                $results = array();
+                foreach ($options['results'] as $val) {
+                    if (isset($results_criteria[$val])) {
+                        $results[] = $results_criteria[$val];
+                    }
+                }
+                $cmd .= 'SEARCH RETURN (' . implode(' ', $results) . ') CHARSET ';
+            } else {
+                $cmd .= 'SEARCH CHARSET ';
+            }
+
+            // SEARCHRES requires ESEARCH
+            unset($this->_temp['searchnotsaved']);
+        }
+
+        $er = &$this->_temp['esearchresp'];
+        $sr = &$this->_temp['searchresp'];
+        $er = $sr = array();
+
+        $this->_sendLine($cmd . $options['_query']['charset'] . ' ' . $options['_query']['query']);
+
+        if ($return_sort && !$server_sort) {
+            $sr = array_values($this->_clientSort($sr, $options));
+        }
+
+        $ret = array();
+        foreach ($options['results'] as $val) {
+            switch ($val) {
+            case self::SORT_RESULTS_COUNT:
+                $ret['count'] = $esearch ? $er['count'] : count($sr);
+                break;
+
+            case self::SORT_RESULTS_MATCH:
+                $ret[$return_sort ? 'sort' : 'match'] = $sr;
+                break;
+
+            case self::SORT_RESULTS_MAX:
+                $ret['max'] = $esearch ? (isset($er['max']) ? $er['max'] : null) : (empty($sr) ? null : max($sr));
+                break;
+
+            case self::SORT_RESULTS_MIN:
+                $ret['min'] = $esearch ? (isset($er['min']) ? $er['min'] : null) : (empty($sr) ? null : min($sr));
+                break;
+
+            case self::SORT_RESULTS_SAVE:
+                $ret['save'] = $esearch ? empty($this->_temp['searchnotsaved']) : false;
+            }
+        }
+
+        // Add modseq data, if needed.
+        if (!empty($er['modseq'])) {
+            $ret['modseq'] = $er['modseq'];
+        }
+
+        return $ret;
+    }
+
+    /**
+     * Parse a SEARCH/SORT response (RFC 3501 [7.2.5]; RFC 4466 [3];
+     * RFC 5256 [4]; RFC 5267 [3]).
+     *
+     * @param array $data  The server response.
+     */
+    protected function _parseSearch($data)
+    {
+        // The extended search response will have a (NAME VAL) entry(s) at
+        // the end of the returned data. Do a check for this data.
+        if (is_array(end($data))) {
+            $this->_parseEsearch(array_pop($data));
+        }
+
+        $this->_temp['searchresp'] = $data;
+    }
+
+    /**
+     * Parse an ESEARCH response (RFC 4466 [2.6.2])
+     * Format: (TAG "a567") UID COUNT 5 ALL 4:19,21,28
+     *
+     * @param array $data  The server response.
+     */
+    protected function _parseEsearch($data)
+    {
+        $i = 0;
+        $len = count($data);
+
+        // Ignore search correlator information
+        if (is_array($data[$i])) {
+            ++$i;
+        }
+
+        // Ignore UID tag
+        if (($i != $len) && (strtoupper($data[$i]) == 'UID')) {
+            ++$i;
+        }
+
+        // This catches the case of an '(ALL)' esearch with no results
+        if ($i == $len) {
+            return;
+        }
+
+        for (; $i < $len; $i += 2) {
+            $val = $data[$i + 1];
+            $tag = strtoupper($data[$i]);
+            switch ($tag) {
+            case 'ALL':
+                $this->_temp['searchresp'] = $this->fromSequenceString($val);
+                break;
+
+            case 'COUNT':
+            case 'MAX':
+            case 'MIN':
+            case 'MODSEQ':
+                $this->_temp['esearchresp'][strtolower($tag)] = $val;
+                break;
+            }
+        }
+    }
+
+    /**
+     * If server does not support the SORT IMAP extension (RFC 5256), we need
+     * to do sorting on the client side.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param array $res   The search results.
+     * @param array $opts  The options to search().
+     *
+     * @return array  The sort results.
+     */
+    protected function _clientSort($res, $opts)
+    {
+        if (empty($res)) {
+            return $res;
+        }
+
+        /* Generate the FETCH command needed. */
+        $criteria = array();
+        foreach ($opts['sort'] as $val) {
+            switch ($val) {
+            case self::SORT_DATE:
+                $criteria[self::FETCH_DATE] = true;
+                // Fall through
+
+            case self::SORT_CC:
+            case self::SORT_FROM:
+            case self::SORT_SUBJECT:
+            case self::SORT_TO:
+                $criteria[self::FETCH_ENVELOPE] = true;
+                break;
+
+            case self::SORT_SIZE:
+                $criteria[self::FETCH_SIZE] = true;
+                break;
+            }
+        }
+
+        /* Get the FETCH results now. */
+        if (!empty($criteria)) {
+            $fetch_res = $this->fetch($this->_selected, $criteria, array('ids' => $res, 'sequence' => $opts['sequence']));
+        }
+
+        /* The initial sort is on the entire set. */
+        $slices = array(0 => $res);
+
+        $reverse = false;
+        foreach ($opts['sort'] as $val) {
+            if ($val == self::SORT_REVERSE) {
+                $reverse = true;
+                continue;
+            }
+
+            $slices_list = $slices;
+            $slices = array();
+
+            foreach ($slices_list as $slice_start => $slice) {
+                $sorted = array();
+
+                if ($reverse) {
+                    $slice = array_reverse($slice);
+                }
+
+                switch ($val) {
+                case self::SORT_ARRIVAL:
+                    /* There is no requirement that IDs be returned in
+                     * sequence order (see RFC 4549 [4.3.1]). So we must sort
+                     * ourselves. */
+                    $sorted = $slice;
+                    sort($sorted, SORT_NUMERIC);
+                    break;
+
+                case self::SORT_SIZE:
+                    foreach ($slice as $num) {
+                        $sorted[$num] = $fetch_res[$num]['size'];
+                    }
+                    asort($sorted, SORT_NUMERIC);
+                    break;
+
+                case self::SORT_CC:
+                case self::SORT_FROM:
+                case self::SORT_TO:
+                    if ($val == self::SORT_CC) {
+                        $field = 'cc';
+                    } elseif ($val = self::SORT_FROM) {
+                        $field = 'from';
+                    } else {
+                        $field = 'to';
+                    }
+
+                    foreach ($slice as $num) {
+                        $sorted[$num] = empty($fetch_res[$num]['envelope'][$field])
+                            ? null
+                            : $fetch_res[$num]['envelope'][$field][0]['mailbox'];
+                    }
+                    asort($sorted, SORT_LOCALE_STRING);
+                    break;
+
+                case self::SORT_DATE:
+                    // Date sorting rules in RFC 5256 [2.2]
+                    $sorted = $this->_getSentDates($fetch_res, $slice);
+                    asort($sorted, SORT_NUMERIC);
+                    break;
+
+                case self::SORT_SUBJECT:
+                    // Subject sorting rules in RFC 5256 [2.1]
+                    foreach ($slice as $num) {
+                        $sorted[$num] = empty($fetch_res[$num]['envelope']['subject'])
+                            ? ''
+                            : $this->getBaseSubject($fetch_res[$num]['envelope']['subject']);
+                    }
+                    asort($sorted, SORT_LOCALE_STRING);
+                    break;
+                }
+
+                // At this point, keys of $sorted are sequence/UID and values
+                // are the sort strings
+                if (!empty($sorted)) {
+                    if (count($sorted) == count($res)) {
+                        $res = array_keys($sorted);
+                    } else {
+                        array_splice($res, $slice_start, count($slice), array_keys($sorted));
+                    }
+
+                    // Check for ties.
+                    $last = $start = null;
+                    $i = 0;
+                    reset($sorted);
+                    while (list($k, $v) = each($sorted)) {
+                        if (is_null($last) || ($last != $v)) {
+                            if ($i) {
+                                $slices[array_search($res, $start)] = array_slice($sorted, array_search($sorted, $start), $i + 1);
+                                $i = 0;
+                            }
+                            $last = $v;
+                            $start = $k;
+                        } else {
+                            ++$i;
+                        }
+                    }
+                    if ($i) {
+                        $slices[array_search($res, $start)] = array_slice($sorted, array_search($sorted, $start), $i + 1);
+                    }
+                }
+            }
+
+            $reverse = false;
+        }
+
+        return $res;
+    }
+
+    /**
+     * Get the sent dates for purposes of SORT/THREAD sorting under RFC 5256
+     * [2.2].
+     *
+     * @param array $data  Data returned from fetch() that includes both the
+     *                     'envelope' and 'date' keys.
+     * @param array $ids   The IDs to process.
+     *
+     * @return array  A mapping of IDs -> UNIX timestamps.
+     */
+    protected function _getSentDates($data, $ids)
+    {
+        $dates = array();
+
+        $tz = new DateTimeZone('UTC');
+        foreach ($ids as $num) {
+            if (empty($data[$num]['envelope']['date'])) {
+                $dt = $data[$num]['date'];
+                $dt->setTimezone($tz);
+            } else {
+                $dt = new DateTime($data[$num]['envelope']['date'], $tz);
+            }
+            $dates[$num] = $dt->format('U');
+        }
+
+        return $dates;
+    }
+
+    /**
+     * Set the comparator to use for searching/sorting (RFC 5255).
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $comparator  The comparator string (see RFC 4790 [3.1] -
+     *                            "collation-id" - for format). The reserved
+     *                            string 'default' can be used to select
+     *                            the default comparator.
+     */
+    protected function _setComparator($comparator)
+    {
+        $this->_login();
+
+        $cmd = array();
+        foreach (explode(' ', $comparator) as $val) {
+            $cmd[] = $this->escape($val);
+        }
+
+        $this->_sendLine('COMPARATOR ' . implode(' ', $cmd));
+    }
+
+    /**
+     * Get the comparator used for searching/sorting (RFC 5255).
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @return mixed  Null if the default comparator is being used, or an
+     *                array of comparator information (see RFC 5255 [4.8]).
+     */
+    protected function _getComparator()
+    {
+        $this->_login();
+
+        $this->_sendLine('COMPARATOR');
+
+        return isset($this->_temp['comparator']) ? $this->_temp['comparator'] : null;
+    }
+
+    /**
+     * Parse a COMPARATOR response (RFC 5255 [4.8])
+     *
+     * @param array $data  The server response.
+     */
+    protected function _parseComparator($data)
+    {
+        $this->_temp['comparator'] = $data;
+    }
+
+    /**
+     * Thread sort a given list of messages (RFC 5256).
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param array $options  Additional options.
+     *
+     * @return array  See Horde_Imap_Client_Base::_thread().
+     */
+    protected function _thread($options)
+    {
+        $thread_criteria = array(
+            self::THREAD_ORDEREDSUBJECT => 'ORDEREDSUBJECT',
+            self::THREAD_REFERENCES => 'REFERENCES'
+        );
+
+        $tsort = (isset($options['criteria']))
+            ? (is_string($options['criteria']) ? strtoupper($options['criteria']) : $thread_criteria[$options['criteria']])
+            : 'REFERENCES';
+
+        $cap = $this->queryCapability('THREAD');
+        if (!$cap || !in_array($tsort, $cap)) {
+            if ($tsort == 'ORDEREDSUBJECT') {
+                if (empty($options['search'])) {
+                    $ids = array();
+                } else {
+                    $search_res = $this->search($this->_selected, $options['search'], array('sequence' => !empty($options['sequence'])));
+                    $ids = $search_res['match'];
+                }
+
+                /* Do client-side ORDEREDSUBJECT threading. */
+                $fetch_res = $this->fetch($this->_selected, array(self::FETCH_ENVELOPE => true, self::FETCH_DATE => true), array('ids' => $ids, 'sequence' => !empty($options['sequence'])));
+                return $this->_clientThreadOrderedsubject($fetch_res);
+            } else {
+                throw new Horde_Imap_Client_Exception('Server does not support REFERENCES thread sort.', Horde_Imap_Client_Exception::NOSUPPORTIMAPEXT);
+            }
+        }
+
+        if (empty($options['search'])) {
+            $charset = 'US-ASCII';
+            $search = 'ALL';
+        } else {
+            $search_query = $options['search']->build();
+            $charset = $search_query['charset'];
+            $search = $search_query['query'];
+        }
+
+        $this->_temp['threadresp'] = array();
+        $this->_sendLine((empty($options['sequence']) ? 'UID ' : '') . 'THREAD ' . $tsort . ' ' . $charset . ' ' . $search);
+
+        return $this->_temp['threadresp'];
+    }
+
+    /**
+     * Parse a THREAD response (RFC 5256 [4]).
+     *
+     * @param array $data      An array of thread token data.
+     * @param boolean $islast  Is this the last item in the level?
+     * @param integer $level   The current tree level.
+     */
+    protected function _parseThread($data, $level = 0, $islast = true)
+    {
+        $tb = &$this->_temp['threadbase'];
+        $tr = &$this->_temp['threadresp'];
+
+        if (!$level) {
+            $tb = null;
+        }
+        $cnt = count($data) - 1;
+
+        reset($data);
+        while (list($key, $val) = each($data)) {
+            if (is_array($val)) {
+                $this->_parseThread($val, $level, $cnt == $key);
+            } else {
+                if (is_null($tb) && ($level || $cnt)) {
+                    $tb = $val;
+                }
+                $tr[$val] = array(
+                    'base' => $tb,
+                    'last' => $islast,
+                    'level' => $level++,
+                    'id' => $val
+                );
+            }
+        }
+    }
+
+    /**
+     * If server does not support the THREAD IMAP extension (RFC 5256), do
+     * ORDEREDSUBJECT threading on the client side.
+     *
+     * @param array $res   The search results.
+     * @param array $opts  The options to search().
+     *
+     * @return array  The sort results.
+     */
+    protected function _clientThreadOrderedsubject($data)
+    {
+        $dates = $this->_getSentDates($data, array_keys($data));
+        $level = $sorted = $tsort = array();
+        $this->_temp['threadresp'] = array();
+
+        reset($data);
+        while(list($k, $v) = each($data)) {
+            $subject = empty($v['envelope']['subject'])
+                ? ''
+                : $this->getBaseSubject($v['envelope']['subject']);
+            if (!isset($sorted[$subject])) {
+                $sorted[$subject] = array();
+            }
+            $sorted[$subject][$k] = $dates[$k];
+        }
+
+        /* Step 1: Sort by base subject (already done).
+         * Step 2: Sort by sent date within each thread. */
+        foreach (array_keys($sorted) as $key) {
+            asort($sorted[$key], SORT_NUMERIC);
+            $tsort[$key] = reset($sorted[$key]);
+        }
+
+        /* Step 3: Sort by the sent date of the first message in the
+         * thread. */
+        asort($tsort, SORT_NUMERIC);
+
+        /* Now, $tsort contains the order of the threads, and each thread
+         * is sorted in $sorted. */
+        foreach (array_keys($tsort) as $key) {
+            $keys = array_keys($sorted[$key]);
+            $tmp = array($keys[0]);
+            if (count($keys) > 1) {
+                $tmp[] = array_slice($keys, 1);
+            }
+            $this->_parseThread($tmp);
+        }
+
+        return $this->_temp['threadresp'];
+    }
+
+    /**
+     * Fetch message data.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @todo Provide a function that would allow streaming of large data
+     *       items like bodytext.
+     *
+     * @param array $criteria  The fetch criteria.
+     * @param array $options   Additional options.
+     *
+     * @return array  See self::fetch().
+     */
+    protected function _fetch($criteria, $options)
+    {
+        $t = &$this->_temp;
+        $t['fetchparams'] = array();
+        $fp = &$t['fetchparams'];
+        $fetch = array();
+
+        /* Build an IMAP4rev1 compliant FETCH query. We handle the following
+         * criteria:
+         *   BINARY[.PEEK][<section #>]<<partial>> (RFC 3516)
+         *     see BODY[] response
+         *   BINARY.SIZE[<section #>] (RFC 3516)
+         *   BODY
+         *   BODY[.PEEK][<section>]<<partial>>
+         *     <section> = HEADER, HEADER.FIELDS, HEADER.FIELDS.NOT, MIME,
+         *                 TEXT, empty
+         *     <<partial>> = 0.# (# of bytes)
+         *   BODYSTRUCTURE
+         *   ENVELOPE
+         *   FLAGS
+         *   INTERNALDATE
+         *   MODSEQ (RFC 4551)
+         *   RFC822.SIZE
+         *   UID
+         *
+         * No need to support these (can be built from other queries):
+         * ===========================================================
+         *   ALL macro => (FLAGS INTERNALDATE RFC822.SIZE ENVELOPE)
+         *   FAST macro => (FLAGS INTERNALDATE RFC822.SIZE)
+         *   FULL macro => (FLAGS INTERNALDATE RFC822.SIZE ENVELOPE BODY)
+         *   RFC822 => BODY[]
+         *   RFC822.HEADER => BODY[HEADER]
+         *   RFC822.TEXT => BODY[TEXT]
+         */
+        reset($criteria);
+        while (list($type, $c_val) = each($criteria)) {
+            if (!is_array($c_val)) {
+                $c_val = array();
+            }
+
+            switch ($type) {
+            case self::FETCH_STRUCTURE:
+                $fp['parsestructure'] = !empty($c_val['parse']);
+                $fetch[] = !empty($c_val['noext']) ? 'BODY' : 'BODYSTRUCTURE';
+                break;
+
+            case self::FETCH_FULLMSG:
+                if (empty($c_val['peek'])) {
+                    $this->openMailbox($this->_selected, self::OPEN_READWRITE);
+                }
+                $fetch[] = 'BODY' .
+                    (!empty($c_val['peek']) ? '.PEEK' : '') .
+                    '[]' .
+                    (isset($c_val['start']) && !empty($c_val['length']) ? ('<' . $c_val['start'] . '.' . $c_val['length'] . '>') : '');
+                break;
+
+            case self::FETCH_HEADERTEXT:
+            case self::FETCH_BODYTEXT:
+            case self::FETCH_MIMEHEADER:
+            case self::FETCH_BODYPART:
+            case self::FETCH_HEADERS:
+                foreach ($c_val as $val) {
+                    $main_cmd = 'BODY';
+
+                    if (empty($val['id'])) {
+                        $cmd = '';
+                    } else {
+                        $cmd = $val['id'] . '.';
+                    }
+
+                    switch ($type) {
+                    case self::FETCH_HEADERTEXT:
+                        $fp['parseheadertext'] = !empty($val['parse']);
+                        $cmd .= 'HEADER';
+                        break;
+
+                    case self::FETCH_BODYTEXT:
+                        $cmd .= 'TEXT';
+                        break;
+
+                    case self::FETCH_MIMEHEADER:
+                        if (empty($val['id'])) {
+                            throw new Horde_Imap_Client_Exception('Need a MIME ID when retrieving a MIME header.');
+                        }
+                        $cmd .= 'MIME';
+                        break;
+
+                    case self::FETCH_BODYPART:
+                        if (empty($val['id'])) {
+                            throw new Horde_Imap_Client_Exception('Need a MIME ID when retrieving a MIME body part.');
+                        }
+                        // Remove the last dot from the string.
+                        $cmd = substr($cmd, 0, -1);
+
+                        if (!empty($val['decode']) &&
+                            $this->queryCapability('BINARY')) {
+                            $main_cmd = 'BINARY';
+                        }
+                        break;
+
+                    case self::FETCH_HEADERS:
+                        if (empty($val['label'])) {
+                            throw new Horde_Imap_Client_Exception('Need a unique label when doing a headers field search.');
+                        }
+                        if (empty($val['headers'])) {
+                            throw new Horde_Imap_Client_Exception('Need headers to query when doing a headers field search.');
+                        }
+                        $fp['parseheaders'] = !empty($val['parse']);
+
+                        $cmd .= 'HEADER.FIELDS';
+                        if (!empty($val['notsearch'])) {
+                            $cmd .= '.NOT';
+                        }
+                        $cmd .= ' (' . implode(' ', array_map('strtoupper', $val['headers'])) . ')';
+
+                        // Maintain a command -> label lookup so we can put
+                        // the results in the proper location.
+                        if (!isset($fp['hdrfields'])) {
+                            $fp['hdrfields'] = array();
+                        }
+                        $fp['hdrfields'][$cmd] = $val['label'];
+                    }
+
+                    if (empty($c_val['peek'])) {
+                        $this->openMailbox($this->_selected, self::OPEN_READWRITE);
+                    }
+
+                    $fetch[] = $main_cmd .
+                        (!empty($c_val['peek']) ? '.PEEK' : '') .
+                        '[' . $cmd . ']' .
+                        (isset($c_val['start']) && !empty($c_val['length']) ? ('<' . $c_val['start'] . '.' . $c_val['length'] . '>') : '');
+                }
+                break;
+
+            case self::FETCH_BODYPARTSIZE:
+                foreach ($c_val as $val) {
+                    if (empty($val['id'])) {
+                        throw new Horde_Imap_Client_Exception('Need a MIME ID when retrieving unencoded MIME body part size.');
+                    }
+                    $fetch[] = 'BINARY.SIZE[' . $val['id'] . ']';
+                }
+                break;
+
+            case self::FETCH_ENVELOPE:
+                $fetch[] = 'ENVELOPE';
+                break;
+
+            case self::FETCH_FLAGS:
+                $fetch[] = 'FLAGS';
+                break;
+
+            case self::FETCH_DATE:
+                $fetch[] = 'INTERNALDATE';
+                break;
+
+            case self::FETCH_SIZE:
+                $fetch[] = 'RFC822.SIZE';
+                break;
+
+            case self::FETCH_UID:
+                $fetch[] = 'UID';
+                break;
+
+            case self::FETCH_SEQ:
+                // Nothing we need to add to fetch criteria.
+                break;
+
+            case self::FETCH_MODSEQ:
+                /* RFC 4551 [3.1] - trying to do a FETCH of MODSEQ on a
+                 * mailbox that doesn't support it will return BAD. Catch that
+                 * here and throw an exception. */
+                if (is_null($this->_temp['mailbox']['highestmodseq'])) {
+                    throw new Horde_Imap_Client_Exception('Mailbox does not support mod-sequences.', Horde_Imap_Client_Exception::MBOXNOMODSEQ);
+                }
+                $fetch[] = 'MODSEQ';
+                break;
+            }
+        }
+
+        $seq = empty($options['ids'])
+            ? '1:*'
+            : ((reset($options['ids']) === self::USE_SEARCHRES)
+                 ? '$'
+                 : $this->toSequenceString($options['ids']));
+        $use_seq = !empty($options['sequence']);
+
+        $cmd = ($use_seq ? '' : 'UID ') . 'FETCH ' . $seq . ' (' . implode(' ', $fetch) . ')';
+
+        if (!empty($options['changedsince'])) {
+            if (is_null($this->_temp['mailbox']['highestmodseq'])) {
+                throw new Horde_Imap_Client_Exception('Mailbox does not support mod-sequences.', Horde_Imap_Client_Exception::MBOXNOMODSEQ);
+            }
+            $cmd .= ' (CHANGEDSINCE ' . intval($options['changedsince']) . ')';
+        }
+
+        $this->_sendLine($cmd);
+
+        return $t['fetchresp'][$use_seq ? 'seq' : 'uid'];
+    }
+
+    /**
+     * Parse a FETCH response (RFC 3501 [7.4.2]). A FETCH response may occur
+     * due to a FETCH command, or due to a change in a message's state (i.e.
+     * the flags change).
+     *
+     * @param integer $id  The message sequence number.
+     * @param array $data  The server response.
+     */
+    protected function _parseFetch($id, $data)
+    {
+        $section_storage = array(
+            'HEADER' => 'headertext',
+            'TEXT' => 'bodytext',
+            'MIME' => 'mimeheader'
+        );
+
+        $i = 0;
+        $cnt = count($data);
+
+        if (isset($this->_temp['fetchresp']['seq'][$id])) {
+            $tmp = $this->_temp['fetchresp']['seq'][$id];
+            $uid = isset($tmp['uid']) ? $tmp['uid'] : null;
+        } else {
+            $tmp = array('seq' => $id);
+            $uid = null;
+        }
+
+        while ($i < $cnt) {
+            $tag = strtoupper($data[$i]);
+            switch ($tag) {
+            case 'BODY':
+            case 'BODYSTRUCTURE':
+                // Only care about these if doing a FETCH command.
+                $tmp['structure'] = empty($this->_temp['fetchparams']['parsestructure'])
+                    ? $this->_parseBodystructure($data[++$i])
+                    : Horde_MIME_Message::parseStructure($this->_parseBodystructure($data[++$i]));
+                break;
+
+            case 'ENVELOPE':
+                $tmp['envelope'] = $this->_parseEnvelope($data[++$i]);
+                break;
+
+            case 'FLAGS':
+                $tmp['flags'] = array_map('strtolower', $data[++$i]);
+                break;
+
+            case 'INTERNALDATE':
+                $tmp['date'] = new DateTime($data[++$i]);
+                break;
+
+            case 'RFC822.SIZE':
+                $tmp['size'] = $data[++$i];
+                break;
+
+            case 'UID':
+                $uid = $tmp['uid'] = $data[++$i];
+                break;
+
+            case 'MODSEQ':
+                $tmp['modseq'] = reset($data[++$i]);
+                break;
+
+            default:
+                // Catch BODY[*]<#> responses
+                if (strpos($tag, 'BODY[') === 0) {
+                    // Remove the beginning 'BODY['
+                    $tag = substr($tag, 5);
+
+                    // BODY[HEADER.FIELDS] request
+                    if (!empty($this->_temp['fetchparams']['hdrfields']) &&
+                        (strpos($tag, 'HEADER.FIELDS') !== false)) {
+                        if (!isset($tmp['headers'])) {
+                            $tmp['headers'] = array();
+                        }
+
+                        // A HEADER.FIELDS entry will be tokenized thusly:
+                        //   [0] => BODY[#.HEADER.FIELDS.NOT
+                        //   [1] => Array
+                        //     (
+                        //       [0] => MESSAGE-ID
+                        //     )
+                        //   [2] => ]<0>
+                        //   [3] => **Header search text**
+                        $sig = $tag . ' (' . implode(' ', array_map('strtoupper', $data[++$i])) . ')';
+
+                        // Ignore the trailing bracket
+                        ++$i;
+
+                        $tmp['headers'][$this->_temp['fetchparams']['hdrfields'][$sig]] = empty($this->_temp['fetchparams']['parseheaders'])
+                            ? $data[++$i]
+                            : Horde_MIME_Headers::parseHeaders($data[++$i]);
+                    } else {
+                        // Remove trailing bracket and octet start info
+                        $tag = substr($tag, 0, strrpos($tag, ']'));
+
+                        if (!strlen($tag)) {
+                            // BODY[] request
+                            $tmp['fullmsg'] = $data[++$i];
+                        } elseif (is_numeric(substr($tag, -1))) {
+                            // BODY[MIMEID] request
+                            if (!isset($tmp['bodypart'])) {
+                                $tmp['bodypart'] = array();
+                            }
+                            $tmp['bodypart'][$tag] = $data[++$i];
+                        } else {
+                            // BODY[HEADER|TEXT|MIME] request
+                            if (($last_dot = strrpos($tag, '.')) === false) {
+                                $mime_id = 0;
+                            } else {
+                                $mime_id = substr($tag, 0, $last_dot);
+                                $tag = substr($tag, $last_dot + 1);
+                            }
+
+                            $label = $section_storage[$tag];
+
+                            if (!isset($tmp[$label])) {
+                                $tmp[$label] = array();
+                            }
+                            $tmp[$label][$mime_id] = empty($this->_temp['fetchparams']['parseheadertext'])
+                                ? $data[++$i]
+                                : Horde_MIME_Headers::parseHeaders($data[++$i]);
+                        }
+                    }
+                } elseif (strpos($tag, 'BINARY[') === 0) {
+                    // Catch BINARY[*]<#> responses
+                    // Remove the beginning 'BINARY[' and the trailing bracket
+                    // and octet start info
+                    $tag = substr($tag, 7, strrpos($tag, ']') - 7);
+                    if (!isset($tmp['bodypart'])) {
+                        $tmp['bodypart'] = $tmp['bodypartdecode'] = array();
+                    }
+                    $tmp['bodypart'][$tag] = $data[++$i];
+                    $tmp['bodypartdecode'][$tag] = !empty($this->_temp['literal8']) ? 'binary': '8bit';
+                } elseif (strpos($tag, 'BINARY.SIZE[') === 0) {
+                    // Catch BINARY.SIZE[*] responses
+                    // Remove the beginning 'BINARY.SIZE[' and the trailing
+                    // bracket and octet start info
+                    $tag = substr($tag, 12, strrpos($tag, ']') - 12);
+                    if (!isset($tmp['bodypartsize'])) {
+                        $tmp['bodypartsize'] = array();
+                    }
+                    $tmp['bodypartsize'][$tag] = $data[++$i];
+                }
+                break;
+            }
+
+            ++$i;
+        }
+
+        $this->_temp['fetchresp']['seq'][$id] = $tmp;
+        if (!is_null($uid)) {
+            $this->_temp['fetchresp']['uid'][$uid] = $tmp;
+        }
+    }
+
+    /**
+     * Recursively parse BODYSTRUCTURE data from a FETCH return (see
+     * RFC 3501 [7.4.2]).
+     *
+     * @param array $data  The tokenized information from the server.
+     *
+     * @return array  The array of bodystructure information.
+     */
+    protected function _parseBodystructure($data)
+    {
+        // If index 0 is an array, this is a multipart part.
+        if (is_array($data[0])) {
+            $ret = array(
+                'parts' => array(),
+                'type' => 'multipart'
+            );
+
+            // Keep going through array values until we find a non-array.
+            for ($i = 0, $cnt = count($data); $i < $cnt; ++$i) {
+                if (!is_array($data[$i])) {
+                    break;
+                }
+                $ret['parts'][] = $this->_parseBodystructure($data[$i]);
+            }
+
+            // The first string entry after an array entry gives us the
+            // subpart type.
+            $ret['subtype'] = strtolower($data[$i]);
+
+            // After the subtype is further extension information. This
+            // information won't be present if this is a BODY request, and
+            // MAY not appear for BODYSTRUCTURE requests.
+
+            // This is parameter information.
+            if (isset($data[++$i]) && is_array($data[$i])) {
+                $ret['parameters'] = $this->_parseStructureParams($data[$i]);
+            }
+
+            // This is disposition information.
+            if (isset($data[++$i]) && is_array($data[$i])) {
+                $ret['disposition'] = strtolower($data[$i][0]);
+                $ret['dparameters'] = $this->_parseStructureParams($data[$i][1]);
+            }
+
+            // This is body language information.
+            if (isset($data[++$i])) {
+                if (is_array($data[$i])) {
+                    $ret['language'] = $data[$i];
+                } elseif ($data[$i] != 'NIL') {
+                    $ret['language'] = array($data[$i]);
+                }
+            }
+
+            // This is body location information
+            if (isset($data[++$i]) && ($data[$i] != 'NIL')) {
+                $ret['location'] = $data[$i];
+            }
+
+            // There can be further information returned in the future, but
+            // for now we are done.
+        } else {
+            $ret = array(
+                'type' => strtolower($data[0]),
+                'subtype' => strtolower($data[1]),
+                'parameters' => $this->_parseStructureParams($data[2]),
+                'id' => ($data[3] == 'NIL') ? null : $data[3],
+                'description' => ($data[4] == 'NIL') ? null : $data[4],
+                'encoding' => ($data[5] == 'NIL') ? null : strtolower($data[5]),
+                'size' => ($data[6] == 'NIL') ? null : $data[6]
+            );
+
+            // If the type is 'message/rfc822' or 'text/*', several extra
+            // fields are included
+            switch ($ret['type']) {
+            case 'message':
+                if ($ret['subtype'] == 'rfc822') {
+                    $ret['envelope'] = $this->_parseEnvelope($data[7]);
+                    $ret['structure'] = $this->_parseBodystructure($data[8]);
+                    $ret['lines'] = $data[9];
+                    $i = 10;
+                } else {
+                    $i = 7;
+                }
+                break;
+
+            case 'text':
+                $ret['lines'] = $data[7];
+                $i = 8;
+                break;
+
+            default:
+                $i = 7;
+                break;
+            }
+
+            // After the subtype is further extension information. This
+            // information won't be present if this is a BODY request, and
+            // MAY not appear for BODYSTRUCTURE requests.
+
+            // This is MD5 information
+            if (isset($data[$i]) && ($data[$i] != 'NIL')) {
+                $ret['md5'] = $data[$i];
+            }
+
+            // This is disposition information
+            if (isset($data[++$i]) && is_array($data[$i])) {
+                $ret['disposition'] = strtolower($data[$i][0]);
+                $ret['dparameters'] = $this->_parseStructureParams($data[$i][1]);
+            }
+
+            // This is body language information.
+            if (isset($data[++$i])) {
+                if (is_array($data[$i])) {
+                    $ret['language'] = $data[$i];
+                } elseif ($data[$i] != 'NIL') {
+                    $ret['language'] = array($data[$i]);
+                }
+            }
+
+            // This is body location information
+            if (isset($data[++$i]) && ($data[$i] != 'NIL')) {
+                $ret['location'] = $data[$i];
+            }
+        }
+
+        return $ret;
+    }
+
+    /**
+     * Helper function to parse a parameters-like tokenized array.
+     *
+     * @param array $data  The tokenized data.
+     *
+     * @return array  The parameter array.
+     */
+    protected function _parseStructureParams($data)
+    {
+        $ret = array();
+
+        if (is_array($data)) {
+            for ($i = 0, $cnt = count($data); $i < $cnt; ++$i) {
+                $ret[strtolower($data[$i])] = $data[++$i];
+            }
+        }
+
+        return $ret;
+    }
+
+    /**
+     * Parse ENVELOPE data from a FETCH return (see RFC 3501 [7.4.2]).
+     *
+     * @param array $data  The tokenized information from the server.
+     *
+     * @return array  The array of envelope information.
+     */
+    protected function _parseEnvelope($data)
+    {
+        $addr_structure = array(
+            'personal', 'adl', 'mailbox', 'host'
+        );
+        $env_data = array(
+            0 => 'date',
+            1 => 'subject',
+            8 => 'in-reply-to',
+            9 => 'message-id'
+        );
+        $env_data_array = array(
+            2 => 'from',
+            3 => 'sender',
+            4 => 'reply-to',
+            5 => 'to',
+            6 => 'cc',
+            7 => 'bcc'
+        );
+
+        $ret = array();
+
+        foreach ($env_data as $key => $val) {
+            $ret[$val] = (strtoupper($data[$key]) == 'NIL') ? null : $data[$key];
+        }
+
+        // These entries are address structures.
+        foreach ($env_data_array as $key => $val) {
+            $ret[$val] = array();
+            // Check for 'NIL' value here.
+            if (is_array($data[$key])) {
+                reset($data[$key]);
+                while (list(,$a_val) = each($data[$key])) {
+                    $tmp_addr = array();
+                    foreach ($addr_structure as $add_key => $add_val) {
+                        if (strtoupper($a_val[$add_key]) != 'NIL') {
+                            $tmp_addr[$add_val] = $a_val[$add_key];
+                        }
+                    }
+                    $ret[$val][] = $tmp_addr;
+                }
+            }
+        }
+
+        return $ret;
+    }
+
+    /**
+     * Store message flag data.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param array $options  Additional options.
+     *
+     * @return array  See Horde_Imap_Client::store().
+     */
+    protected function _store($options)
+    {
+        $seq = empty($options['ids'])
+            ? '1:*'
+            : ((reset($options['ids']) === self::USE_SEARCHRES)
+                 ? '$'
+                 : $this->toSequenceString($options['ids']));
+
+        $cmd_prefix = (empty($options['sequence']) ? 'UID ' : '') .
+                      'STORE ' . $seq . ' ';
+        $ucsince = !empty($options['unchangedsince']);
+
+        if ($ucsince) {
+            /* RFC 4551 [3.1] - trying to do a UNCHANGEDSINCE STORE on a
+             * mailbox that doesn't support it will return BAD. Catch that
+             * here and throw an exception. */
+            if (is_null($this->_temp['mailbox']['highestmodseq'])) {
+                throw new Horde_Imap_Client_Exception('Mailbox does not support mod-sequences.', Horde_Imap_Client_Exception::MBOXNOMODSEQ);
+            }
+
+            $cmd .= '(UNCHANGEDSINCE ' . intval($options['unchangedsince']) . ') ';
+        }
+
+        $this->_temp['modified'] = array();
+
+        if (!empty($options['replace'])) {
+            $this->_sendLine($cmd_prefix . 'FLAGS' . ($this->_debug ? '.SILENT' : '') . ' (' . implode(' ', $options['replace']) . ')');
+        } else {
+            foreach (array('add' => '+', 'remove' => '-') as $k => $v) {
+                if (!empty($options[$k])) {
+                    $this->_sendLine($cmd_prefix . $v . 'FLAGS' . ($this->_debug ? '.SILENT' : '') . ' (' . implode(' ', $options[$k]) . ')');
+                }
+            }
+        }
+
+        /* Update the flags in the cache. Only update if store was successful
+         * and flag information was not returned. */
+        if (!empty($this->_temp['fetchresp']) &&
+            isset($this->_init['enabled']['CONDSTORE'])) {
+            $fr = $this->_temp['fetchresp'];
+            $out = $uids = array();
+
+            if (empty($fr['uid'])) {
+                $res = $fr['seq'];
+                $seq_res = $this->_getSeqUIDLookup(array_keys($res), true);
+            } else {
+                $res = $fr['uid'];
+                $seq_res = null;
+            }
+
+            foreach (array_keys($res) as $key) {
+                if (!isset($res[$key]['flags'])) {
+                    $uids[is_null($seq_res) ? $key : $seq_res['lookup'][$key]] = $res[$key]['modseq'];
+                }
+            }
+
+            /* Get the list of flags from the cache. */
+            if (empty($options['replace'])) {
+                $data = $this->_cacheOb->get($this->_selected, array_keys($uids), array('HICflags'), $this->_temp['mailbox']['uidvalidity']);
+
+                foreach ($uids as $uid => $modseq) {
+                    $flags = isset($data[$uid]['HICflags']) ? $data[$uid]['HICflags'] : array();
+                    if (!empty($options['add'])) {
+                        $flags = array_merge($flags, $options['add']);
+                    }
+                    if (!empty($options['remove'])) {
+                        $flags = array_diff($flags, $options['remove']);
+                    }
+                    $out[$uid] = array('modseq' => $uids[$uid], 'flags' => array_keys(array_flip($flags)));
+                }
+            } else {
+                foreach ($uids as $uid => $modseq) {
+                    $out[$uid] = array('modseq' => $uids[$uid], 'flags' => $options['replace']);
+                }
+            }
+
+            $this->_updateCache($out);
+        }
+
+        return $this->_temp['modified'];
+    }
+
+    /**
+     * Copy messages to another mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $dest    The destination mailbox (UTF7-IMAP).
+     * @param array $options  Additional options.
+     *
+     * @return mixed  An array mapping old UIDs (keys) to new UIDs (values) on
+     *                success (if the IMAP server and/or driver support the
+     *                UIDPLUS extension) or true.
+     */
+    protected function _copy($dest, $options)
+    {
+        $this->_temp['copyuid'] = $this->_temp['trycreate'] = null;
+        $this->_temp['uidplusmbox'] = $dest;
+
+        $seq = empty($options['ids'])
+            ? '1:*'
+            : ((reset($options['ids']) === self::USE_SEARCHRES)
+                 ? '$'
+                 : $this->toSequenceString($options['ids']));
+
+        // COPY returns no untagged information (RFC 3501 [6.4.7])
+        try {
+            $this->_sendLine((empty($options['sequence']) ? 'UID ' : '') . 'COPY ' . $seq . ' ' . $this->escape($dest));
+        } catch (Horde_Imap_Client_Exception $e) {
+            if (!empty($options['create']) && $this->_temp['trycreate']) {
+                $this->createMailbox($dest);
+                unset($options['create']);
+                return $this->_copy($dest, $options);
+            }
+            throw $e;
+        }
+
+        // If moving, delete the old messages now.
+        if (!empty($options['move'])) {
+            $opts = array('ids' => empty($options['ids']) ? array() : $options['ids'], 'sequence' => !empty($options['sequence']));
+            $this->store($this->_selected, array_merge(array('add' => array('\\deleted')), $opts));
+            $this->expunge($this->_selected, $opts);
+        }
+
+        /* UIDPLUS (RFC 4315) allows easy determination of the UID of the
+         * copied messages. If UID not returned, then destination mailbox
+         * does not support persistent UIDs.
+         * @todo Use UIDPLUS information to move cached data to new
+         * mailbox (see RFC 4549 [4.2.2.1]). */
+        return is_null($this->_temp['copyuid'])
+            ? true
+            : $this->_temp['copyuid'];
+    }
+
+    /**
+     * Set quota limits.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $root    The quota root (UTF7-IMAP).
+     * @param array $options  Additional options.
+     */
+    protected function _setQuota($root, $options)
+    {
+        $this->login();
+
+        $limits = array();
+        if (isset($options['messages'])) {
+            $limits[] = 'MESSAGE ' . $options['messages'];
+        }
+        if (isset($options['storage'])) {
+            $limits[] = 'STORAGE ' . $options['storage'];
+        }
+
+        $this->_sendLine('SETQUOTA ' . $this->escape($root) . ' (' . implode(' ', $limits) . ')');
+    }
+
+    /**
+     * Get quota limits.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $root  The quota root (UTF7-IMAP).
+     *
+     * @return mixed  An array with these possible keys: 'messages' and
+     *                'storage'; each key holds an array with 2 values:
+     *                'limit' and 'usage'.
+     */
+    protected function _getQuota($root)
+    {
+        $this->login();
+
+        $this->_temp['quotaresp'] = array();
+        $this->_sendLine('GETQUOTA ' . $this->escape($root));
+        return reset($this->_temp['quotaresp']);
+    }
+
+    /**
+     * Parse a QUOTA response (RFC 2087 [5.1]).
+     *
+     * @param array $data  The server response.
+     */
+    protected function _parseQuota($data)
+    {
+        $c = &$this->_temp['quotaresp'];
+
+        $root = $data[0];
+        $c[$root] = array();
+
+        for ($i = 0, $len = count($data[1]); $i < $len; $i += 3) {
+            if (in_array($data[1][$i], array('MESSAGE', 'STORAGE'))) {
+                $c[$root][strtolower($data[1][$i])] = array('limit' => $data[1][$i + 2], 'usage' => $data[1][$i + 1]);
+
+            }
+        }
+    }
+
+    /**
+     * Get quota limits for a mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox  A mailbox (UTF7-IMAP).
+     *
+     * @return mixed  An array with the keys being the quota roots. Each key
+     *                holds an array with two possible keys: 'messages' and
+     *                'storage'; each of these keys holds an array with 2
+     *                values: 'limit' and 'usage'.
+     */
+    protected function _getQuotaRoot($mailbox)
+    {
+        $this->login();
+
+        $this->_temp['quotaresp'] = array();
+        $this->_sendLine('GETQUOTAROOT ' . $this->escape($mailbox));
+        return $this->_temp['quotaresp'];
+    }
+
+    /**
+     * Set ACL rights for a given mailbox/identifier.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox     A mailbox (UTF7-IMAP).
+     * @param string $identifier  The identifier to alter (UTF7-IMAP).
+     * @param array $options      Additional options.
+     */
+    protected function _setACL($mailbox, $identifier, $options)
+    {
+        $this->login();
+
+        // SETACL/DELETEACL returns no untagged information (RFC 4314 [3.1 &
+        // 3.2]).
+        if (empty($options['rights']) && !empty($options['remove'])) {
+            $this->_sendLine('DELETEACL ' . $this->escape($mailbox) . ' ' . $identifier);
+        } else {
+            $this->_sendLine('SETACL ' . $this->escape($mailbox) . ' ' . $identifier . ' ' . $options['rights']);
+        }
+    }
+
+    /**
+     * Get ACL rights for a given mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox  A mailbox (UTF7-IMAP).
+     *
+     * @return array  An array with identifiers as the keys and an array of
+     *                rights as the values.
+     */
+    protected function _getACL($mailbox)
+    {
+        $this->login();
+
+        $this->_temp['getacl'] = array();
+        $this->_sendLine('GETACL ' . $this->escape($mailbox));
+        return $this->_temp['getacl'];
+    }
+
+    /**
+     * Parse an ACL response (RFC 4314 [3.6]).
+     *
+     * @param array $data  The server response.
+     */
+    protected function _parseACL($data)
+    {
+        $acl = &$this->_temp['getacl'];
+
+        // Ignore mailbox argument -> index 1
+        for ($i = 1, $len = count($data); $i < $len; $i += 2) {
+            $acl[$data[$i]] = str_split($data[$i + 1]);
+        }
+    }
+
+    /**
+     * Get ACL rights for a given mailbox/identifier.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox     A mailbox (UTF7-IMAP).
+     * @param string $identifier  The identifier (US-ASCII).
+     *
+     * @return array  An array of rights (keys: 'required' and 'optional').
+     */
+    protected function _listACLRights($mailbox, $identifier)
+    {
+        $this->login();
+
+        $this->_temp['listaclrights'] = array();
+        $this->_sendLine('LISTRIGHTS ' . $this->escape($mailbox) . ' ' . $identifier);
+        return $this->_temp['listaclrights'];
+    }
+
+    /**
+     * Parse a LISTRIGHTS response (RFC 4314 [3.7]).
+     *
+     * @param array $data  The server response.
+     */
+    protected function _parseListRights($data)
+    {
+        // Ignore mailbox and identifier arguments
+        $this->_temp['myrights'] = array(
+            'required' => str_split($data[2]),
+            'optional' => array_slice($data, 3)
+        );
+    }
+
+    /**
+     * Get the ACL rights for the current user for a given mailbox.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $mailbox  A mailbox (UTF7-IMAP).
+     *
+     * @return array  An array of rights.
+     */
+    protected function _getMyACLRights($mailbox)
+    {
+        $this->login();
+
+        $this->_temp['myrights'] = array();
+        $this->_sendLine('MYRIGHTS ' . $this->escape($mailbox));
+        return $this->_temp['myrights'];
+    }
+
+    /**
+     * Parse a MYRIGHTS response (RFC 4314 [3.8]).
+     *
+     * @param array $data  The server response.
+     */
+    protected function _parseMyRights($data)
+    {
+        $this->_temp['myrights'] = $data[1];
+    }
+
+    /* Internal functions. */
+
+    /**
+     * Perform a command on the IMAP server. A connection to the server must
+     * have already been made.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @todo RFC 3501 allows the sending of multiple commands at once. For
+     *       simplicity of implementation at this time, we will execute
+     *       commands one at a time. This allows us to easily determine data
+     *       meant for a command while scanning for untagged responses
+     *       unilaterally sent by the server.
+     *
+     * @param string $query   The IMAP command to execute.
+     * @param array $options  Additional options:
+     * <pre>
+     * 'binary' - (boolean) Does $query contain binary data?  If so, and the
+     *            'BINARY' extension is available on the server, the data
+     *            will be sent in literal8 format. If not available, an
+     *            exception will be returned. 'binary' requires literal to
+     *            be defined.
+     *            DEFAULT: Sends literals in a non-binary compliant method.
+     * 'debug' - (string) When debugging, send this string instead of the
+     *           actual command/data sent.
+     *           DEFAULT: Raw data output to debug stream.
+     * 'literal' - (integer) Send the command followed by a literal. The value
+     *             of 'literal' is the length of the literal data.
+     *             Will attempt to use LITERAL+ capability if possible.
+     *             DEFAULT: Do not send literal
+     * 'literaldata' - (boolean) Is this literal data?  If so, will parse the
+     *                 server response based on the existence of LITERAL+.
+     *                 DEFAULT: Server specific.
+     * 'noparse' - (boolean) Don't parse the response and instead return the
+     *             server response.
+     *             DEFAULT: Parses the response
+     * 'notag' - (boolean) Don't prepend an IMAP tag (i.e. for a continuation
+     *           response).
+     *           DEFAULT: false
+     * </pre>
+     */
+    protected function _sendLine($query, $options = array())
+    {
+        if (empty($options['notag'])) {
+            $query = ++$this->_tag . ' ' . $query;
+
+            /* Catch all FETCH responses until a tagged response. */
+            $this->_temp['fetchresp'] = array('seq' => array(), 'uid' => array());
+        }
+
+        $continuation = $literalplus = false;
+
+        if (!empty($options['literal']) || !empty($options['literaldata'])) {
+            if ($this->queryCapability('LITERAL+')) {
+                /* RFC 2088 - If LITERAL+ is available, saves a roundtrip
+                 * from the server. */
+                $literalplus = true;
+            } else {
+                $continuation = true;
+            }
+
+            if (!empty($options['literal'])) {
+                $query .= ' ';
+
+                // RFC 3516 - Send literal8 if we have binary data.
+                if (!empty($options['binary'])) {
+                    if (!$this->queryCapability('BINARY')) {
+                        throw new Horde_Imap_Client_Exception('Can not send binary data to server that does not support it.', Horde_Imap_Client_Exception::NOSUPPORTIMAPEXT);
+                    }
+                    $query .= '~';
+                }
+
+                $query .= '{' . $options['literal'] . ($literalplus ? '+' : '') . '}';
+            }
+        }
+
+        if ($this->_debug) {
+            fwrite($this->_debug, 'C: ' . (empty($options['debug']) ? $query : $options['debug']) . "\n");
+        }
+
+        fwrite($this->_stream, $query . "\r\n");
+
+        if ($literalplus) {
+            return;
+        }
+
+        if ($continuation) {
+            $ob = $this->_getLine();
+            if ($ob['type'] != 'continuation') {
+                throw new Horde_Imap_Client_Exception('Unexpected response from IMAP server while waiting for a continuation request: ' . $ob['line']);
+            }
+        } elseif (empty($options['noparse'])) {
+            $this->_parseResponse($this->_tag);
+        } else {
+            return $this->_getLine();
+        }
+    }
+
+    /**
+     * Gets a line from the IMAP stream and parses it.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @return array  An array with the following keys:
+     * <pre>
+     * 'line' - (string) The server response text (set for all but an untagged
+     *          response with no response code).
+     * 'response' - (string) Either 'OK', 'NO', 'BAD', 'PREAUTH', or ''.
+     * 'tag' - (string) If tagged response, the tag string.
+     * 'token' - (array) The tokenized response (set if an untagged response
+     *           with no response code).
+     * 'type' - (string) Either 'tagged', 'untagged', or 'continuation'.
+     * </pre>
+     */
+    protected function _getLine()
+    {
+        $ob = array('line' => '', 'response' => '', 'tag' => '', 'token' => '');
+
+        if (feof($this->_stream)) {
+            $this->_temp['logout'] = 2;
+            $this->logout();
+            throw new Horde_Imap_Client_Exception('IMAP Server closed the connection unexpectedly.', Horde_Imap_Client_Exception::IMAP_DISCONNECT);
+        }
+
+        $read = rtrim(fgets($this->_stream));
+        if (empty($read)) {
+            return;
+        }
+
+        if ($this->_debug) {
+            fwrite($this->_debug, 'S: ' . $read . "\n");
+        }
+
+        $read = explode(' ', $read, 3);
+
+        switch ($read[0]) {
+        /* Continuation response. */
+        case '+':
+            $ob['line'] = implode(' ', array_slice($read, 1));
+            $ob['type'] = 'continuation';
+            break;
+
+        /* Untagged response. */
+        case '*':
+            $ob['type'] = 'untagged';
+
+            $read[1] = strtoupper($read[1]);
+            if ($read[1] == 'BYE') {
+                if (!empty($this->_temp['logout']) &&
+                    ($this->_temp['logout'] == 1)) {
+                    /* A BYE response received as part of a logout cmd should
+                     * be treated like a regular command. A client MUST
+                     * process the entire command until logging out. RFC 3501
+                     * [3.4]. */
+                    $ob['response'] = $read[1];
+                    $ob['line'] = implode(' ', array_slice($read, 2));
+                } else {
+                    $this->_temp['logout'] = 2;
+                    $this->logout();
+                    throw new Horde_Imap_Client_Exception('IMAP Server closed the connection: ' . implode(' ', array_slice($read, 1)), Horde_Imap_Client_Exception::IMAP_DISCONNECT);
+                }
+            }
+
+            if (in_array($read[1], array('OK', 'NO', 'BAD', 'PREAUTH'))) {
+                $ob['response'] = $read[1];
+                $ob['line'] = implode(' ', array_slice($read, 2));
+            } else {
+                /* Tokenize response. */
+                $line = implode(' ', array_slice($read, 1));
+                $binary = $literal = false;
+                $this->_temp['token'] = null;
+                $this->_temp['literal8'] = array();
+
+                do {
+                    $literal_len = null;
+
+                    if (!$literal && (substr($line, -1) == '}')) {
+                        $pos = strrpos($line, '{');
+                        $literal_len = substr($line, $pos + 1, -1);
+                        if (is_numeric($literal_len)) {
+
+                            // Check for literal8 response
+                            if ($line[$pos - 1] == '~') {
+                                $binary = true;
+                                $line = substr($line, 0, $pos - 1);
+                                $this->_temp['literal8'][substr($line, strrpos($line, ' '))] = true;
+                            } else {
+                                $line = substr($line, 0, $pos);
+                            }
+                        } else {
+                            $literal_len = null;
+                        }
+                    }
+
+                    if ($literal) {
+                        $this->_temp['token']['ptr'][$this->_temp['token']['paren']][] = $line;
+                    } else {
+                        $this->_tokenizeData($line);
+                    }
+
+                    if (is_null($literal_len)) {
+                        if (!$literal) {
+                            break;
+                        }
+                        $binary = $literal = false;
+                        $line = rtrim(fgets($this->_stream));
+                    } else {
+                        $literal = true;
+                        $line = '';
+                        while ($literal_len) {
+                            $data_read = fread($this->_stream, min($literal_len, 8192));
+                            $literal_len -= strlen($data_read);
+                            $line .= rtrim($data_read);
+                        }
+                    }
+
+                    if ($this->_debug) {
+                        $debug_line = $binary
+                            ? "[BINARY DATA - $literal_len bytes]"
+                            : $line;
+                        fwrite($this->_debug, 'S: ' . $debug_line . "\n");
+                    }
+                } while (true);
+
+                $ob['token'] = $this->_temp['token']['out'];
+            }
+            break;
+
+        /* Tagged response. */
+        default:
+            $ob['type'] = 'tagged';
+            $ob['line'] = implode(' ', array_slice($read, 2));
+            $ob['tag'] = $read[0];
+            $ob['response'] = $read[1];
+            break;
+        }
+
+        return $ob;
+    }
+
+    /**
+     * Tokenize IMAP data. Handles quoted strings and parantheses.
+     *
+     * @param string $line  The raw IMAP data.
+     */
+    protected function _tokenizeData($line)
+    {
+        if (is_null($this->_temp['token'])) {
+            $this->_temp['token'] = array(
+                'in_quote' => false,
+                'paren' => 0,
+                'out' => array(),
+                'ptr' => array()
+            );
+            $this->_temp['token']['ptr'][0] = &$this->_temp['token']['out'];
+        }
+
+        $c = &$this->_temp['token'];
+        $tmp = '';
+
+        for ($i = 0, $len = strlen($line); $i < $len; ++$i) {
+            $char = $line[$i];
+            switch ($char) {
+            case '"':
+                if ($c['in_quote']) {
+                    if ($i && ($line[$i - 1] != '//')) {
+                        $c['in_quote'] = false;
+                        $c['ptr'][$c['paren']][] = stripcslashes($tmp);
+                        $tmp = '';
+                    } else {
+                        $tmp .= $char;
+                    }
+                } else {
+                    $c['in_quote'] = true;
+                }
+                break;
+
+            default:
+                if ($c['in_quote']) {
+                    $tmp .= $char;
+                    break;
+                }
+
+                switch ($char) {
+                case '(':
+                    $c['ptr'][$c['paren']][] = array();
+                    $c['ptr'][$c['paren'] + 1] = &$c['ptr'][$c['paren']][count($c['ptr'][$c['paren']]) - 1];
+                    ++$c['paren'];
+                    break;
+
+                case ')':
+                    if (strlen($tmp)) {
+                        $c['ptr'][$c['paren']][] = $tmp;
+                        $tmp = '';
+                    }
+                    --$c['paren'];
+                    break;
+
+                case ' ':
+                    if (strlen($tmp)) {
+                        $c['ptr'][$c['paren']][] = $tmp;
+                        $tmp = '';
+                    }
+                    break;
+
+                default:
+                    $tmp .= $char;
+                    break;
+                }
+                break;
+            }
+        }
+
+        if (strlen($tmp)) {
+            $c['ptr'][$c['paren']][] = $tmp;
+        }
+    }
+
+    /**
+     * Parse all untagged and tagged responses for a given command.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string $tag  The IMAP tag of the current command.
+     */
+    protected function _parseResponse($tag)
+    {
+        while ($ob = $this->_getLine()) {
+            if (($ob['type'] == 'tagged') && ($ob['tag'] == $tag)) {
+                // Here we know there isn't an untagged response, so directly
+                // call _parseStatusResponse().
+                $this->_parseStatusResponse($ob);
+
+                // Now that any status response has been processed, we can
+                // throw errors if appropriate.
+                switch ($ob['response']) {
+                case 'BAD':
+                case 'NO':
+                    if (empty($this->_temp['parsestatuserr'])) {
+                        $errcode = 0;
+                        $errstr = empty($ob['line']) ? '[No error message returned by server.]' : $ob['line'];
+                    } else {
+                        list($errcode, $errstr) = $this->_temp['parsestatuserr'];
+                    }
+                    $this->_temp['parseresperr'] = $ob;
+
+                    if ($ob['response'] == 'BAD') {
+                        throw new Horde_Imap_Client_Exception('Bad IMAP request: ' . $errstr, $errcode);
+                    } else {
+                        throw new Horde_Imap_Client_Exception('IMAP error: ' . $errstr, $errcode);
+                    }
+                }
+
+                /* Update the cache, if needed. */
+                $tmp = $this->_temp['fetchresp'];
+                if (!empty($tmp['uid'])) {
+                    $this->_updateCache($tmp['uid']);
+                } elseif (!empty($tmp['seq'])) {
+                    $this->_updateCache($tmp['seq'], array('seq' => true));
+                }
+
+                break;
+            }
+            $this->_parseServerResponse($ob);
+        }
+    }
+
+    /**
+     * Handle unilateral server responses - untagged data not returned from an
+     * explicit server call (see RFC 3501 [2.2.2]).
+     *
+     * @param array  An array returned from self::_getLine().
+     */
+    protected function _parseServerResponse($ob)
+    {
+        if (!empty($ob['response'])) {
+            $this->_parseStatusResponse($ob);
+        } else {
+            // First, catch all untagged responses where the name appears
+            // first on the line.
+            switch (strtoupper($ob['token'][0])) {
+            case 'CAPABILITY':
+                $this->_parseCapability(array_slice($ob['token'], 1));
+                break;
+
+            case 'LIST':
+            case 'LSUB':
+                $this->_parseList($ob['token'], 1);
+                break;
+
+            case 'STATUS':
+                // Parse a STATUS response (RFC 3501 [7.2.4]).
+                $this->_parseStatus($ob['token'][2]);
+                break;
+
+            case 'SEARCH':
+            case 'SORT':
+                // Parse a SEARCH/SORT response (RFC 3501 [7.2.5] &
+                // RFC 5256 [4]).
+                $this->_parseSearch(array_slice($ob['token'], 1));
+                break;
+
+            case 'ESEARCH':
+                // Parse an ESEARCH response (RFC 4466 [2.6.2]).
+                $this->_parseEsearch(array_slice($ob['token'], 1));
+                break;
+
+            case 'FLAGS':
+                $this->_temp['mailbox']['flags'] = array_map('strtolower', $ob['token'][1]);
+                break;
+
+            case 'QUOTA':
+                $this->_parseQuota(array_slice($ob['token'], 1));
+                break;
+
+            case 'QUOTAROOT':
+                // Ignore this line - we can get this information from
+                // the untagged QUOTA responses.
+                break;
+
+            case 'NAMESPACE':
+                $this->_parseNamespace(array_slice($ob['token'], 1));
+                break;
+
+            case 'THREAD':
+                foreach (array_slice($ob['token'], 1) as $val) {
+                    $this->_parseThread($val);
+                }
+                break;
+
+            case 'ACL':
+                $this->_parseACL(array_slice($ob['token'], 1));
+                break;
+
+            case 'LISTRIGHTS':
+                $this->_parseListRights(array_slice($ob['token'], 1));
+                break;
+
+            case 'MYRIGHTS':
+                $this->_parseMyRights(array_slice($ob['token'], 1));
+                break;
+
+            case 'ID':
+                // ID extension (RFC 2971)
+                $this->_parseID(array_slice($ob['token'], 1));
+                break;
+
+            case 'ENABLED':
+                // ENABLE extension (RFC 5161)
+                $this->_parseEnabled(array_slice($ob['token'], 1));
+                break;
+
+            case 'LANGUAGE':
+                // LANGUAGE extension (RFC 5255 [3.2])
+                $this->_parseLanguage(array_slice($ob['token'], 1));
+                break;
+
+            case 'COMPARATOR':
+                // I18NLEVEL=2 extension (RFC 5255 [4.7])
+                $this->_parseComparator(array_slice($ob['token'], 1));
+                break;
+
+            case 'VANISHED':
+                // QRESYNC extension (RFC 5162 [3.6])
+                $this->_parseVanished(array_slice($ob['token'], 1));
+                break;
+
+            default:
+                // Next, look for responses where the keywords occur second.
+                $type = strtoupper($ob['token'][1]);
+                switch ($type) {
+                case 'EXISTS':
+                case 'RECENT':
+                    // RECENT response - RFC 3501 [7.3.1]
+                    // EXISTS response - RFC 3501 [7.3.2]
+                    $this->_temp['mailbox'][$type == 'RECENT' ? 'recent' : 'messages'] = $ob['token'][0];
+                    break;
+
+                case 'EXPUNGE':
+                    // EXPUNGE response - RFC 3501 [7.4.1]
+                    $this->_parseExpunge($ob['token'][0]);
+                    break;
+
+                case 'FETCH':
+                    // FETCH response - RFC 3501 [7.4.2]
+                    $this->_parseFetch($ob['token'][0], reset(array_slice($ob['token'], 2)));
+                    break;
+                }
+                break;
+            }
+        }
+    }
+
+    /**
+     * Handle status responses (see RFC 3501 [7.1]).
+     *
+     * @param array  An array returned from self::_getLine().
+     */
+    protected function _parseStatusResponse($ob)
+    {
+        if ($ob['line'][0] != '[') {
+            return;
+        }
+
+        $pos = strpos($ob['line'], ' ', 2);
+        $end_pos = strpos($ob['line'], ']', 2);
+        if ($pos > $end_pos) {
+            $code = strtoupper(substr($ob['line'], 1, $end_pos - 1));
+            $data = null;
+        } else {
+            $code = strtoupper(substr($ob['line'], 1, $pos - 1));
+            $data = substr($ob['line'], $pos + 1, $end_pos - $pos - 1);
+        }
+
+        $this->_temp['parsestatuserr'] = null;
+
+        switch ($code) {
+        case 'ALERT':
+            if (!isset($this->_temp['alerts'])) {
+                $this->_temp['alerts'] = array();
+            }
+            $this->_temp['alerts'][] = $data;
+            break;
+
+        case 'BADCHARSET':
+            /* @todo Store the list of search charsets supported by the server
+             * (this is a MAY response, not a MUST response) */
+            $this->_temp['parsestatuserr'] = array(
+                Horde_Imap_Client_Exception::BADCHARSET,
+                substr($ob['line'], $end_pos + 2)
+            );
+            break;
+
+        case 'CAPABILITY':
+            $this->_temp['token'] = null;
+            $this->_tokenizeData($data);
+            $this->_parseCapability($this->_temp['token']['out']);
+            break;
+
+        case 'PARSE':
+            $this->_temp['parsestatuserr'] = array(
+                Horde_Imap_Client_Exception::PARSEERROR,
+                substr($ob['line'], $end_pos + 2)
+            );
+            break;
+
+        case 'READ-ONLY':
+        case 'READ-WRITE':
+            // Ignore - openMailbox() takes care of this for us
+            break;
+
+        case 'TRYCREATE':
+            // RFC 3501 [7.1]
+            $this->_temp['trycreate'] = true;
+            break;
+
+        case 'PERMANENTFLAGS':
+            $this->_temp['token'] = null;
+            $this->_tokenizeData($data);
+            $this->_temp['mailbox']['permflags'] = array_map('strtolower', reset($this->_temp['token']['out']));
+            break;
+
+        case 'UIDNEXT':
+        case 'UIDVALIDITY':
+            $this->_temp['mailbox'][strtolower($code)] = $data;
+            break;
+
+        case 'UNSEEN':
+            /* This is different from the STATUS UNSEEN response - this item,
+             * if defined, returns the first UNSEEN message in the mailbox. */
+            $this->_temp['mailbox']['firstunseen'] = $data;
+            break;
+
+        case 'REFERRAL':
+            // Defined by RFC 2221
+            $this->_temp['referral'] = $this->parseImapURL($data);
+            break;
+
+        case 'UNKNOWN-CTE':
+            // Defined by RFC 3516
+            $this->_temp['parsestatuserr'] = array(
+                Horde_Imap_Client_Exception::UNKNOWNCTE,
+                substr($ob['line'], $end_pos + 2)
+            );
+            break;
+
+        case 'APPENDUID':
+        case 'COPYUID':
+            // Defined by RFC 4315
+            // APPENDUID: [0] = UIDVALIDITY, [1] = UID(s)
+            // COPYUID: [0] = UIDVALIDITY, [1] = UIDFROM, [2] = UIDTO
+            $parts = explode(' ', $data);
+
+            if (($this->_selected == $this->_temp['uidplusmbox']) &&
+                ($this->_temp['mailbox']['uidvalidity'] != $parts[0])) {
+                $this->_temp['mailbox'] = array('uidvalidity' => $parts[0]);
+                $this->_temp['searchnotsaved'] = true;
+            }
+
+            /* Check for cache expiration (see RFC 4549 [4.1]). */
+            $this->_updateCache(array(), array('mailbox' => $this->_temp['uidplusmbox'], 'uidvalid' => $parts[0]));
+
+            if ($code == 'APPENDUID') {
+                $this->_temp['appenduid'] = array_merge($this->_temp['appenduid'], $this->fromSequenceString($parts[1]));
+            } else {
+                $this->_temp['copyuid'] = array_combine($this->fromSequenceString($parts[1]), $this->fromSequenceString($parts[2]));
+            }
+            break;
+
+        case 'UIDNOTSTICKY':
+            // Defined by RFC 4315 [3]
+            $this->_temp['mailbox']['uidnotsticky'] = true;
+            break;
+
+        case 'HIGHESTMODSEQ':
+        case 'NOMODSEQ':
+            // Defined by RFC 4551 [3.1.1 & 3.1.2]
+            $this->_temp['mailbox']['highestmodseq'] = ($code == 'HIGHESTMODSEQ') ? $data : null;
+            break;
+
+        case 'MODIFIED':
+            // Defined by RFC 4551 [3.2]
+            $this->_temp['modified'] = $this->fromSequenceString($data);
+            break;
+
+        case 'CLOSED':
+            // Defined by RFC 5162 [3.7]
+            if (isset($this->_temp['qresyncmbox'])) {
+                $this->_temp['mailbox'] = array('name' => $this->_temp['qresyncmbox']);
+                $this->_selected = $this->_temp['qresyncmbox'];
+            }
+            break;
+
+        case 'NOTSAVED':
+            // Defined by RFC 5182 [2.5]
+            $this->_temp['searchnotsaved'] = true;
+            break;
+
+        case 'BADCOMPARATOR':
+            // Defined by RFC 5255 [4.9]
+            $this->_temp['parsestatuserr'] = array(
+                Horde_Imap_Client_Exception::BADCOMPARATOR,
+                substr($ob['line'], $end_pos + 2)
+            );
+            break;
+
+        case 'XPROXYREUSE':
+            // The proxy connection was reused, so no need to do login tasks.
+            $this->_temp['proxyreuse'] = true;
+            break;
+
+        default:
+            // Unknown response codes SHOULD be ignored - RFC 3501 [7.1]
+            break;
+        }
+    }
+}
diff --git a/framework/Imap_Client/lib/Horde/Imap/Client/Sort.php b/framework/Imap_Client/lib/Horde/Imap/Client/Sort.php
new file mode 100644 (file)
index 0000000..d431312
--- /dev/null
@@ -0,0 +1,116 @@
+<?php
+/**
+ * Horde_Imap_Client_Sort:: provides a function to sort a list of IMAP
+ * mailboxes.
+ *
+ * $Horde: framework/Imap_Client/lib/Horde/Imap/Client/Sort.php,v 1.5 2008/10/17 05:56:16 slusarz Exp $
+ *
+ * Copyright 2004-2008 The Horde Project (http://www.horde.org/)
+ *
+ * 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@horde.org>
+ * @category Horde
+ * @package  Horde_Imap_Client
+ */
+class Horde_Imap_Client_Sort
+{
+    /**
+     * The delimiter character to use.
+     *
+     * @var string
+     */
+    private static $_delimiter = '.';
+
+    /**
+     * Should we sort with 'INBOX' at the front of the list?
+     *
+     * @var boolean
+     */
+    private static $_sortinbox = true;
+
+    /**
+     * Sort a list of mailboxes.
+     * $mbox will be sorted after running this function.
+     *
+     * @param array &$mbox    The list of mailboxes to sort.
+     * @param array $options  Additional options:
+     * <pre>
+     * 'delimiter' - (string) The delimiter to use.
+     *               DEFAULT: '.'
+     * 'inbox' - (boolean) Always put 'INBOX' at the head of the list?
+     *           DEFAULT: Yes
+     * 'index' - (boolean) If sorting by value ('keysort' is false), maintain
+     *           key index association?
+     *           DEFAULT: No
+     * 'keysort' - (boolean) Sort by $mbox's keys?
+     *             DEFAULT: Sort by $mbox values.
+     * </pre>
+     */
+    public static final function sortMailboxes(&$mbox, $options)
+    {
+        if (isset($options['delimiter'])) {
+            self::$_delimiter = $options['delimiter'];
+        }
+
+        if (empty($options['inbox'])) {
+            self::$_sortinbox = false;
+        }
+
+        $cmp = array('Horde_Imap_Client_Sort', 'mboxCompare');
+        if (!empty($options['keysort'])) {
+            uksort($mbox, $cmp);
+        } elseif (!empty($options['index'])) {
+            uasort($mbox, $cmp);
+        } else {
+            usort($mbox, $cmp);
+        }
+    }
+
+    /**
+     * Hierarchical folder sorting function (used with usort()).
+     *
+     * @param string $a  Comparison item 1.
+     * @param string $b  Comparison item 2.
+     *
+     * @return integer  See usort().
+     */
+    public static final function mboxCompare($a, $b)
+    {
+        /* Always return INBOX as "smaller". */
+        if (self::$_sortinbox) {
+            if (strcasecmp($a, 'INBOX') == 0) {
+                return -1;
+            } elseif (strcasecmp($b, 'INBOX') == 0) {
+                return 1;
+            }
+        }
+
+        $a_parts = explode(self::$_delimiter, $a);
+        $b_parts = explode(self::$_delimiter, $b);
+
+        $a_count = count($a_parts);
+        $b_count = count($b_parts);
+
+        for ($i = 0, $iMax = min($a_count, $b_count); $i < $iMax; ++$i) {
+            if ($a_parts[$i] != $b_parts[$i]) {
+                /* If only one of the folders is under INBOX, return it as
+                 * "smaller". */
+                if (self::$_sortinbox && ($i == 0)) {
+                    $a_base = (strcasecmp($a_parts[0], 'INBOX') == 0);
+                    $b_base = (strcasecmp($b_parts[0], 'INBOX') == 0);
+                    if ($a_base && !$b_base) {
+                        return -1;
+                    } elseif (!$a_base && $b_base) {
+                        return 1;
+                    }
+                }
+                $cmp = strnatcasecmp($a_parts[$i], $b_parts[$i]);
+                return ($cmp == 0) ? strcmp($a_parts[$i], $b_parts[$i]) : $cmp;
+            }
+        }
+
+        return ($a_count - $b_count);
+    }
+}
diff --git a/framework/Imap_Client/lib/Horde/Imap/Client/Utf7imap.php b/framework/Imap_Client/lib/Horde/Imap/Client/Utf7imap.php
new file mode 100644 (file)
index 0000000..46ce7e8
--- /dev/null
@@ -0,0 +1,287 @@
+<?php
+/**
+ * Horde_Imap_Client_Utf7imap:: provides code to convert between UTF-8 and
+ * UTF7-IMAP (RFC 3501 [5.1.3]).
+ *
+ * Originally based on code:
+ *  Copyright (C) 2000 Edmund Grimley Evans <edmundo@rano.org>
+ *  Released under the GPL (version 2)
+ *
+ *  Translated from C to PHP by Thomas Bruederli <roundcube@gmail.com>
+ *  Code extracted from the RoundCube Webmail (http://roundcube.net) project,
+ *    SVN revision 1757
+ *  The RoundCube project is released under the GPL (version 2)
+ *
+ * Copyright 2008 The Horde Project (http://www.horde.org/)
+ *
+ * $Horde: framework/Imap_Client/lib/Horde/Imap/Client/Utf7imap.php,v 1.4 2008/10/09 04:43:26 slusarz Exp $
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author   Michael Slusarz <slusarz@curecanti.org>
+ * @category Horde
+ * @package  Horde_Imap_Client
+ */
+class Horde_Imap_Client_Utf7imap
+{
+    /**
+     * Lookup table for conversion.
+     *
+     * @var array
+     */
+    private static $_index64 = array(
+        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, 63, -1, -1, -1,
+        52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1,
+        -1,  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14,
+        15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
+        -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
+        41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1
+    );
+
+    /**
+     * Lookup table for conversion.
+     *
+     * @var array
+     */
+    private static $_base64 = array(
+        'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
+        'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b',
+        'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p',
+        'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3',
+        '4', '5', '6', '7', '8', '9', '+', ','
+    );
+
+    /**
+     * Is mbstring extension available?
+     *
+     * @var array
+     */
+    private static $_mbstring = null;
+
+    /**
+     * Convert a string from UTF7-IMAP to UTF-8.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string  The UTF7-IMAP string.
+     *
+     * @return string  The converted UTF-8 string.
+     */
+    public static function Utf7ImapToUtf8($str)
+    {
+        /* Try mbstring, if available, which should be faster. Don't use the
+         * IMAP utf7_* functions because they are known to be buggy. */
+        if (is_null(self::$_mbstring)) {
+            self::$_mbstring = extension_loaded('mbstring');
+        }
+        if (self::$_mbstring) {
+            $old_error = error_reporting(0);
+            $output = mb_convert_encoding($str, 'UTF-8', 'UTF7-IMAP');
+            error_reporting($old_error);
+            return $output;
+        }
+
+        $str = strval($str);
+        $p = '';
+        $ptr = &self::$_index64;
+
+        for ($i = 0, $u7len = strlen($str); $u7len > 0; ++$i, --$u7len) {
+            $u7 = $str[$i];
+            if ($u7 == '&') {
+                $u7 = $str[++$i];
+                if (--$u7len && ($u7 == '-')) {
+                    $p .= '&';
+                    continue;
+                }
+
+                $ch = 0;
+                $k = 10;
+                for (; $u7len > 0; ++$i, --$u7len) {
+                    $u7 = $str[$i];
+
+                    if ((ord($u7) & 0x80) || ($b = $ptr[ord($u7)]) == -1) {
+                        break;
+                    }
+
+                    if ($k > 0) {
+                        $ch |= $b << $k;
+                        $k -= 6;
+                    } else {
+                        $ch |= $b >> (-$k);
+                        if ($ch < 0x80) {
+                            /* Printable US-ASCII */
+                            if ((0x20 <= $ch) && ($ch < 0x7f)) {
+                                throw new Horde_Imap_Client_Exception('Error converting string.', Horde_Imap_Client_Exception::UTF7IMAP_CONVERSION);
+                            }
+                            $p .= chr($ch);
+                        } else if ($ch < 0x800) {
+                            $p .= chr(0xc0 | ($ch >> 6)) .
+                                  chr(0x80 | ($ch & 0x3f));
+                        } else {
+                            $p .= chr(0xe0 | ($ch >> 12)) .
+                                  chr(0x80 | (($ch >> 6) & 0x3f)) .
+                                  chr(0x80 | ($ch & 0x3f));
+                        }
+
+                        $ch = ($b << (16 + $k)) & 0xffff;
+                        $k += 10;
+                    }
+                }
+
+                /* Non-zero or too many extra bits. */
+                if ($ch || ($k < 6)) {
+                    throw new Horde_Imap_Client_Exception('Error converting string.', Horde_Imap_Client_Exception::UTF7IMAP_CONVERSION);
+                }
+
+                /* Base64 not properly terminated. */
+                if (!$u7len || $u7 != '-') {
+                    throw new Horde_Imap_Client_Exception('Error converting string.', Horde_Imap_Client_Exception::UTF7IMAP_CONVERSION);
+                }
+
+                /* Adjacent Base64 sections. */
+                if (($u7len > 2) &&
+                    ($str[$i + 1] == '&') &&
+                    ($str[$i + 2] != '-')) {
+                    throw new Horde_Imap_Client_Exception('Error converting string.', Horde_Imap_Client_Exception::UTF7IMAP_CONVERSION);
+                }
+            } elseif ((ord($u7) < 0x20) || (ord($u7) >= 0x7f)) {
+                /* Not printable US-ASCII */
+                throw new Horde_Imap_Client_Exception('Error converting string.', Horde_Imap_Client_Exception::UTF7IMAP_CONVERSION);
+            } else {
+                $p .= $u7;
+            }
+        }
+
+        return $p;
+    }
+
+    /**
+     * Convert a string from UTF-8 to UTF7-IMAP.
+     * Throws a Horde_Imap_Client_Exception on error.
+     *
+     * @param string  The UTF-8 string.
+     *
+     * @return string  The converted UTF7-IMAP string.
+     */
+    public static function Utf8ToUtf7Imap($str)
+    {
+        /* No need to do conversion if all chars are in US-ASCII range. */
+        if (!preg_match('/[\x80-\xff]/', $str)) {
+            return $str;
+        }
+
+        /* Try mbstring, if available, which should be faster. Don't use the
+         * IMAP utf7_* functions because they are known to be buggy. */
+        if (is_null(self::$_mbstring)) {
+            self::$_mbstring = extension_loaded('mbstring');
+        }
+        if (self::$_mbstring) {
+            $old_error = error_reporting(0);
+            $output = mb_convert_encoding($str, 'UTF7-IMAP', 'UTF-8');
+            error_reporting($old_error);
+            return $output;
+        }
+
+        $u8len = strlen($str);
+        $i = 0;
+        $base64 = false;
+        $p = '';
+        $ptr = &self::$_base64;
+
+        while ($u8len) {
+            $u8 = $str[$i];
+            $c = ord($u8);
+
+            if ($c < 0x80) {
+                $ch = $c;
+                $n = 0;
+            } elseif ($c < 0xc2) {
+                throw new Horde_Imap_Client_Exception('Error converting string.', Horde_Imap_Client_Exception::UTF7IMAP_CONVERSION);
+            } elseif ($c < 0xe0) {
+                $ch = $c & 0x1f;
+                $n = 1;
+            } elseif ($c < 0xf0) {
+                $ch = $c & 0x0f;
+                $n = 2;
+            } elseif ($c < 0xf8) {
+                $ch = $c & 0x07;
+                $n = 3;
+            } elseif ($c < 0xfc) {
+                $ch = $c & 0x03;
+                $n = 4;
+            } elseif ($c < 0xfe) {
+                $ch = $c & 0x01;
+                $n = 5;
+            } else {
+                throw new Horde_Imap_Client_Exception('Error converting string.', Horde_Imap_Client_Exception::UTF7IMAP_CONVERSION);
+            }
+
+            if ($n > --$u8len) {
+                throw new Horde_Imap_Client_Exception('Error converting string.', Horde_Imap_Client_Exception::UTF7IMAP_CONVERSION);
+            }
+
+            ++$i;
+
+            for ($j = 0; $j < $n; ++$j) {
+                $o = ord($str[$i + $j]);
+                if (($o & 0xc0) != 0x80) {
+                    throw new Horde_Imap_Client_Exception('Error converting string.', Horde_Imap_Client_Exception::UTF7IMAP_CONVERSION);
+                }
+                $ch = ($ch << 6) | ($o & 0x3f);
+            }
+
+            if (($n > 1) && !($ch >> ($n * 5 + 1))) {
+                throw new Horde_Imap_Client_Exception('Error converting string.', Horde_Imap_Client_Exception::UTF7IMAP_CONVERSION);
+            }
+
+            $i += $n;
+            $u8len -= $n;
+
+            if (($ch < 0x20) || ($ch >= 0x7f)) {
+                if (!$base64) {
+                    $p .= '&';
+                    $base64 = true;
+                    $b = 0;
+                    $k = 10;
+                }
+
+                if ($ch & ~0xffff) {
+                    $ch = 0xfffe;
+                }
+
+                $p .= $ptr[($b | $ch >> $k)];
+                $k -= 6;
+                for (; $k >= 0; $k -= 6) {
+                    $p .= $ptr[(($ch >> $k) & 0x3f)];
+                }
+
+                $b = ($ch << (-$k)) & 0x3f;
+                $k += 16;
+            } else {
+                if ($base64) {
+                    if ($k > 10) {
+                        $p .= $ptr[$b];
+                    }
+                    $p .= '-';
+                    $base64 = false;
+                }
+
+                $p .= chr($ch);
+                if (chr($ch) == '&') {
+                    $p .= '-';
+                }
+            }
+        }
+
+        if ($base64) {
+            if ($k > 10) {
+                $p .= $ptr[$b];
+            }
+            $p .= '-';
+        }
+
+        return $p;
+    }
+}
diff --git a/framework/Imap_Client/package.xml b/framework/Imap_Client/package.xml
new file mode 100644 (file)
index 0000000..a88d2e8
--- /dev/null
@@ -0,0 +1,109 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package packagerversion="1.4.9" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
+http://pear.php.net/dtd/tasks-1.0.xsd
+http://pear.php.net/dtd/package-2.0
+http://pear.php.net/dtd/package-2.0.xsd">
+ <name>Horde_Imap_Client</name>
+ <channel>pear.horde.org</channel>
+ <summary>Horde IMAP abstraction interface</summary>
+ <description>This package provides an abstracted API interface to various
+ IMAP4rev1 (RFC 3501) backend drivers.
+ </description>
+ <lead>
+  <name>Michael Slusarz</name>
+  <user>slusarz</user>
+  <email>slusarz@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <lead>
+  <name>Chuck Hagenbuch</name>
+  <user>chuck</user>
+  <email>chuck@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <date>2008-02-25</date>
+ <version>
+  <release>0.0.1</release>
+  <api>0.0.1</api>
+ </version>
+ <stability>
+  <release>alpha</release>
+  <api>alpha</api>
+ </stability>
+ <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+ <notes>
+   * Initial release
+ </notes>
+ <contents>
+  <dir name="/">
+   <dir name="lib">
+    <dir name="Horde">
+     <dir name="Imap">
+      <dir name="Client">
+       <file name="Base.php" role="php" />
+       <file name="Cache.php" role="php" />
+       <file name="Cclient.php" role="php" />
+       <file name="Cclient-pop3.php" role="php" />
+       <file name="Exception.php" role="php" />
+       <file name="Socket.php" role="php" />
+       <file name="Sort.php" role="php" />
+       <file name="Utf7imap.php" role="php" />
+      </dir> <!-- /lib/Horde/Imap/Client -->
+      <file name="Client.php" role="php" />
+     </dir> <!-- /lib/Horde/Imap -->
+    </dir> <!-- /lib/Horde -->
+   </dir> <!-- /lib -->
+  </dir> <!-- / -->
+ </contents>
+ <dependencies>
+  <required>
+   <php>
+    <min>5.2.0</min>
+   </php>
+   <pearinstaller>
+    <min>1.5.0</min>
+   </pearinstaller>
+  </required>
+  <optional>
+   <package>
+    <name>Auth_SASL</name>
+    <channel>pear.php.net</channel>
+   </package>
+   <package>
+   <name>Horde_Cache</name>
+    <channel>pear.horde.org</channel>
+   </package>
+   <package>
+   <name>Horde_Serialize</name>
+    <channel>pear.horde.org</channel>
+   </package>
+   <package>
+    <name>MIME</name>
+    <channel>pear.horde.org</channel>
+   </package>
+   <package>
+    <name>Secret</name>
+    <channel>pear.horde.org</channel>
+   </package>
+   <extension>
+    <name>imap</name>
+   </extension>
+   <extension>
+    <name>mbstring</name>
+   </extension>
+  </optional>
+ </dependencies>
+ <phprelease>
+  <filelist>
+   <install name="lib/Horde/Imap/Client/Base.php" as="Horde/Imap/Client/Base.php" />
+   <install name="lib/Horde/Imap/Client/Cache.php" as="Horde/Imap/Client/Cache.php" />
+   <install name="lib/Horde/Imap/Client/Cclient.php" as="Horde/Imap/Client/Cclient.php" />
+   <install name="lib/Horde/Imap/Client/Cclient-pop3.php" as="Horde/Imap/Client/Cclient-pop3.php" />
+   <install name="lib/Horde/Imap/Client/Exception.php" as="Horde/Imap/Client/Exception.php" />
+   <install name="lib/Horde/Imap/Client/Socket.php" as="Horde/Imap/Client/Socket.php" />
+   <install name="lib/Horde/Imap/Client/Sort.php" as="Horde/Imap/Client/Sort.php" />
+   <install name="lib/Horde/Imap/Client/Utf7imap.php" as="Horde/Imap/Client/Utf7imap.php" />
+   <install name="lib/Horde/Imap/Client.php" as="Horde/Imap/Client.php" />
+  </filelist>
+ </phprelease>
+</package>
diff --git a/framework/Imap_Client/test/Horde/Imap/test_client.php b/framework/Imap_Client/test/Horde/Imap/test_client.php
new file mode 100644 (file)
index 0000000..1f0df9a
--- /dev/null
@@ -0,0 +1,910 @@
+<?php
+/**
+ * Test script for the Horde_Imap_Client:: library.
+ *
+ * Usage:
+ *   test_client.php [[username] [[password] [[IMAP URL]]]]
+ *
+ * Username/password/hostspec on the command line will override the $params
+ * values.
+ *
+ * TODO:
+ *   + Test for 'charset' searching
+ *   + setQuota(), getQuota(), getQuotaRoot()
+ *   + setACL(), listACLRights(), getMyACLRights()
+ *   + setLanguage()
+ *   + setComparator()
+ *   + RFC 4551 (CONDSTORE) related functions
+ *
+ * $Horde: framework/Imap_Client/test/Horde/Imap/test_client.php,v 1.47 2008/10/23 04:53:14 slusarz Exp $
+ *
+ * @author     Michael Slusarz <slusarz@horde.org>
+ * @category   Horde
+ * @package    Horde_Imap_Client
+ */
+
+/** Configuration **/
+$driver = 'Socket'; // 'Socket', 'Cclient', or 'Cclient-pop3'
+$params = array(
+    'username' => '',
+    'password' => '',
+    'hostspec' => '',
+    'port' => '',
+    'secure' => '', // empty, 'ssl', or 'tls'
+    'debug' => 'php://output'
+);
+
+$cache_params = array(
+    'driver' => 'file', // REQUIRED - Horde_Cache driver.
+    'driver_params' => array( // REQUIRED
+        'dir' => '/tmp',
+        'prefix' => 'iclient'
+    ),
+    'compress' => null, // false, 'gzip', or 'lzf'
+    'lifetime' => null, // (integer) Lifetime, in seconds
+    'slicesize' => null // (integer) Slicesize
+);
+
+// Test mailbox names (without namespace information)
+$test_mbox = 'TestMailboxTest';
+$test_mbox_utf8 = 'TestMailboxTest1รจ';
+/** End Configuration **/
+
+
+$currdir = dirname(__FILE__);
+$dir = dirname(dirname(dirname($currdir))) . '/lib/Horde/Imap/';
+require_once $dir . 'Client.php';
+require_once $dir . '/Client/Sort.php';
+
+/* Check for Horde_Cache::. */
+if (@require_once 'Horde/Cache.php') {
+    $horde_cache = true;
+    print "Using Horde_Imap_Client_Cache (driver: " . $cache_params['driver'] . ").\n\n";
+    $params['cache'] = $cache_params;
+} else {
+    $horde_cache = false;
+}
+
+if (!empty($argv[1])) {
+    $params['username'] = $argv[1];
+}
+if (empty($params['username'])) {
+    exit("Need username. Exiting.\n");
+}
+
+if (empty($params['password'])) {
+    $params['password'] = $argv[2];
+}
+if (empty($argv[2])) {
+    exit("Need password. Exiting.\n");
+}
+
+if (!empty($argv[3])) {
+    $params = array_merge($params, Horde_Imap_Client::parseImapURL($argv[3]));
+}
+
+function error_handler($exception) {
+    print "\n=====================================\n" .
+          'ERROR EXCEPTION: ' . $exception->getMessage() .
+          "\n=====================================\n";
+}
+set_exception_handler('error_handler');
+
+function exception_handler($exception) {
+    print "\n=====================================\n" .
+          'UNCAUGHT EXCEPTION: ' . $exception->getMessage() .
+          "\n=====================================\n";
+}
+set_exception_handler('exception_handler');
+
+if (@require_once 'Benchmark/Timer.php') {
+    $timer = new Benchmark_Timer();
+    $timer->start();
+}
+
+// Add an ID field to send to server (ID extension)
+$params['id'] = array('name' => 'Horde_Imap_Client test program');
+
+$imap_client = Horde_Imap_Client::getInstance($driver, $params);
+if ($driver == 'Cclient-pop3') {
+    $test_mbox = $test_mbox_utf8 = 'INBOX';
+}
+
+$use_imapproxy = false;
+print "CAPABILITY listing:\n";
+try {
+    print_r($imap_client->capability());
+    $use_imapproxy = $imap_client->queryCapability('XIMAPPROXY');
+} catch (Horde_Imap_Client_Exception $e) {
+    print 'ERROR: ' . $e->getMessage() . "\n";
+}
+
+print "\nLOGIN:\n";
+try {
+    $imap_client->login();
+    print "Login: OK.\n";
+} catch (Horde_Imap_Client_Exception $e) {
+    print 'ERROR: ' . $e->getMessage() . "\n";
+    exit("Login: FAILED. EXITING.\n");
+}
+
+print "\nUsing a secure connection: " . ($imap_client->isSecureConnection() ? 'YES' : 'NO') . "\n";
+
+print "\nID information from server:\n";
+try {
+    print_r($imap_client->getID());
+    print "ID information: OK.\n";
+} catch (Horde_Imap_Client_Exception $e) {
+    print 'ERROR: ' . $e->getMessage() . "\n";
+    print "ID information: FAILED.\n";
+}
+
+print "\nLanguage information from server:\n";
+try {
+    print_r($imap_client->getLanguage(true));
+    $lang = $imap_client->getLanguage();
+    print "Language information (" . ($lang ? $lang : 'NONE') . "): OK.\n";
+} catch (Horde_Imap_Client_Exception $e) {
+    print 'ERROR: ' . $e->getMessage() . "\n";
+    print "Language information: FAILED.\n";
+}
+
+print "\nComparator information from server:\n";
+try {
+    print_r($imap_client->getComparator());
+    print "Comparator information: OK.\n";
+} catch (Horde_Imap_Client_Exception $e) {
+    print 'ERROR: ' . $e->getMessage() . "\n";
+    print "Comparator information: FAILED.\n";
+}
+
+print "\nNAMESPACES:\n";
+try {
+    $namespaces = $imap_client->getNamespaces();
+    print_r($namespaces);
+    print "Namespaces: OK.\n";
+} catch (Horde_Imap_Client_Exception $e) {
+    print 'ERROR: ' . $e->getMessage() . "\n";
+    print "Namespaces: FAILED.\n";
+    $namespaces = array();
+}
+
+// Tack on namespace information to folder names.
+$base_ns = reset($namespaces);
+if (empty($base_ns['name'])) {
+    $ns_prefix = '';
+} else {
+    $ns_prefix = rtrim($base_ns['name'], $base_ns['delimiter']) . $base_ns['delimiter'];
+}
+$test_mbox = $ns_prefix . $test_mbox;
+$test_mbox_utf8 = $ns_prefix . $test_mbox_utf8;
+
+print "\nOpen INBOX read-only, read-write, and auto.\n";
+try {
+    $imap_client->openMailbox('INBOX', Horde_Imap_Client::OPEN_READONLY);
+    print "Read-only: OK\n";
+} catch (Horde_Imap_Client_Exception $e) {
+    print 'ERROR: ' . $e->getMessage() . "\n";
+    print "Read-only: FAILED\n";
+}
+
+try {
+    $imap_client->openMailbox('INBOX', Horde_Imap_Client::OPEN_READWRITE);
+    print "Read-write: OK\n";
+} catch (Horde_Imap_Client_Exception $e) {
+    print 'ERROR: ' . $e->getMessage() . "\n";
+    print "Read-write: FAILED\n";
+}
+
+try {
+    $imap_client->openMailbox('INBOX', Horde_Imap_Client::OPEN_AUTO);
+    print "Auto: OK\n";
+} catch (Horde_Imap_Client_Exception $e) {
+    print 'ERROR: ' . $e->getMessage() . "\n";
+    print "Auto: FAILED\n";
+}
+
+print "\nCurrent mailbox information:\n";
+print_r($imap_client->currentMailbox());
+
+print "\nCreating mailbox " . $test_mbox . ".\n";
+try {
+    $imap_client->createMailbox($test_mbox);
+    print "Creating: OK\n";
+} catch (Horde_Imap_Client_Exception $e) {
+    print 'ERROR: ' . $e->getMessage() . "\n";
+    print "Creating: FAILED\n";
+}
+
+print "\nSubscribing to mailbox " . $test_mbox . ".\n";
+try {
+    $imap_client->subscribeMailbox($test_mbox, true);
+    print "Subscribing: OK\n";
+} catch (Horde_Imap_Client_Exception $e) {
+    print 'ERROR: ' . $e->getMessage() . "\n";
+    print "Subscribing: FAILED\n";
+}
+
+print "\nUnsubscribing to mailbox " . $test_mbox . ".\n";
+try {
+    $imap_client->subscribeMailbox($test_mbox, false);
+    print "Unsubscribing: OK\n";
+} catch (Horde_Imap_Client_Exception $e) {
+    print 'ERROR: ' . $e->getMessage() . "\n";
+    print "Unsubscribing: FAILED\n";
+}
+
+print "\nRenaming mailbox " . $test_mbox . " to " . $test_mbox_utf8 . ".\n";
+try {
+    $imap_client->renameMailbox($test_mbox, $test_mbox_utf8);
+    print "Renaming: OK\n";
+} catch (Horde_Imap_Client_Exception $e) {
+    print 'ERROR: ' . $e->getMessage() . "\n";
+    print "Renaming: FAILED\n";
+}
+
+print "\nDeleting mailbox " . $test_mbox_utf8 . ".\n";
+try {
+    $imap_client->deleteMailbox($test_mbox_utf8);
+    print "Deleting: OK\n";
+} catch (Horde_Imap_Client_Exception $e) {
+    print 'ERROR: ' . $e->getMessage() . "\n";
+    print "Deleting: FAILED\n";
+}
+
+print "\nDeleting (non-existent) mailbox " . $test_mbox_utf8 . ".\n";
+try {
+    $imap_client->deleteMailbox($test_mbox_utf8);
+    print "Failed deletion: FAILED\n";
+} catch (Horde_Imap_Client_Exception $e) {
+    print "Deleting: OK\n";
+    print 'Error returned from IMAP server: ' . $e->getMessage() . "\n";
+}
+
+print "\nListing all mailboxes in base level (flat format).\n";
+print_r($imap_client->listMailboxes('%', Horde_Imap_Client::MBOX_ALL, array('flat' => true)));
+
+print "\nListing all mailboxes (flat format).\n";
+print_r($imap_client->listMailboxes('*', Horde_Imap_Client::MBOX_ALL, array('flat' => true)));
+
+print "\nListing subscribed mailboxes (flat format, in UTF-8 encoding).\n";
+print_r($imap_client->listMailboxes('*', Horde_Imap_Client::MBOX_SUBSCRIBED, array('flat' => true, 'utf8' => true)));
+
+print "\nListing unsubscribed mailboxes in base level (with attribute and delimiter information).\n";
+print_r($imap_client->listMailboxes('%', Horde_Imap_Client::MBOX_UNSUBSCRIBED, array('attributes' => true, 'delimiter' => true)));
+
+print "\nAll status information for INBOX.\n";
+print_r($imap_client->status('INBOX', Horde_Imap_Client::STATUS_ALL));
+
+print "\nOnly UIDNEXT status information for INBOX.\n";
+print_r($imap_client->status('INBOX', Horde_Imap_Client::STATUS_UIDNEXT));
+
+print "\nOnly FIRSTUNSEEN, FLAGS, PERMFLAGS, HIGHESTMODSEQ, and UIDNOTSTICKY status information for INBOX.\n";
+try {
+    print_r($imap_client->status('INBOX', Horde_Imap_Client::STATUS_FIRSTUNSEEN | Horde_Imap_Client::STATUS_FLAGS | Horde_Imap_Client::STATUS_PERMFLAGS | Horde_Imap_Client::STATUS_HIGHESTMODSEQ | Horde_Imap_Client::STATUS_UIDNOTSTICKY));
+    print "Status: OK.\n";
+} catch (Horde_Imap_Client_Exception $e) {
+    print 'ERROR: ' . $e->getMessage() . "\n";
+    print "Status: FAILED.\n";
+}
+
+print "\nCreating mailbox " . $test_mbox . " for message tests.\n";
+try {
+    $imap_client->createMailbox($test_mbox);
+    print "Created " . $test_mbox . " OK.\n";
+} catch (Horde_Imap_Client_Exception $e) {
+    print 'ERROR: ' . $e->getMessage() . "\n";
+    print "Creation: FAILED.\n";
+}
+
+print "\nCreating mailbox " . $test_mbox_utf8 . " for message tests.\n";
+try {
+    $imap_client->createMailbox($test_mbox_utf8);
+    print "Created " . $test_mbox_utf8 . " OK.\n";
+} catch (Horde_Imap_Client_Exception $e) {
+    print 'ERROR: ' . $e->getMessage() . "\n";
+    print "Creation: FAILED.\n";
+}
+
+print "\nAll status information for " . $test_mbox . ", including 'firstunseen' and 'highestmodseq'.\n";
+try {
+    print_r($imap_client->status($test_mbox, Horde_Imap_Client::STATUS_ALL | Horde_Imap_Client::STATUS_FIRSTUNSEEN | Horde_Imap_Client::STATUS_HIGHESTMODSEQ));
+    print "Status: OK.\n";
+} catch (Horde_Imap_Client_Exception $e) {
+    print 'ERROR: ' . $e->getMessage() . "\n";
+    print "Status: FAILED.\n";
+}
+
+$test_email = <<<EOE
+Return-Path: <test@example.com>
+Delivered-To: foo@example.com
+Received: from test1.example.com (test1.example.com [192.168.10.10])
+    by test2.example.com (Postfix) with ESMTP id E6F7890AF
+    for <foo@example.com>; Sat, 26 Jul 2008 20:09:03 -0600 (MDT)
+Message-ID: <abcd1234efgh5678@test1.example.com>
+Date: Sat, 26 Jul 2008 21:10:00 -0500 (CDT)
+From: Test <test@example.com>
+To: foo@example.com
+Subject: Test e-mail 1
+Mime-Version: 1.0
+Content-Type: text/plain
+
+Test.
+EOE;
+
+$test_email2 = <<<EOE
+Return-Path: <test@example.com>
+Delivered-To: foo@example.com
+Received: from test1.example.com (test1.example.com [192.168.10.10])
+    by test2.example.com (Postfix) with ESMTP id E8796FBA
+    for <foo@example.com>; Sat, 26 Jul 2008 21:19:13 -0600 (MDT)
+Message-ID: <98761234@test1.example.com>
+Date: Sat, 26 Jul 2008 21:19:00 -0500 (CDT)
+From: Test <test@example.com>
+To: foo@example.com
+Subject: Re: Test e-mail 1
+Mime-Version: 1.0
+Content-Type: text/plain
+In-Reply-To: <abcd1234efgh5678@test1.example.com>
+References: <abcd1234efgh5678@test1.example.com>
+    <originalreply123123123@test1.example.com>
+
+Test reply.
+EOE;
+
+$uid1 = $uid2 = $uid3 = $uid4 = null;
+
+print "\nAppending test e-mail 1 (with \\Flagged), 2 via a stream (with \\Seen), 3 via a stream, and 4 (with internaldate):\n";
+try {
+    $handle = fopen($currdir . '/test_email.txt', 'r');
+    $handle2 = fopen($currdir . '/test_email2.txt', 'r');
+    $uid = $imap_client->append($test_mbox, array(
+        array('data' => $test_email, 'flags' => array('\\flagged'), 'messageid' => 'abcd1234efgh5678@test1.example.com'),
+        array('data' => $handle, 'flags' => array('\\seen'), 'messageid' => 'aaabbbcccddd111222333444@test1.example.com'),
+        array('data' => $handle2, 'messageid' => '2008yhnujm@foo.example.com'),
+        array('data' => $test_email2, 'internaldate' => new DateTime('17 August 2003'), 'messageid' => '98761234@test1.example.com')
+    ));
+    if ($uid === true) {
+        throw new Horde_Imap_Client_Exception('Append successful but UIDs not properly returned.');
+    }
+    list($uid1, $uid2, $uid3, $uid4) = $uid;
+    print "Append test-email 1 OK [UID: $uid1]\n";
+    print "Append test-email 2 OK [UID: $uid2]\n";
+    print "Append test-email 3 OK [UID: $uid3]\n";
+    print "Append test-email 4 OK [UID: $uid4]\n";
+} catch (Horde_Imap_Client_Exception $e) {
+    print 'ERROR: ' . $e->getMessage() . "\n";
+    print "Appending: FAILED.\n";
+}
+fclose($handle);
+fclose($handle2);
+
+if (!is_null($uid1)) {
+    print "\nCopying test e-mail 1 to " . $test_mbox_utf8 . ".\n";
+    try {
+        $uid5 = $imap_client->copy($test_mbox, $test_mbox_utf8, array('ids' => array($uid1)));
+        if ($uid5 === true) {
+            print "Copy: OK\n";
+            $uid5 = null;
+        } elseif (is_array($uid5)) {
+            print_r($uid5);
+            reset($uid5);
+            print "Copy: OK [From UID " . key($uid5) . " to UID " . current($uid5) . "]\n";
+        }
+    } catch (Horde_Imap_Client_Exception $e) {
+        print 'ERROR: ' . $e->getMessage() . "\n";
+        print "Copy: FAILED\n";
+        $uid5 = null;
+    }
+} else {
+    $uid5 = null;
+}
+
+if (!is_null($uid2)) {
+    print "\nFlagging test e-mail 2 with the Deleted flag.\n";
+    try {
+        $imap_client->store($test_mbox, array('add' => array('\\deleted'), 'ids' => array($uid2)));
+        print "Flagging: OK\n";
+    } catch (Horde_Imap_Client_Exception $e) {
+        print 'ERROR: ' . $e->getMessage() . "\n";
+        print "Flagging: FAILED\n";
+    }
+}
+
+if (!is_null($uid1)) {
+    print "\nExpunging mailbox by specifying non-deleted UID.\n";
+    try {
+        $imap_client->expunge($test_mbox, array('ids' => array($uid1)));
+        print "Expunging: OK\n";
+    } catch (Horde_Imap_Client_Exception $e) {
+        print 'ERROR: ' . $e->getMessage() . "\n";
+        print "Expunging: FAILED\n";
+    }
+}
+
+print "\nGet status of " . $test_mbox . " (should have 4 messages).\n";
+try {
+    print_r($imap_client->status($test_mbox, Horde_Imap_Client::STATUS_ALL));
+    print "Status: OK\n";
+} catch (Horde_Imap_Client_Exception $e) {
+    print 'ERROR: ' . $e->getMessage() . "\n";
+    print "Status: FAILED\n";
+}
+
+print "\nExpunging mailbox (should remove test e-mail 2).\n";
+try {
+    $imap_client->expunge($test_mbox);
+    print "Expunging: OK\n";
+} catch (Horde_Imap_Client_Exception $e) {
+    print 'ERROR: ' . $e->getMessage() . "\n";
+    print "Expunging: FAILED\n";
+}
+
+print "\nGet status of " . $test_mbox . " (should have 3 messages).\n";
+print_r($imap_client->status($test_mbox, Horde_Imap_Client::STATUS_ALL));
+
+if (!is_null($uid5)) {
+    print "\nMove test e-mail 1 from " . $test_mbox_utf8 . " to " . $test_mbox . ".\n";
+    try {
+        $uid6 = $imap_client->copy($test_mbox_utf8, $test_mbox, array('ids' => array(current($uid5)), 'move' => true));
+        if ($uid6 === true) {
+            print "Move: OK\n";
+        } elseif (is_array($uid6)) {
+            print_r($uid6);
+            reset($uid6);
+            print "Move: OK [From UID " . key($uid6) . " to UID " . current($uid6) . "]\n";
+        }
+    } catch (Horde_Imap_Client_Exception $e) {
+        print 'ERROR: ' . $e->getMessage() . "\n";
+        print "Move: FAILED\n";
+    }
+}
+
+print "\nDeleting mailbox " . $test_mbox_utf8 . ".\n";
+try {
+    $imap_client->deleteMailbox($test_mbox_utf8);
+    print "Deleting: OK\n";
+} catch (Horde_Imap_Client_Exception $e) {
+    print 'ERROR: ' . $e->getMessage() . "\n";
+    print "Deleting: FAILED\n";
+}
+
+print "\nFlagging test e-mail 3 with the Deleted flag.\n";
+if (!is_null($uid3)) {
+    try {
+        $imap_client->store($test_mbox, array('add' => array('\\deleted'), 'ids' => array($uid3)));
+        print "Flagging: OK\n";
+    } catch (Horde_Imap_Client_Exception $e) {
+        print 'ERROR: ' . $e->getMessage() . "\n";
+        print "Flagging: FAILED\n";
+    }
+}
+
+print "\nClosing " . $test_mbox . " without expunging.\n";
+try {
+    $imap_client->close();
+    print "Closing: OK\n";
+} catch (Horde_Imap_Client_Exception $e) {
+    print 'ERROR: ' . $e->getMessage() . "\n";
+    print "Closing: FAILED\n";
+}
+
+print "\nGet status of " . $test_mbox . " (should have 4 messages).\n";
+print_r($imap_client->status($test_mbox, Horde_Imap_Client::STATUS_ALL));
+
+// Create a simple 'ALL' search query
+$all_query = new Horde_Imap_Client_Search_Query();
+
+print "\nSearching " . $test_mbox . " for all messages (returning UIDs).\n";
+try {
+    print_r($imap_client->search($test_mbox, $all_query));
+    print "Search: OK\n";
+} catch (Horde_Imap_Client_Exception $e) {
+    print 'ERROR: ' . $e->getMessage() . "\n";
+    print "Search: FAILED\n";
+}
+
+print "\nSearching " . $test_mbox . " for all messages (returning message sequence numbers).\n";
+try {
+    print_r($imap_client->search($test_mbox, $all_query, array('results' => array(Horde_Imap_Client::SORT_RESULTS_COUNT, Horde_Imap_Client::SORT_RESULTS_MATCH, Horde_Imap_Client::SORT_RESULTS_MAX, Horde_Imap_Client::SORT_RESULTS_MIN), 'sequence' => true)));
+    print "Search: OK\n";
+} catch (Horde_Imap_Client_Exception $e) {
+    print 'ERROR: ' . $e->getMessage() . "\n";
+    print "Search: FAILED\n";
+}
+
+print "\nSearching " . $test_mbox . " (should be optimized by using internal status instead).\n";
+try {
+    $query1 = $query2 = $all_query;
+    print_r($imap_client->search($test_mbox, $all_query, array('results' => array(Horde_Imap_Client::SORT_RESULTS_COUNT))));
+    $query1->flag('\\recent');
+    print_r($imap_client->search($test_mbox, $query1, array('results' => array(Horde_Imap_Client::SORT_RESULTS_COUNT))));
+    $query2->flag('\\seen', false);
+    print_r($imap_client->search($test_mbox, $query2, array('results' => array(Horde_Imap_Client::SORT_RESULTS_COUNT))));
+    print_r($imap_client->search($test_mbox, $query2, array('results' => array(Horde_Imap_Client::SORT_RESULTS_MIN))));
+    print "Search: OK\n";
+} catch (Horde_Imap_Client_Exception $e) {
+    print 'ERROR: ' . $e->getMessage() . "\n";
+    print "Search: FAILED\n";
+}
+
+print "\nSort " . $test_mbox . " by from and reverse date for all messages (returning UIDs).\n";
+try {
+    print_r($imap_client->search($test_mbox, $all_query, array('sort' => array(Horde_Imap_Client::SORT_FROM, Horde_Imap_Client::SORT_REVERSE, Horde_Imap_Client::SORT_DATE))));
+    print "Search: OK\n";
+} catch (Horde_Imap_Client_Exception $e) {
+    print 'ERROR: ' . $e->getMessage() . "\n";
+    print "Search: FAILED\n";
+}
+
+print "\nSort " . $test_mbox . " by thread - references algorithm (UIDs).\n";
+try {
+    print_r($imap_client->thread($test_mbox, array('criteria' => Horde_Imap_Client::THREAD_REFERENCES)));
+    print "Thread search: OK\n";
+} catch (Horde_Imap_Client_Exception $e) {
+    print 'ERROR: ' . $e->getMessage() . "\n";
+    print "Thread search: FAILED\n";
+}
+
+print "\nSort 1st 5 messages in " . $test_mbox . " by thread - references algorithm (UIDs).\n";
+try {
+    $ten_query = new Horde_Imap_Client_Search_Query();
+    $ten_query->sequence(Horde_Imap_Client::fromSequenceString('1:5'), true);
+    print_r($imap_client->thread($test_mbox, array('search' => $ten_query, 'criteria' => Horde_Imap_Client::THREAD_REFERENCES)));
+    print "Thread search: OK\n";
+} catch (Horde_Imap_Client_Exception $e) {
+    print 'ERROR: ' . $e->getMessage() . "\n";
+    print "Thread search: FAILED\n";
+}
+
+print "\nSort " . $test_mbox . " by thread - orderedsubject algorithm (sequence numbers).\n";
+try {
+    print_r($imap_client->thread($test_mbox, array('criteria' => Horde_Imap_Client::THREAD_ORDEREDSUBJECT, 'sequence' => true)));
+    print "Thread search: OK\n";
+} catch (Horde_Imap_Client_Exception $e) {
+    print 'ERROR: ' . $e->getMessage() . "\n";
+    print "Thread search: FAILED\n";
+}
+
+$simple_fetch = array(
+    Horde_Imap_Client::FETCH_STRUCTURE => true,
+    Horde_Imap_Client::FETCH_ENVELOPE => true,
+    Horde_Imap_Client::FETCH_FLAGS => true,
+    Horde_Imap_Client::FETCH_DATE => true,
+    Horde_Imap_Client::FETCH_SIZE => true
+);
+
+print "\nSimple fetch example:\n";
+try {
+    print_r($imap_client->fetch($test_mbox, $simple_fetch));
+    print "Fetch: OK\n";
+} catch (Horde_Imap_Client_Exception $e) {
+    print 'ERROR: ' . $e->getMessage() . "\n";
+    print "Fetch: FAILED\n";
+}
+
+if ($horde_cache) {
+   print "\nRepeat simple fetch example - should retrieve data from cache:\n";
+    try {
+        print_r($imap_client->fetch($test_mbox, $simple_fetch));
+        print "Fetch: OK\n";
+    } catch (Horde_Imap_Client_Exception $e) {
+        print 'ERROR: ' . $e->getMessage() . "\n";
+        print "Fetch: FAILED\n";
+    }
+}
+
+print "\nFetching message information from complex MIME message:\n";
+try {
+    $fetch_res = $imap_client->fetch($test_mbox, array(
+        Horde_Imap_Client::FETCH_FULLMSG => array(
+            'length' => 100,
+            'peek' => true,
+            'start' => 0
+        ),
+
+        Horde_Imap_Client::FETCH_HEADERTEXT => array(
+            // Header of entire message
+            array(
+                'length' => 100,
+                'peek' => true,
+                'start' => 0
+            ),
+            // Header of message/rfc822 part
+            array(
+                'id' => 2,
+                'length' => 100,
+                'peek' => true,
+                'start' => 0
+            )
+        ),
+
+        Horde_Imap_Client::FETCH_BODYTEXT => array(
+            // Body text of entire message
+            array(
+                'length' => 100,
+                'peek' => true,
+                'start' => 0
+            ),
+            // Body text of message/rfc822 part
+            array(
+                'id' => 2,
+                'length' => 100,
+                'peek' => true,
+                'start' => 0
+            )
+        ),
+
+        Horde_Imap_Client::FETCH_MIMEHEADER => array(
+            // MIME Header of multipart/alternative part
+            array(
+                'id' => 1,
+                'length' => 100,
+                'peek' => true,
+                'start' => 0
+            ),
+            // MIME Header of text/plain part embedded in message/rfc822 part
+            array(
+                'id' => '2.1',
+                'length' => 100,
+                'peek' => true,
+                'start' => 0
+            )
+        ),
+
+        Horde_Imap_Client::FETCH_BODYPART => array(
+            // Body text of multipart/alternative part
+            array(
+                'id' => 1,
+                'length' => 100,
+                'peek' => true,
+                'start' => 0
+            ),
+            // Body text of image/png part embedded in message/rfc822 part
+            // Try to do server-side decoding, if available
+            array(
+                'decode' => true,
+                'id' => '2.2',
+                'length' => 100,
+                'peek' => true,
+                'start' => 0
+            )
+        ),
+
+        // If supported, return decoded body part size
+        Horde_Imap_Client::FETCH_BODYPARTSIZE => array(
+            array('id' => '2.2')
+        ),
+
+        Horde_Imap_Client::FETCH_HEADERS => array(
+            // Select message-id header from base message header
+            array(
+                'headers' => array('message-id'),
+                'label' => 'headersearch1',
+                'length' => 100,
+                'peek' => true,
+                'start' => 0
+            ),
+            // Select everything but message-id header from message/rfc822
+            // header
+            array(
+                'id' => 2,
+                'headers' => array('message-id'),
+                'label' => 'headersearch2',
+                'length' => 100,
+                'notsearch' => true,
+                'peek' => true,
+                'start' => 0
+            )
+        ),
+
+        Horde_Imap_Client::FETCH_STRUCTURE => true,
+        Horde_Imap_Client::FETCH_ENVELOPE => true,
+        Horde_Imap_Client::FETCH_FLAGS => true,
+        Horde_Imap_Client::FETCH_DATE => true,
+        Horde_Imap_Client::FETCH_SIZE => true,
+        Horde_Imap_Client::FETCH_UID => true,
+        Horde_Imap_Client::FETCH_MODSEQ => true
+    ), array('ids' => array($uid3)));
+    print_r($fetch_res);
+    print "Fetch: OK\n";
+} catch (Horde_Imap_Client_Exception $e) {
+    print 'ERROR: ' . $e->getMessage() . "\n";
+    print "Fetch: FAILED\n";
+
+    // If POP3, try easier fetch criteria
+    if ($driver == 'Cclient-pop3') {
+        try {
+            print_r($imap_client->fetch('INBOX', array(
+                Horde_Imap_Client::FETCH_FULLMSG => array(
+                    'length' => 100,
+                    'peek' => true,
+                    'start' => 0)
+                ), array('ids' => array(1))));
+            print "Fetch: OK\n";
+        } catch (Horde_Imap_Client_Exception $e) {
+            print 'ERROR: ' . $e->getMessage() . "\n";
+            print "Fetch (POP3): FAILED\n";
+        }
+    }
+}
+
+print "\nFetching parsed header information (requires Horde MIME library):\n";
+try {
+    print_r($imap_client->fetch($test_mbox, array(
+        Horde_Imap_Client::FETCH_HEADERS => array(
+            array(
+                'headers' => array('message-id'),
+                'label' => 'headersearch1',
+                'parse' => true,
+                'peek' => true)
+            )
+        ), array('ids' => array($uid3))));
+    print "Fetch: OK\n";
+} catch (Horde_Imap_Client_Exception $e) {
+    print 'ERROR: ' . $e->getMessage() . "\n";
+    print "Fetch: FAILED\n";
+}
+
+print "\nRe-open " . $test_mbox . " READ-WRITE.\n";
+try {
+    $imap_client->openMailbox($test_mbox, Horde_Imap_Client::OPEN_READWRITE);
+    print "Read-write: OK\n";
+} catch (Horde_Imap_Client_Exception $e) {
+    print 'ERROR: ' . $e->getMessage() . "\n";
+    print "Read-write: FAILED\n";
+}
+
+print "\nClosing " . $test_mbox . " while expunging.\n";
+try {
+    $imap_client->close(array('expunge' => true));
+    print "Closing: OK\n";
+} catch (Horde_Imap_Client_Exception $e) {
+    print 'ERROR: ' . $e->getMessage() . "\n";
+    print "Closing: FAILED\n";
+}
+
+print "\nGet status of " . $test_mbox . " (should have 3 messages).\n";
+print_r($imap_client->status($test_mbox, Horde_Imap_Client::STATUS_ALL));
+
+print "\nDeleting mailbox " . $test_mbox . ".\n";
+try {
+    $imap_client->deleteMailbox($test_mbox);
+    print "Deleting " . $test_mbox . ": OK\n";
+} catch (Horde_Imap_Client_Exception $e) {
+    print 'ERROR: ' . $e->getMessage() . "\n";
+    print "Deleting: FAILED\n";
+}
+
+print "\nTesting a complex search query built using Horde_Imap_Client_Search_Query:\n";
+$query = new Horde_Imap_Client_Search_Query();
+$query->flag('\\Answered');
+$query->flag('\\Deleted', true);
+$query->flag('\\Recent');
+$query->flag('\\Unseen');
+$query->flag('TestKeyword');
+// This second flag request for '\Answered' should overrule the first request
+$query->flag('\\Answered', true);
+// Querying for new should clear both '\Recent' and '\Unseen'
+$query->newMsgs();
+$query->headerText('cc', 'Testing');
+$query->headerText('message-id', 'abcdefg1234567', true);
+$query->headerText('subject', '8bit char 1รจ.');
+$query->charset('UTF-8');
+$query->text('Test1');
+$query->text('Test2', false);
+$query->size('1024', true);
+$query->size('4096', false);
+$query->sequence(array(1, 5, 50, 51, 52, 55, 54, 53, 55, 55, 100, 500, 501));
+$query->dateSearch(6, 15, 2008, Horde_Imap_Client_Search_Query::DATE_BEFORE, true, true);
+$query->dateSearch(6, 20, 2008, Horde_Imap_Client_Search_Query::DATE_ON, false);
+// Add 2 simple OR queries
+$query2 = new Horde_Imap_Client_Search_Query();
+$query2->text('Test3', false, true);
+$query3 = new Horde_Imap_Client_Search_Query();
+$query3->newMsgs(false);
+$query3->intervalSearch(100000, Horde_Imap_Client_Search_Query::INTERVAL_YOUNGER);
+$query3->modseq(1234, '/flags/\deleted', 'all');
+$query->orSearch(array($query2, $query3));
+print_r($query->build());
+
+print "\nTesting mailbox sorting:\n";
+$test_sort = array(
+    'A',
+    'Testing.JJ',
+    'A1',
+    'INBOX',
+    'a',
+    'A1.INBOX',
+    '2.A',
+    'a.A.1.2',
+    $test_mbox,
+    $test_mbox_utf8
+);
+print_r($test_sort);
+Horde_Imap_Client_Sort::sortMailboxes($test_sort, array('delimiter' => '.', 'inbox' => true));
+print_r($test_sort);
+
+print "Testing serialization of object. Will automatically logout.\n";
+$old_error = error_reporting(0);
+if (require_once 'Horde/Secret.php') {
+    Horde_Imap_Client::$encryptKey = uniqid();
+}
+error_reporting($old_error);
+$serialized_data = serialize($imap_client);
+print "\nSerialized object:\n";
+print_r($serialized_data);
+
+// Unset $encryptKey so password is not output in cleartext
+Horde_Imap_Client::$encryptKey = null;
+$unserialized_data = unserialize($serialized_data);
+print "\n\nUnserialized object:\n";
+print_r($unserialized_data);
+
+if ($use_imapproxy) {
+    print "\nTesting reuse of imapproxy connection.\n";
+    try {
+        $unserialized_data->status('INBOX', Horde_Imap_Client::STATUS_MESSAGES);
+        print "OK.\n";
+    } catch (Horde_Imap_Client_Exception $e) {
+        print 'ERROR: ' . $e->getMessage() . "\n";
+    }
+
+    $unserialized_data->logout();
+    print "\nLogging out: OK\n";
+}
+
+$subject_lines = array(
+    'Re: Test',
+    're: Test',
+    'Fwd: Test',
+    'fwd: Test',
+    'Fwd: Re: Test',
+    'Fwd: Re: Test (fwd)',
+    "Re: re:re: fwd:[fwd: \t  Test]  (fwd)  (fwd)(fwd) "
+);
+
+print "\nBase subject parsing:\n";
+foreach ($subject_lines as $val) {
+    print "  ORIGINAL: \"" . $val . "\"\n";
+    print "  BASE: \"" . Horde_Imap_Client::getBaseSubject($val) . "\"\n\n";
+}
+
+$imap_urls = array(
+    'NOT A VALID URL',
+    'imap://test.example.com/',
+    'imap://test.example.com:143/',
+    'imap://testuser@test.example.com/',
+    'imap://testuser@test.example.com:143/',
+    'imap://;AUTH=PLAIN@test.example.com/',
+    'imap://;AUTH=PLAIN@test.example.com:143/',
+    'imap://;AUTH=*@test.example.com:143/',
+    'imap://testuser;AUTH=*@test.example.com:143/',
+    'imap://testuser;AUTH=PLAIN@test.example.com:143/'
+);
+
+print "\nRFC 5092 URL parsing:\n";
+foreach ($imap_urls as $val) {
+    print "URL: " . $val . "\n";
+    print "PARSED:\n";
+    print_r(Horde_Imap_Client::parseImapURL($val));
+    print "\n";
+}
+
+if (isset($fetch_res) &&
+    @require_once 'Horde/MIME/Message.php') {
+    print "\nTesting MIME_Message::parseStructure() on complex MIME message:\n";
+    $parse_res = MIME_Message::parseStructure($fetch_res[$uid3]['structure']);
+    print_r($parse_res);
+
+    print "\nTesting MIME_Message::parseMessage() on complex MIME message:\n";
+    $parse_text_res = MIME_Message::parseMessage(file_get_contents($currdir . '/test_email2.txt'));
+    print_r($parse_text_res);
+}
+
+if (isset($timer)) {
+    $timer->stop();
+    print "\nTime elapsed: " . $timer->timeElapsed() . " seconds";
+}
+
+print "\nMemory used: " . memory_get_usage() . " (Peak: " . memory_get_peak_usage() . ")\n";
diff --git a/framework/Imap_Client/test/Horde/Imap/test_email.txt b/framework/Imap_Client/test/Horde/Imap/test_email.txt
new file mode 100644 (file)
index 0000000..ad5f2a8
--- /dev/null
@@ -0,0 +1,14 @@
+Return-Path: <test@example.com>
+Delivered-To: foo@example.com
+Received: from test1.example.com (test1.example.com [192.168.10.10])
+    by test2.example.com (Postfix) with ESMTP id E6F7610184CDD
+    for <foo@example.com>; Sat, 26 Jul 2008 22:55:03 -0600 (MDT)
+Message-ID: <aaabbbcccddd111222333444@test1.example.com>
+Date: Sat, 26 Jul 2008 23:55:00 -0500 (CDT)
+From: Test <test@example.com>
+To: foo@example.com
+Subject: Test e-mail 2
+Mime-Version: 1.0
+Content-Type: text/plain
+
+Test.
diff --git a/framework/Imap_Client/test/Horde/Imap/test_email2.txt b/framework/Imap_Client/test/Horde/Imap/test_email2.txt
new file mode 100644 (file)
index 0000000..33f1e8f
--- /dev/null
@@ -0,0 +1,112 @@
+Return-Path: <test@example.com>
+Delivered-To: test@example.com
+Received: from localhost (localhost [127.0.0.1])
+    by foo.example.com (Postfix) with ESMTP id BBCC02F667
+    for <test@example.com>; Thu,  2 Oct 2008 13:15:25 -0600 (MDT)
+Received: from foo2.example.com (foo2.example.com [192.168.100.100]) by
+    foo.example.com (Horde Framework) with HTTP;
+    Thu, 02 Oct 2008 13:15:25 -0600
+Message-ID: <2008yhnujm@foo.example.com>
+Date: Thu, 02 Oct 2008 13:15:25 -0600
+From: "A. Test User" <test@example.com>
+To: test@example.com
+Subject: Fwd: Test
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary="=_5oyqt8yksw5p"
+Content-Transfer-Encoding: 7bit
+User-Agent: Test e-mail program
+
+This message is in MIME format.
+
+--=_5oyqt8yksw5p
+Content-Type: multipart/alternative; boundary="=_5otck98hqrbh"
+Content-Transfer-Encoding: 7bit
+
+This message is in MIME format.
+
+--=_5otck98hqrbh
+Content-Type: text/plain; charset=ISO-8859-1
+Content-Description: Plaintext Version of Message
+Content-Disposition: inline
+Content-Transfer-Encoding: 7bit
+
+multipart/alternative test.
+
+--=_5otck98hqrbh
+Content-Type: text/html; charset=ISO-8859-1
+Content-Description: HTML Version of Message
+Content-Disposition: inline
+Content-Transfer-Encoding: 7bit
+
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+    <head>
+        <title></title>
+    </head>
+    <body>
+        <p>
+            multipart/alternative test.<br />
+        </p>
+    </body>
+</html>
+--=_5otck98hqrbh--
+
+--=_5oyqt8yksw5p
+Content-Type: message/rfc822;
+    name="Forwarded Message: Test"
+MIME-Version: 1.0
+
+Return-Path: <test@example.com>
+Delivered-To: test@example.com
+Received: from localhost (localhost [127.0.0.1])
+    by foo.example.com (Postfix) with ESMTP id E57162F667
+    for <test@example.com>; Thu,  2 Oct 2008 13:12:11 -0600 (MDT)
+Received: from foo2.example.com (foo2.example.com [192.168.100.100]) by
+    foo.example.com (Horde Framework) with HTTP;
+    Thu, 02 Oct 2008 13:12:11 -0600
+Message-ID: <20081002131211.2404701yx4vklbez@test.example.com>
+Date: Thu, 02 Oct 2008 13:12:11 -0600
+From: Test User <test@example.com>
+To: Test User <test@example.com>
+Subject: Test
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary="=_22nxtd2snnyj"
+Content-Transfer-Encoding: 7bit
+User-Agent: Test Agent
+
+This message is in MIME format.
+
+--=_22nxtd2snnyj
+Content-Type: text/plain; charset=ISO-8859-1; DelSp="Yes"; format="flowed"
+Content-Disposition: inline
+Content-Transfer-Encoding: 7bit
+
+Test w/attachment
+
+--=_22nxtd2snnyj
+Content-Type: image/png; name="info_icon.png"
+Content-Disposition: attachment; filename="info_icon.png"
+Content-Transfer-Encoding: base64
+
+iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAwFBMVEUAAAAMExoUGyMVGyMcIysj
+OE0yRlwvS2cwS2czT2w0T2w1UnA5VnREWGw9WnhAW3hKWm1CXnxGYoBKZoRLZoNOaohVbYdTb41U
+b4xXc5Fbd5VldolfeJJgeJFfe5ljf51nf5pog6Fsh6Vuh6F3hpZ9ipiBlq2AmLOHm66WqL2Yqr2Z
+rMCcrL2issOltMamtciwvc7/////////////////////////////////////////////////////
+//////+mdyt7AAAAQHRSTlP/////////////////////////////////////////////////////
+//////////////////////////////8AwnuxRAAAAIxJREFUeNptj1cOwkAMRIdesoQQSkjooYc6
+J+D+t8L2CpAi5u89z2ptvErxApafwK64nA89fET9ccw3q2U2rXmBpzCZpckIKjDTOZkm42ETKgrr
+K8d9E3dhksKD0MRN56SyM3HVPqncNbHfyntSOGjAvj2thUkXtOEXqy7msc5ble/q0SR0Hen/P66U
+N4YNJcNOYmoYAAAAAElFTkSuQmCC
+
+--=_22nxtd2snnyj--
+
+
+--=_5oyqt8yksw5p
+Content-Type: text/plain; charset=UTF-8; name="test_txt.txt"
+Content-Disposition: attachment; filename="test_txt.txt"
+Content-Transfer-Encoding: 7bit
+
+Test text attachment.
+--=_5oyqt8yksw5p--
+