<?php
/**
 * Copyright 2008-2017 Horde LLC (http://www.horde.org/)
 *
 * See the enclosed file LICENSE for license information (LGPL). If you
 * did not receive this file, see http://www.horde.org/licenses/lgpl21.
 *
 * @category  Horde
 * @copyright 2008-2017 Horde LLC
 * @license   http://www.horde.org/licenses/lgpl21 LGPL 2.1
 * @package   Imap_Client
 */

/**
 * An abstracted API interface to IMAP backends supporting the IMAP4rev1
 * protocol (RFC 3501).
 *
 * @author    Michael Slusarz <slusarz@horde.org>
 * @category  Horde
 * @copyright 2008-2017 Horde LLC
 * @license   http://www.horde.org/licenses/lgpl21 LGPL 2.1
 * @package   Imap_Client
 *
 * @property-read Horde_Imap_Client_Base_Alert $alerts_ob
                  The alert reporting object (@since 2.26.0)
 * @property-read Horde_Imap_Client_Data_Capability $capability
 *                A capability object. (@since 2.24.0)
 * @property-read Horde_Imap_Client_Data_SearchCharset $search_charset
 *                A search charset object. (@since 2.24.0)
 * @property-read Horde_Imap_Client_Url $url  The URL object for the current
 *                connection parameters (@since 2.24.0)
 */
abstract class Horde_Imap_Client_Base
implements Serializable, SplObserver
{
    /** Serialized version. */
    const VERSION = 3;

    /** Cache names for miscellaneous data. */
    const CACHE_MODSEQ = '_m';
    const CACHE_SEARCH = '_s';
    /* @since 2.9.0 */
    const CACHE_SEARCHID = '_i';

    /** Cache names used exclusively within this class. @since 2.11.0 */
    const CACHE_DOWNGRADED = 'HICdg';

    /**
     * The list of fetch fields that can be cached, and their cache names.
     *
     * @var array
     */
    public $cacheFields = array(
        Horde_Imap_Client::FETCH_ENVELOPE => 'HICenv',
        Horde_Imap_Client::FETCH_FLAGS => 'HICflags',
        Horde_Imap_Client::FETCH_HEADERS => 'HIChdrs',
        Horde_Imap_Client::FETCH_IMAPDATE => 'HICdate',
        Horde_Imap_Client::FETCH_SIZE => 'HICsize',
        Horde_Imap_Client::FETCH_STRUCTURE => 'HICstruct'
    );

    /**
     * Has the internal configuration changed?
     *
     * @var boolean
     */
    public $changed = false;

    /**
     * Horde_Imap_Client is optimized for short (i.e. 1 seconds) scripts. It
     * makes heavy use of mailbox caching to save on server accesses. This
     * property should be set to false for long-running scripts, or else
     * status() data may not reflect the current state of the mailbox on the
     * server.
     *
     * @since 2.14.0
     *
     * @var boolean
     */
    public $statuscache = true;

    /**
     * Alerts reporting object.
     *
     * @var Horde_Imap_Client_Base_Alerts
     */
    protected $_alerts;

    /**
     * The Horde_Imap_Client_Cache object.
     *
     * @var Horde_Imap_Client_Cache
     */
    protected $_cache = null;

    /**
     * Connection to the IMAP server.
     *
     * @var Horde\Socket\Client
     */
    protected $_connection = null;

    /**
     * The debug object.
     *
     * @var Horde_Imap_Client_Base_Debug
     */
    protected $_debug = null;

    /**
     * The default ports to use for a connection.
     * First element is non-secure, second is SSL.
     *
     * @var array
     */
    protected $_defaultPorts = array();

    /**
     * The fetch data object type to return.
     *
     * @var string
     */
    protected $_fetchDataClass = 'Horde_Imap_Client_Data_Fetch';

    /**
     * Cached server data.
     *
     * @var array
     */
    protected $_init;

    /**
     * Is there an active authenticated connection to the IMAP Server?
     *
     * @var boolean
     */
    protected $_isAuthenticated = false;

    /**
     * The current mailbox selection mode.
     *
     * @var integer
     */
    protected $_mode = 0;

    /**
     * Hash containing connection parameters.
     * This hash never changes.
     *
     * @var array
     */
    protected $_params = array();

    /**
     * The currently selected mailbox.
     *
     * @var Horde_Imap_Client_Mailbox
     */
    protected $_selected = null;

    /**
     * Temp array (destroyed at end of process).
     *
     * @var array
     */
    protected $_temp = array();

    /**
     * Constructor.
     *
     * @param array $params   Configuration parameters:
     * <pre>
     * - cache: (array) If set, caches data from fetch(), search(), and
     *          thread() calls. Requires the horde/Cache package to be
     *          installed. The array can contain the following keys (see
     *          Horde_Imap_Client_Cache for default values):
     *   - backend: [REQUIRED (or cacheob)] (Horde_Imap_Client_Cache_Backend)
     *              Backend cache driver [@since 2.9.0].
     *   - fetch_ignore: (array) A list of mailboxes to ignore when storing
     *                   fetch data.
     *   - fields: (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:
     *     - Horde_Imap_Client::FETCH_ENVELOPE
     *     - Horde_Imap_Client::FETCH_FLAGS
     *       Only if server supports CONDSTORE extension
     *     - Horde_Imap_Client::FETCH_HEADERS
     *       Only for queries that specifically request caching
     *     - Horde_Imap_Client::FETCH_IMAPDATE
     *     - Horde_Imap_Client::FETCH_SIZE
     *     - Horde_Imap_Client::FETCH_STRUCTURE
     * - capability_ignore: (array) A list of IMAP capabilites to ignore, even
     *                      if they are supported on the server.
     *                      DEFAULT: No supported capabilities are ignored.
     * - comparator: (string) The search comparator to use instead of the
     *               default server comparator. See setComparator() for
     *               format.
     *               DEFAULT: Use the server default
     * - context: (array) Any context parameters passed to
     *            stream_create_context(). @since 2.27.0
     * - debug: (string) If set, will output debug information to the stream
     *          provided. The value can be any PHP supported wrapper that can
     *          be opened via PHP's fopen() function.
     *          DEFAULT: No debug output
     * - hostspec: (string) The hostname or IP address of the server.
     *             DEFAULT: 'localhost'
     * - id: (array) Send ID information to the server (only if server
     *       supports the ID extension). An array with the keys as the fields
     *       to send and the values being the associated values. See RFC 2971
     *       [3.3] for a list of standard 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
     * - password: (mixed) The user password. Either a string or a
     *             Horde_Imap_Client_Base_Password object [@since 2.14.0].
     * - 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 (No encryption)
     *   - 'ssl' (Auto-detect SSL version)
     *   - 'sslv2' (Force SSL version 3)
     *   - 'sslv3' (Force SSL version 2)
     *   - 'tls' (TLS; started via protocol-level negotation over
     *     unencrypted channel; RECOMMENDED way of initiating secure
     *     connection)
     *   - 'tlsv1' (TLS direct version 1.x connection to server) [@since
     *     2.16.0]
     *   - true (TLS if available/necessary) [@since 2.15.0]
     *     DEFAULT: false
     * - timeout: (integer)  Connection timeout, in seconds.
     *            DEFAULT: 30 seconds
     * - username: (string) [REQUIRED] The username.
     * - authusername (string) The username used for SASL authentication.
     * 	 If specified this is the user name whose password is used
     * 	 (e.g. administrator).
     * 	 Only valid for RFC 2595/4616 - PLAIN SASL mechanism.
     * 	 DEFAULT: the same value provided in the username parameter.
     * </pre>
     */
    public function __construct(array $params = array())
    {
        if (!isset($params['username'])) {
            throw new InvalidArgumentException('Horde_Imap_Client requires a username.');
        }

        $this->_setInit();

        // Default values.
        $params = array_merge(array(
            'context' => array(),
            'hostspec' => 'localhost',
            'secure' => false,
            'timeout' => 30
        ), array_filter($params));

        if (!isset($params['port']) && strpos($params['hostspec'], 'unix://') !== 0) {
            $params['port'] = (!empty($params['secure']) && in_array($params['secure'], array('ssl', 'sslv2', 'sslv3'), true))
                ? $this->_defaultPorts[1]
                : $this->_defaultPorts[0];
        }

        if (empty($params['cache'])) {
            $params['cache'] = array('fields' => array());
        } elseif (empty($params['cache']['fields'])) {
            $params['cache']['fields'] = $this->cacheFields;
        } else {
            $params['cache']['fields'] = array_flip($params['cache']['fields']);
        }

        if (empty($params['cache']['fetch_ignore'])) {
            $params['cache']['fetch_ignore'] = array();
        }

        $this->_params = $params;
        if (isset($params['password'])) {
            $this->setParam('password', $params['password']);
        }

        $this->changed = true;
        $this->_initOb();
    }

    /**
     * Get encryption key.
     *
     * @deprecated  Pass callable into 'password' parameter instead.
     *
     * @return string  The encryption key.
     */
    protected function _getEncryptKey()
    {
        if (is_callable($ekey = $this->getParam('encryptKey'))) {
            return call_user_func($ekey);
        }

        throw new InvalidArgumentException('encryptKey parameter is not a valid callback.');
    }

    /**
     * Do initialization tasks.
     */
    protected function _initOb()
    {
        register_shutdown_function(array($this, 'shutdown'));

        $this->_alerts = new Horde_Imap_Client_Base_Alerts();
        // @todo: Remove (BC)
        $this->_alerts->attach($this);

        $this->_debug = ($debug = $this->getParam('debug'))
            ? new Horde_Imap_Client_Base_Debug($debug)
            : new Horde_Support_Stub();

        // @todo: Remove (BC purposes)
        if (isset($this->_init['capability']) &&
            !is_object($this->_init['capability'])) {
            $this->_setInit('capability');
        }

        foreach (array('capability', 'search_charset') as $val) {
            if (isset($this->_init[$val])) {
                $this->_init[$val]->attach($this);
            }
        }
    }

    /**
     * Shutdown actions.
     */
    public function shutdown()
    {
        try {
            $this->logout();
        } catch (Horde_Imap_Client_Exception $e) {
        }
    }

    /**
     * This object can not be cloned.
     */
    public function __clone()
    {
        throw new LogicException('Object cannot be cloned.');
    }

    /**
     */
    #[ReturnTypeWillChange]
    public function update(SplSubject $subject)
    {
        if (($subject instanceof Horde_Imap_Client_Data_Capability) ||
            ($subject instanceof Horde_Imap_Client_Data_SearchCharset)) {
            $this->changed = true;
        }

        /* @todo: BC - remove */
        if ($subject instanceof Horde_Imap_Client_Base_Alerts) {
            $this->_temp['alerts'][] = $subject->getLast()->alert;
        }
    }

    /**
     */
    public function serialize()
    {
        return serialize($this->__serialize());
    }

    /**
     */
    public function unserialize($data)
    {
        $data = @unserialize($data);
        if (!is_array($data)) {
            throw new Exception('Cache version change');
        }
        $this->__unserialize($data);
    }

    /**
     * @return array
     */
    public function __serialize()
    {
        return array(
            'i' => $this->_init,
            'p' => $this->_params,
            'v' => self::VERSION
        );
    }

    public function __unserialize(array $data)
    {
        if (empty($data['v']) || $data['v'] != self::VERSION) {
            throw new Exception('Cache version change');
        }

        $this->_init = $data['i'];
        $this->_params = $data['p'];

        $this->_initOb();
    }

    /**
     */
    public function __get($name)
    {
        switch ($name) {
        case 'alerts_ob':
            return $this->_alerts;

        case 'capability':
            return $this->_capability();

        case 'search_charset':
            if (!isset($this->_init['search_charset'])) {
                $this->_init['search_charset'] = new Horde_Imap_Client_Data_SearchCharset();
                $this->_init['search_charset']->attach($this);
            }
            $this->_init['search_charset']->setBaseOb($this);
            return $this->_init['search_charset'];

        case 'url':
            $url = new Horde_Imap_Client_Url();
            $url->hostspec = $this->getParam('hostspec');
            $url->port = $this->getParam('port');
            $url->protocol = 'imap';
            return $url;
        }
    }

    /**
     * Set an initialization value.
     *
     * @param string $key  The initialization key. If null, resets all keys.
     * @param mixed $val   The cached value. If null, removes the key.
     */
    public function _setInit($key = null, $val = null)
    {
        if (is_null($key)) {
            $this->_init = array();
        } elseif (is_null($val)) {
            unset($this->_init[$key]);
        } else {
            switch ($key) {
            case 'capability':
                if ($ci = $this->getParam('capability_ignore')) {
                    $ignored = array();

                    foreach ($ci as $val2) {
                        $c = explode('=', $val2);

                        if ($val->query($c[0], isset($c[1]) ? $c[1] : null)) {
                            $ignored[] = $val2;
                            $val->remove($c[0], isset($c[1]) ? $c[1] : null);
                        }
                    }

                    if ($this->_debug->debug && !empty($ignored)) {
                        $this->_debug->info(sprintf(
                            'CONFIG: IGNORING these IMAP capabilities: %s',
                            implode(', ', $ignored)
                        ));
                    }
                }

                $val->attach($this);
                break;
            }

            /* Nothing has changed. */
            if (isset($this->_init[$key]) && ($this->_init[$key] === $val)) {
                return;
            }

            $this->_init[$key] = $val;
        }

        $this->changed = true;
    }

    /**
     * Initialize the Horde_Imap_Client_Cache object, if necessary.
     *
     * @param boolean $current  If true, we are going to update the currently
     *                          selected mailbox. Add an additional check to
     *                          see if caching is available in current
     *                          mailbox.
     *
     * @return boolean  Returns true if caching is enabled.
     */
    protected function _initCache($current = false)
    {
        $c = $this->getParam('cache');

        if (empty($c['fields'])) {
            return false;
        }

        if (is_null($this->_cache)) {
            if (isset($c['backend'])) {
                $backend = $c['backend'];
            } elseif (isset($c['cacheob'])) {
                /* Deprecated */
                $backend = new Horde_Imap_Client_Cache_Backend_Cache($c);
            } else {
                return false;
            }

            $this->_cache = new Horde_Imap_Client_Cache(array(
                'backend' => $backend,
                'baseob' => $this,
                'debug' => $this->_debug
            ));
        }

        return $current
            /* If UIDs are labeled as not sticky, don't cache since UIDs will
             * change on every access. */
            ? !($this->_mailboxOb()->getStatus(Horde_Imap_Client::STATUS_UIDNOTSTICKY))
            : true;
    }

    /**
     * Returns a value from the internal params array.
     *
     * @param string $key  The param key.
     *
     * @return mixed  The param value, or null if not found.
     */
    public function getParam($key)
    {
        /* Passwords may be stored encrypted. */
        switch ($key) {
        case 'password':
            if (isset($this->_params[$key]) &&
                ($this->_params[$key] instanceof Horde_Imap_Client_Base_Password)) {
                return $this->_params[$key]->getPassword();
            }

            // DEPRECATED
            if (!empty($this->_params['_passencrypt'])) {
                try {
                    $secret = new Horde_Secret();
                    return $secret->read($this->_getEncryptKey(), $this->_params['password']);
                } catch (Exception $e) {
                    return null;
                }
            }
            break;
        }

        return isset($this->_params[$key])
            ? $this->_params[$key]
            : null;
    }

    /**
     * Sets a configuration parameter value.
     *
     * @param string $key  The param key.
     * @param mixed $val   The param value.
     */
    public function setParam($key, $val)
    {
        switch ($key) {
        case 'password':
            if ($val instanceof Horde_Imap_Client_Base_Password) {
                break;
            }

            // DEPRECATED: Encrypt password.
            try {
                $encrypt_key = $this->_getEncryptKey();
                if (strlen($encrypt_key)) {
                    $secret = new Horde_Secret();
                    $val = $secret->write($encrypt_key, $val);
                    $this->_params['_passencrypt'] = true;
                }
            } catch (Exception $e) {}
            break;
        }

        $this->_params[$key] = $val;
        $this->changed = true;
    }

    /**
     * Returns the Horde_Imap_Client_Cache object used, if available.
     *
     * @return mixed  Either the cache object or null.
     */
    public function getCache()
    {
        $this->_initCache();
        return $this->_cache;
    }

    /**
     * Returns the correct IDs object for use with this driver.
     *
     * @param mixed $ids  Either self::ALL, self::SEARCH_RES, self::LARGEST,
     *                    Horde_Imap_Client_Ids object, array, or sequence
     *                    string.
     * @param boolean $sequence  Are $ids message sequence numbers?
     *
     * @return Horde_Imap_Client_Ids  The IDs object.
     */
    public function getIdsOb($ids = null, $sequence = false)
    {
        return new Horde_Imap_Client_Ids($ids, $sequence);
    }

    /**
     * Returns whether the IMAP server supports the given capability
     * (See RFC 3501 [6.1.1]).
     *
     * @deprecated  Use $capability property instead.
     *
     * @param string $capability  The capability string to query.
     *
     * @return 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)
    {
        try {
            $c = $this->_capability();
            return ($out = $c->getParams($capability))
                ? $out
                : $c->query($capability);
        } catch (Horde_Imap_Client_Exception $e) {
            return false;
        }
    }

    /**
     * Get CAPABILITY information from the IMAP server.
     *
     * @deprecated  Use $capability property instead.
     *
     * @return array  The capability array.
     *
     * @throws Horde_Imap_Client_Exception
     */
    public function capability()
    {
        return $this->_capability()->toArray();
    }

    /**
     * Query server capability.
     *
     * Required because internal code can't call capability via magic method
     * directly - it may not exist yet, the creation code may call capability
     * recursively, and __get() doesn't allow recursive calls to the same
     * property (chicken/egg issue).
     *
     * @return mixed  The capability object if no arguments provided. If
     *                arguments are provided, they are passed to the query()
     *                method and this value is returned.
     * @throws Horde_Imap_Client_Exception
     */
    protected function _capability()
    {
        if (!isset($this->_init['capability'])) {
            $this->_initCapability();
        }

        return ($args = func_num_args())
            ? $this->_init['capability']->query(func_get_arg(0), ($args > 1) ? func_get_arg(1) : null)
            : $this->_init['capability'];
    }

    /**
     * Retrieve capability information from the IMAP server.
     *
     * @throws Horde_Imap_Client_Exception
     */
    abstract protected function _initCapability();

    /**
     * Send a NOOP command (RFC 3501 [6.1.2]).
     *
     * @throws Horde_Imap_Client_Exception
     */
    public function noop()
    {
        if (!$this->_connection) {
            // NOOP can be called in the unauthenticated state.
            $this->_connect();
        }
        $this->_noop();
    }

    /**
     * Send a NOOP command.
     *
     * @throws Horde_Imap_Client_Exception
     */
    abstract protected function _noop();

    /**
     * Get the NAMESPACE information from the IMAP server (RFC 2342).
     *
     * @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 be UTF-8
     *                           strings.
     * @param array $opts        Additional options:
     *   - ob_return: (boolean) If true, returns a
     *                Horde_Imap_Client_Namespace_List object instead of an
     *                array.
     *
     * @return mixed  A Horde_Imap_Client_Namespace_List object if
     *                'ob_return', is true. Otherwise, an array of namespace
     *                objects (@deprecated) with the name as the key (UTF-8)
     *                and the following values:
     * <pre>
     *  - delimiter: (string) The namespace delimiter.
     *  - hidden: (boolean) Is this a hidden namespace?
     *  - name: (string) The namespace name (UTF-8).
     *  - translation: (string) Returns the translated name of the namespace
     *                 (UTF-8). Requires RFC 5255 and a previous call to
     *                 setLanguage().
     *  - type: (integer) The namespace type. Either:
     *    - Horde_Imap_Client::NS_PERSONAL
     *    - Horde_Imap_Client::NS_OTHER
     *    - Horde_Imap_Client::NS_SHARED
     * </pre>
     *
     * @throws Horde_Imap_Client_Exception
     */
    public function getNamespaces(
        array $additional = array(), array $opts = array()
    )
    {
        $additional = array_map('strval', $additional);
        $sig = hash(
            'md5',
            json_encode($additional) . intval(empty($opts['ob_return']))
        );

        if (isset($this->_init['namespace'][$sig])) {
            $ns = $this->_init['namespace'][$sig];
        } else {
            $this->login();

            $ns = $this->_getNamespaces();

            /* Skip namespaces if we have already auto-detected them. Also,
             * hidden namespaces cannot be empty. */
            $to_process = array_diff(array_filter($additional, 'strlen'), array_map('strlen', iterator_to_array($ns)));
            if (!empty($to_process)) {
                foreach ($this->listMailboxes($to_process, Horde_Imap_Client::MBOX_ALL, array('delimiter' => true)) as $key => $val) {
                    $ob = new Horde_Imap_Client_Data_Namespace();
                    $ob->delimiter = $val['delimiter'];
                    $ob->hidden = true;
                    $ob->name = $key;
                    $ob->type = $ob::NS_SHARED;
                    $ns[$val] = $ob;
                }
            }

            if (!count($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('', Horde_Imap_Client::MBOX_ALL, array('delimiter' => true));
                $first = reset($mbox);

                $ob = new Horde_Imap_Client_Data_Namespace();
                $ob->delimiter = $first['delimiter'];
                $ns[''] = $ob;
            }

            $this->_init['namespace'][$sig] = $ns;
            $this->_setInit('namespace', $this->_init['namespace']);
        }

        if (!empty($opts['ob_return'])) {
            return $ns;
        }

        /* @todo Remove for 3.0 */
        $out = array();
        foreach ($ns as $key => $val) {
            $out[$key] = array(
                'delimiter' => $val->delimiter,
                'hidden' => $val->hidden,
                'name' => $val->name,
                'translation' => $val->translation,
                'type' => $val->type
            );
        }

        return $out;
    }

    /**
     * Get the NAMESPACE information from the IMAP server.
     *
     * @return Horde_Imap_Client_Namespace_List  Namespace list object.
     *
     * @throws Horde_Imap_Client_Exception
     */
    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->_connection && $this->_connection->secure);
    }

    /**
     * Connect to the remote server.
     *
     * @throws Horde_Imap_Client_Exception
     */
    abstract protected function _connect();

    /**
     * Return a list of alerts that MUST be presented to the user (RFC 3501
     * [7.1]).
     *
     * @deprecated  Add an observer to the $alerts_ob property instead.
     *
     * @return array  An array of alert messages.
     */
    public function alerts()
    {
        $alerts = isset($this->_temp['alerts'])
            ? $this->_temp['alerts']
            : array();
        unset($this->_temp['alerts']);
        return $alerts;
    }

    /**
     * Login to the IMAP server.
     *
     * @throws Horde_Imap_Client_Exception
     */
    public function login()
    {
        if (!$this->_isAuthenticated && $this->_login()) {
            if ($this->getParam('id')) {
                try {
                    $this->sendID();
                    /* ID is queued - force sending the queued command. */
                    $this->_sendCmd($this->_pipeline());
                } catch (Horde_Imap_Client_Exception_NoSupportExtension $e) {
                    // Ignore if server doesn't support ID extension.
                }
            }

            if ($this->getParam('comparator')) {
                try {
                    $this->setComparator();
                } catch (Horde_Imap_Client_Exception_NoSupportExtension $e) {
                    // Ignore if server doesn't support I18NLEVEL=2
                }
            }
        }

        $this->_isAuthenticated = true;
    }

    /**
     * Login to the IMAP server.
     *
     * @return boolean  Return true if global login tasks should be run.
     *
     * @throws Horde_Imap_Client_Exception
     */
    abstract protected function _login();

    /**
     * Logout from the IMAP server (see RFC 3501 [6.1.3]).
     */
    public function logout()
    {
        if ($this->_isAuthenticated && $this->_connection->connected) {
            $this->_logout();
            $this->_connection->close();
        }

        $this->_connection = $this->_selected = null;
        $this->_isAuthenticated = false;
        $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).
     *
     * @param array $info  Overrides the value of the 'id' param and sends
     *                     this information instead.
     *
     * @throws Horde_Imap_Client_Exception
     * @throws Horde_Imap_Client_Exception_NoSupportExtension
     */
    public function sendID($info = null)
    {
        if (!$this->_capability('ID')) {
            throw new Horde_Imap_Client_Exception_NoSupportExtension('ID');
        }

        $this->_sendID(is_null($info) ? ($this->getParam('id') ?: array()) : $info);
    }

    /**
     * Send ID information to the IMAP server (RFC 2971).
     *
     * @param array $info  The information to send to the server.
     *
     * @throws Horde_Imap_Client_Exception
     */
    abstract protected function _sendID($info);

    /**
     * Return ID information from the IMAP server (RFC 2971).
     *
     * @return array  An array of information returned, with the keys as the
     *                'field' and the values as the 'value'.
     *
     * @throws Horde_Imap_Client_Exception
     * @throws Horde_Imap_Client_Exception_NoSupportExtension
     */
    public function getID()
    {
        if (!$this->_capability('ID')) {
            throw new Horde_Imap_Client_Exception_NoSupportExtension('ID');
        }

        return $this->_getID();
    }

    /**
     * Return ID information from the IMAP server (RFC 2971).
     *
     * @return array  An array of information returned, with the keys as the
     *                'field' and the values as the 'value'.
     *
     * @throws Horde_Imap_Client_Exception
     */
    abstract protected function _getID();

    /**
     * Sets the preferred language for server response messages (RFC 5255).
     *
     * @param array $langs  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.
     *
     * @throws Horde_Imap_Client_Exception
     */
    public function setLanguage($langs = null)
    {
        $lang = null;

        if ($this->_capability('LANGUAGE')) {
            $lang = is_null($langs)
                ? $this->getParam('lang')
                : $langs;
        }

        return is_null($lang)
            ? null
            : $this->_setLanguage($lang);
    }

    /**
     * Sets the preferred language for server response messages (RFC 5255).
     *
     * @param array $langs  The preferred list of languages.
     *
     * @return string  The language accepted by the server, or null if the
     *                 default language is used.
     *
     * @throws Horde_Imap_Client_Exception
     */
    abstract protected function _setLanguage($langs);

    /**
     * Gets the preferred language for server response messages (RFC 5255).
     *
     * @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.
     *
     * @throws Horde_Imap_Client_Exception
     */
    public function getLanguage($list = false)
    {
        if (!$this->_capability('LANGUAGE')) {
            return $list ? array() : null;
        }

        return $this->_getLanguage($list);
    }

    /**
     * Gets the preferred language for server response messages (RFC 5255).
     *
     * @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.
     *
     * @throws Horde_Imap_Client_Exception
     */
    abstract protected function _getLanguage($list);

    /**
     * Open a mailbox.
     *
     * @param mixed $mailbox  The mailbox to open. Either a
     *                        Horde_Imap_Client_Mailbox object or a string
     *                        (UTF-8).
     * @param integer $mode   The access mode. Either
     *   - Horde_Imap_Client::OPEN_READONLY
     *   - Horde_Imap_Client::OPEN_READWRITE
     *   - Horde_Imap_Client::OPEN_AUTO
     *
     * @throws Horde_Imap_Client_Exception
     */
    public function openMailbox($mailbox, $mode = Horde_Imap_Client::OPEN_AUTO)
    {
        $this->login();

        $change = false;
        $mailbox = Horde_Imap_Client_Mailbox::get($mailbox);

        if ($mode == Horde_Imap_Client::OPEN_AUTO) {
            if (is_null($this->_selected) ||
                !$mailbox->equals($this->_selected)) {
                $mode = Horde_Imap_Client::OPEN_READONLY;
                $change = true;
            }
        } else {
            $change = (is_null($this->_selected) ||
                       !$mailbox->equals($this->_selected) ||
                       ($mode != $this->_mode));
        }

        if ($change) {
            $this->_openMailbox($mailbox, $mode);
            $this->_mailboxOb()->open = true;
            if ($this->_initCache(true)) {
                $this->_condstoreSync();
            }
        }
    }

    /**
     * Open a mailbox.
     *
     * @param Horde_Imap_Client_Mailbox $mailbox  The mailbox to open.
     * @param integer $mode                       The access mode.
     *
     * @throws Horde_Imap_Client_Exception
     */
    abstract protected function _openMailbox(Horde_Imap_Client_Mailbox $mailbox,
                                             $mode);

    /**
     * Called when the selected mailbox is changed.
     *
     * @param mixed $mailbox  The selected mailbox or null.
     * @param integer $mode   The access mode.
     */
    protected function _changeSelected($mailbox = null, $mode = null)
    {
        $this->_mode = $mode;
        if (is_null($mailbox)) {
            $this->_selected = null;
        } else {
            $this->_selected = clone $mailbox;
            $this->_mailboxOb()->reset();
        }
    }

    /**
     * Return the Horde_Imap_Client_Base_Mailbox object.
     *
     * @param string $mailbox  The mailbox name. Defaults to currently
     *                         selected mailbox.
     *
     * @return Horde_Imap_Client_Base_Mailbox  Mailbox object.
     */
    protected function _mailboxOb($mailbox = null)
    {
        $name = is_null($mailbox)
            ? strval($this->_selected)
            : strval($mailbox);

        if (!isset($this->_temp['mailbox_ob'][$name])) {
            $this->_temp['mailbox_ob'][$name] = new Horde_Imap_Client_Base_Mailbox();
        }

        return $this->_temp['mailbox_ob'][$name];
    }

    /**
     * Return the currently opened mailbox and access mode.
     *
     * @return mixed  Null if no mailbox selected, or an array with two
     *                elements:
     *   - mailbox: (Horde_Imap_Client_Mailbox) The mailbox object.
     *   - mode: (integer) Current mode.
     *
     * @throws Horde_Imap_Client_Exception
     */
    public function currentMailbox()
    {
        return is_null($this->_selected)
            ? null
            : array(
                'mailbox' => clone $this->_selected,
                'mode' => $this->_mode
            );
    }

    /**
     * Create a mailbox.
     *
     * @param mixed $mailbox  The mailbox to create. Either a
     *                        Horde_Imap_Client_Mailbox object or a string
     *                        (UTF-8).
     * @param array $opts     Additional options:
     *   - special_use: (array) An array of special-use flags to mark the
     *                  mailbox with. The server MUST support RFC 6154.
     *
     * @throws Horde_Imap_Client_Exception
     */
    public function createMailbox($mailbox, array $opts = array())
    {
        $this->login();

        if (!$this->_capability('CREATE-SPECIAL-USE')) {
            unset($opts['special_use']);
        }

        $this->_createMailbox(Horde_Imap_Client_Mailbox::get($mailbox), $opts);
    }

    /**
     * Create a mailbox.
     *
     * @param Horde_Imap_Client_Mailbox $mailbox  The mailbox to create.
     * @param array $opts                         Additional options. See
     *                                            createMailbox().
     *
     * @throws Horde_Imap_Client_Exception
     */
    abstract protected function _createMailbox(Horde_Imap_Client_Mailbox $mailbox,
                                               $opts);

    /**
     * Delete a mailbox.
     *
     * @param mixed $mailbox  The mailbox to delete. Either a
     *                        Horde_Imap_Client_Mailbox object or a string
     *                        (UTF-8).
     *
     * @throws Horde_Imap_Client_Exception
     */
    public function deleteMailbox($mailbox)
    {
        $this->login();

        $mailbox = Horde_Imap_Client_Mailbox::get($mailbox);

        $this->_deleteMailbox($mailbox);
        $this->_deleteMailboxPost($mailbox);
    }

    /**
     * Delete a mailbox.
     *
     * @param Horde_Imap_Client_Mailbox $mailbox  The mailbox to delete.
     *
     * @throws Horde_Imap_Client_Exception
     */
    abstract protected function _deleteMailbox(Horde_Imap_Client_Mailbox $mailbox);

    /**
     * Actions to perform after a mailbox delete.
     *
     * @param Horde_Imap_Client_Mailbox $mailbox  The deleted mailbox.
     */
    protected function _deleteMailboxPost(Horde_Imap_Client_Mailbox $mailbox)
    {
        /* Delete mailbox caches. */
        if ($this->_initCache()) {
            $this->_cache->deleteMailbox($mailbox);
        }
        unset($this->_temp['mailbox_ob'][strval($mailbox)]);

        /* Unsubscribe from mailbox. */
        try {
            $this->subscribeMailbox($mailbox, false);
        } catch (Horde_Imap_Client_Exception $e) {
            // Ignore failed unsubscribe request
        }
    }

    /**
     * Rename a mailbox.
     *
     * @param mixed $old  The old mailbox name. Either a
     *                    Horde_Imap_Client_Mailbox object or a string (UTF-8).
     * @param mixed $new  The new mailbox name. Either a
     *                    Horde_Imap_Client_Mailbox object or a string (UTF-8).
     *
     * @throws Horde_Imap_Client_Exception
     */
    public function renameMailbox($old, $new)
    {
        // Login will be handled by first listMailboxes() call.

        $old = Horde_Imap_Client_Mailbox::get($old);
        $new = Horde_Imap_Client_Mailbox::get($new);

        /* Check if old mailbox(es) were subscribed to. */
        $base = $this->listMailboxes($old, Horde_Imap_Client::MBOX_SUBSCRIBED, array('delimiter' => true));
        if (empty($base)) {
            $base = $this->listMailboxes($old, Horde_Imap_Client::MBOX_ALL, array('delimiter' => true));
            $base = reset($base);
            $subscribed = array();
        } else {
            $base = reset($base);
            $subscribed = array($base['mailbox']);
        }

        $all_mboxes = array($base['mailbox']);
        if (strlen($base['delimiter'])) {
            $search = $old->list_escape . $base['delimiter'] . '*';
            $all_mboxes = array_merge($all_mboxes, $this->listMailboxes($search, Horde_Imap_Client::MBOX_ALL, array('flat' => true)));
            $subscribed = array_merge($subscribed, $this->listMailboxes($search, Horde_Imap_Client::MBOX_SUBSCRIBED, array('flat' => true)));
        }

        $this->_renameMailbox($old, $new);

        /* Delete mailbox actions. */
        foreach ($all_mboxes as $val) {
            $this->_deleteMailboxPost($val);
        }

        foreach ($subscribed as $val) {
            try {
                $this->subscribeMailbox(new Horde_Imap_Client_Mailbox(substr_replace($val, $new, 0, strlen($old))));
            } catch (Horde_Imap_Client_Exception $e) {
                // Ignore failed subscription requests
            }
        }
    }

    /**
     * Rename a mailbox.
     *
     * @param Horde_Imap_Client_Mailbox $old  The old mailbox name.
     * @param Horde_Imap_Client_Mailbox $new  The new mailbox name.
     *
     * @throws Horde_Imap_Client_Exception
     */
    abstract protected function _renameMailbox(Horde_Imap_Client_Mailbox $old,
                                               Horde_Imap_Client_Mailbox $new);

    /**
     * Manage subscription status for a mailbox.
     *
     * @param mixed $mailbox      The mailbox to [un]subscribe to. Either a
     *                            Horde_Imap_Client_Mailbox object or a string
     *                            (UTF-8).
     * @param boolean $subscribe  True to subscribe, false to unsubscribe.
     *
     * @throws Horde_Imap_Client_Exception
     */
    public function subscribeMailbox($mailbox, $subscribe = true)
    {
        $this->login();
        $this->_subscribeMailbox(Horde_Imap_Client_Mailbox::get($mailbox), (bool)$subscribe);
    }

    /**
     * Manage subscription status for a mailbox.
     *
     * @param Horde_Imap_Client_Mailbox $mailbox  The mailbox to [un]subscribe
     *                                            to.
     * @param boolean $subscribe                  True to subscribe, false to
     *                                            unsubscribe.
     *
     * @throws Horde_Imap_Client_Exception
     */
    abstract protected function _subscribeMailbox(Horde_Imap_Client_Mailbox $mailbox,
                                                  $subscribe);

    /**
     * Obtain a list of mailboxes matching a pattern.
     *
     * @param mixed $pattern   The mailbox search pattern(s) (see RFC 3501
     *                         [6.3.8] for the format). A UTF-8 string or an
     *                         array of strings. If a Horde_Imap_Client_Mailbox
     *                         object is given, it is escaped (i.e. wildcard
     *                         patterns are converted to return the miminal
     *                         number of matches possible).
     * @param integer $mode    Which mailboxes to return.  Either:
     *   - Horde_Imap_Client::MBOX_SUBSCRIBED
     *     Return subscribed mailboxes.
     *   - Horde_Imap_Client::MBOX_SUBSCRIBED_EXISTS
     *     Return subscribed mailboxes that exist on the server.
     *   - Horde_Imap_Client::MBOX_UNSUBSCRIBED
     *     Return unsubscribed mailboxes.
     *   - Horde_Imap_Client::MBOX_ALL
     *     Return all mailboxes regardless of subscription status.
     *   - Horde_Imap_Client::MBOX_ALL_SUBSCRIBED (@since 2.23.0)
     *     Return all mailboxes regardless of subscription status, and ensure
     *     the '\subscribed' attribute is set if mailbox is subscribed
     *     (implies 'attributes' option is true).
     * @param array $options   Additional options:
     * <pre>
     *   - attributes: (boolean) If true, return attribute information under
     *                 the 'attributes' key.
     *                 DEFAULT: Do not return this information.
     *   - children: (boolean) Tell server to return children attribute
     *               information (\HasChildren, \HasNoChildren). Requires the
     *               LIST-EXTENDED extension to guarantee this information is
     *               returned. Server MAY return this attribute without this
     *               option, or if the CHILDREN extension is available, but it
     *               is not guaranteed.
     *               DEFAULT: false
     *   - flat: (boolean) If true, return a flat list of mailbox names only.
     *           Overrides the 'attributes' option.
     *           DEFAULT: Do not return flat list.
     *   - recursivematch: (boolean) Force the server to return information
     *                     about parent mailboxes that don't match other
     *                     selection options, but have some sub-mailboxes that
     *                     do. Information about children is returned in the
     *                     CHILDINFO extended data item ('extended'). Requires
     *                     the LIST-EXTENDED extension.
     *                     DEFAULT: false
     *   - remote: (boolean) Tell server to return mailboxes that reside on
     *             another server. Requires the LIST-EXTENDED extension.
     *             DEFAULT: false
     *   - special_use: (boolean) Tell server to return special-use attribute
     *                  information (see Horde_Imap_Client SPECIALUSE_*
     *                  constants). Server must support the SPECIAL-USE return
     *                  option for this setting to have any effect.
     *                  DEFAULT: false
     *   - status: (integer) Tell server to return status information. The
     *             value is a bitmask that may contain any of:
     *     - Horde_Imap_Client::STATUS_MESSAGES
     *     - Horde_Imap_Client::STATUS_RECENT
     *     - Horde_Imap_Client::STATUS_UIDNEXT
     *     - Horde_Imap_Client::STATUS_UIDVALIDITY
     *     - Horde_Imap_Client::STATUS_UNSEEN
     *     - Horde_Imap_Client::STATUS_HIGHESTMODSEQ
     *     DEFAULT: 0
     *   - 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 a list
     *                of Horde_Imap_Client_Mailbox objects. Otherwise, the
     *                keys are UTF-8 mailbox names and the values are arrays
     *                with these keys:
     *   - attributes: (array) List of lower-cased attributes [only if
     *                 'attributes' option is true].
     *   - delimiter: (string) The delimiter for the mailbox.
     *   - extended: (TODO) TODO [only if 'recursivematch' option is true and
     *               LIST-EXTENDED extension is supported on the server].
     *   - mailbox: (Horde_Imap_Client_Mailbox) The mailbox object.
     *   - status: (array) See status() [only if 'status' option is true].
     *
     * @throws Horde_Imap_Client_Exception
     */
    public function listMailboxes($pattern,
                                  $mode = Horde_Imap_Client::MBOX_ALL,
                                  array $options = array())
    {
        $this->login();

        $pattern = is_array($pattern)
            ? array_unique($pattern)
            : array($pattern);

        /* Prepare patterns. */
        $plist = array();
        foreach ($pattern as $val) {
            if ($val instanceof Horde_Imap_Client_Mailbox) {
                $val = $val->list_escape;
            }
            $plist[] = Horde_Imap_Client_Mailbox::get(preg_replace(
                array("/\*{2,}/", "/\%{2,}/"),
                array('*', '%'),
                Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($val)
            ), true);
        }

        if (isset($options['special_use']) &&
            !$this->_capability('SPECIAL-USE')) {
            unset($options['special_use']);
        }

        $ret = $this->_listMailboxes($plist, $mode, $options);

        if (!empty($options['status']) &&
            !$this->_capability('LIST-STATUS')) {
            foreach ($this->status(array_keys($ret), $options['status']) as $key => $val) {
                $ret[$key]['status'] = $val;
            }
        }

        if (empty($options['sort'])) {
            return $ret;
        }

        $list_ob = new Horde_Imap_Client_Mailbox_List(empty($options['flat']) ? array_keys($ret) : $ret);
        $sorted = $list_ob->sort(array(
            'delimiter' => empty($options['sort_delimiter']) ? '.' : $options['sort_delimiter']
        ));

        if (!empty($options['flat'])) {
            return $sorted;
        }

        $out = array();
        foreach ($sorted as $val) {
            $out[$val] = $ret[$val];
        }

        return $out;
    }

    /**
     * Obtain a list of mailboxes matching a pattern.
     *
     * @param array $pattern  The mailbox search patterns
     *                        (Horde_Imap_Client_Mailbox objects).
     * @param integer $mode   Which mailboxes to return.
     * @param array $options  Additional options.
     *
     * @return array  See listMailboxes().
     *
     * @throws Horde_Imap_Client_Exception
     */
    abstract protected function _listMailboxes($pattern, $mode, $options);

    /**
     * Obtain status information for a mailbox.
     *
     * @param mixed $mailbox  The mailbox(es) to query. Either a
     *                        Horde_Imap_Client_Mailbox object, a string
     *                        (UTF-8), or an array of objects/strings (since
     *                        2.10.0).
     * @param integer $flags  A bitmask of information requested from the
     *                        server. Allowed flags:
     * <pre>
     *   - Horde_Imap_Client::STATUS_MESSAGES
     *     Return key: messages
     *     Return format: (integer) The number of messages in the mailbox.
     *
     *   - Horde_Imap_Client::STATUS_RECENT
     *     Return key: recent
     *     Return format: (integer) The number of messages with the \Recent
     *                    flag set as currently reported in the mailbox
     *
     *   - Horde_Imap_Client::STATUS_RECENT_TOTAL
     *     Return key: recent_total
     *     Return format: (integer) The number of messages with the \Recent
     *                    flag set. This returns the total number of messages
     *                    that have been marked as recent in this mailbox
     *                    since the PHP process began. (since 2.12.0)
     *
     *   - Horde_Imap_Client::STATUS_UIDNEXT
     *     Return key: uidnext
     *     Return format: (integer) The next UID to be assigned in the
     *                    mailbox. Only returned if the server automatically
     *                    provides the data.
     *
     *   - Horde_Imap_Client::STATUS_UIDNEXT_FORCE
     *     Return key: uidnext
     *     Return format: (integer) The next UID to be assigned in the
     *                    mailbox. This option will always determine this
     *                    value, even if the server does not automatically
     *                    provide this data.
     *
     *   - Horde_Imap_Client::STATUS_UIDVALIDITY
     *     Return key: uidvalidity
     *     Return format: (integer) The unique identifier validity of the
     *                    mailbox.
     *
     *   - Horde_Imap_Client::STATUS_UNSEEN
     *     Return key: unseen
     *     Return format: (integer) The number of messages which do not have
     *                    the \Seen flag set.
     *
     *   - Horde_Imap_Client::STATUS_FIRSTUNSEEN
     *     Return key: firstunseen
     *     Return format: (integer) The sequence number of the first unseen
     *                    message in the mailbox.
     *
     *   - Horde_Imap_Client::STATUS_FLAGS
     *     Return key: flags
     *     Return format: (array) The list of defined flags in the mailbox
     *                    (all flags are in lowercase).
     *
     *   - 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).
     *
     *   - Horde_Imap_Client::STATUS_HIGHESTMODSEQ
     *     Return key: highestmodseq
     *     Return format: (integer) If the server supports the CONDSTORE
     *                    IMAP extension, this will be the highest
     *                    mod-sequence value of all messages in the mailbox.
     *                    Else 0 if CONDSTORE not available or the mailbox
     *                    does not support mod-sequences.
     *
     *   - Horde_Imap_Client::STATUS_SYNCMODSEQ
     *     Return key: syncmodseq
     *     Return format: (integer) If caching, and the server supports the
     *                    CONDSTORE IMAP extension, this is the cached
     *                    mod-sequence value of the mailbox when it was opened
     *                    for the first time in this access. Will be null if
     *                    not caching, CONDSTORE not available, or the mailbox
     *                    does not support mod-sequences.
     *
     *   - Horde_Imap_Client::STATUS_SYNCFLAGUIDS
     *     Return key: syncflaguids
     *     Return format: (Horde_Imap_Client_Ids) If caching, the server
     *                    supports the CONDSTORE IMAP extension, and the
     *                    mailbox contained cached data when opened for the
     *                    first time in this access, this is the list of UIDs
     *                    in which flags have changed since STATUS_SYNCMODSEQ.
     *
     *   - Horde_Imap_Client::STATUS_SYNCVANISHED
     *     Return key: syncvanished
     *     Return format: (Horde_Imap_Client_Ids) If caching, the server
     *                    supports the CONDSTORE IMAP extension, and the
     *                    mailbox contained cached data when opened for the
     *                    first time in this access, this is the list of UIDs
     *                    which have been deleted since STATUS_SYNCMODSEQ.
     *
     *   - 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.
     *
     *   - Horde_Imap_Client::STATUS_FORCE_REFRESH
     *     Normally, the status information will be cached for a given
     *     mailbox. Since most PHP requests are generally less than a second,
     *     this is fine. However, if your script is long running, the status
     *     information may not be up-to-date. Specifying this flag will ensure
     *     that the server is always polled for the current mailbox status
     *     before results are returned. (since 2.14.0)
     *
     *   - Horde_Imap_Client::STATUS_ALL (DEFAULT)
     *     Shortcut to return 'messages', 'recent', 'uidnext', 'uidvalidity',
     *     and 'unseen' values.
     * </ul>
     * @param array $opts     Additional options:
     * <pre>
     *   - sort: (boolean) If true, sort the list of mailboxes? (since 2.10.0)
     *           DEFAULT: Do not sort the list.
     *   - sort_delimiter: (string) If 'sort' is true, this is the delimiter
     *                     used to sort the mailboxes. (since 2.10.0)
     *                     DEFAULT: '.'
     * </pre>
     *
     * @return array  If $mailbox contains multiple mailboxes, an array with
     *                keys being the UTF-8 mailbox name and values as arrays
     *                containing the requested keys (see above).
     *                Otherwise, an array with keys as the requested keys (see
     *                above) and values as the key data.
     *
     * @throws Horde_Imap_Client_Exception
     */
    public function status($mailbox, $flags = Horde_Imap_Client::STATUS_ALL,
                           array $opts = array())
    {
        $opts = array_merge(array(
            'sort' => false,
            'sort_delimiter' => '.'
        ), $opts);

        $this->login();

        if (is_array($mailbox)) {
            if (empty($mailbox)) {
                return array();
            }
            $ret_array = true;
        } else {
            $mailbox = array($mailbox);
            $ret_array = false;
        }

        $mlist = array_map(array('Horde_Imap_Client_Mailbox', 'get'), $mailbox);

        $unselected_flags = array(
            'messages' => Horde_Imap_Client::STATUS_MESSAGES,
            'recent' => Horde_Imap_Client::STATUS_RECENT,
            'uidnext' => Horde_Imap_Client::STATUS_UIDNEXT,
            'uidvalidity' => Horde_Imap_Client::STATUS_UIDVALIDITY,
            'unseen' => Horde_Imap_Client::STATUS_UNSEEN
        );

        if (!$this->statuscache) {
            $flags |= Horde_Imap_Client::STATUS_FORCE_REFRESH;
        }

        if ($flags & Horde_Imap_Client::STATUS_ALL) {
            foreach ($unselected_flags as $val) {
                $flags |= $val;
            }
        }

        $master = $ret = array();

        /* Catch flags that are not supported. */
        if (($flags & Horde_Imap_Client::STATUS_HIGHESTMODSEQ) &&
            !$this->_capability()->isEnabled('CONDSTORE')) {
            $master['highestmodseq'] = 0;
            $flags &= ~Horde_Imap_Client::STATUS_HIGHESTMODSEQ;
        }

        if (($flags & Horde_Imap_Client::STATUS_UIDNOTSTICKY) &&
            !$this->_capability('UIDPLUS')) {
            $master['uidnotsticky'] = false;
            $flags &= ~Horde_Imap_Client::STATUS_UIDNOTSTICKY;
        }

        /* UIDNEXT return options. */
        if ($flags & Horde_Imap_Client::STATUS_UIDNEXT_FORCE) {
            $flags |= Horde_Imap_Client::STATUS_UIDNEXT;
        }

        foreach ($mlist as $val) {
            $name = strval($val);
            $tmp_flags = $flags;

            if ($val->equals($this->_selected)) {
                /* Check if already in mailbox. */
                $opened = true;

                if ($flags & Horde_Imap_Client::STATUS_FORCE_REFRESH) {
                    $this->noop();
                }
            } else {
                /* A list of STATUS options (other than those handled directly
                 * below) that require the mailbox to be explicitly opened. */
                $opened = ($flags & Horde_Imap_Client::STATUS_FIRSTUNSEEN) ||
                    ($flags & Horde_Imap_Client::STATUS_FLAGS) ||
                    ($flags & Horde_Imap_Client::STATUS_PERMFLAGS) ||
                    ($flags & Horde_Imap_Client::STATUS_UIDNOTSTICKY) ||
                    /* Force mailboxes containing wildcards to be accessed via
                     * STATUS so that wildcards do not return a bunch of
                     * mailboxes in the LIST-STATUS response. */
                    (strpbrk($name, '*%') !== false);
            }

            $ret[$name] = $master;
            $ptr = &$ret[$name];

            /* STATUS_PERMFLAGS requires a read/write mailbox. */
            if ($flags & Horde_Imap_Client::STATUS_PERMFLAGS) {
                $this->openMailbox($val, Horde_Imap_Client::OPEN_READWRITE);
                $opened = true;
            }

            /* Handle SYNC related return options. These require the mailbox
             * to be opened at least once. */
            if ($flags & Horde_Imap_Client::STATUS_SYNCMODSEQ) {
                $this->openMailbox($val);
                $ptr['syncmodseq'] = $this->_mailboxOb($val)->getStatus(Horde_Imap_Client::STATUS_SYNCMODSEQ);
                $tmp_flags &= ~Horde_Imap_Client::STATUS_SYNCMODSEQ;
                $opened = true;
            }

            if ($flags & Horde_Imap_Client::STATUS_SYNCFLAGUIDS) {
                $this->openMailbox($val);
                $ptr['syncflaguids'] = $this->getIdsOb($this->_mailboxOb($val)->getStatus(Horde_Imap_Client::STATUS_SYNCFLAGUIDS));
                $tmp_flags &= ~Horde_Imap_Client::STATUS_SYNCFLAGUIDS;
                $opened = true;
            }

            if ($flags & Horde_Imap_Client::STATUS_SYNCVANISHED) {
                $this->openMailbox($val);
                $ptr['syncvanished'] = $this->getIdsOb($this->_mailboxOb($val)->getStatus(Horde_Imap_Client::STATUS_SYNCVANISHED));
                $tmp_flags &= ~Horde_Imap_Client::STATUS_SYNCVANISHED;
                $opened = true;
            }

            /* Handle RECENT_TOTAL option. */
            if ($flags & Horde_Imap_Client::STATUS_RECENT_TOTAL) {
                $this->openMailbox($val);
                $ptr['recent_total'] = $this->_mailboxOb($val)->getStatus(Horde_Imap_Client::STATUS_RECENT_TOTAL);
                $tmp_flags &= ~Horde_Imap_Client::STATUS_RECENT_TOTAL;
                $opened = true;
            }

            if ($opened) {
                if ($tmp_flags) {
                    $tmp = $this->_status(array($val), $tmp_flags);
                    $ptr += reset($tmp);
                }
            } else {
                $to_process[] = $val;
            }
        }

        if ($flags && !empty($to_process)) {
            if ((count($to_process) > 1) &&
                $this->_capability('LIST-STATUS')) {
                foreach ($this->listMailboxes($to_process, Horde_Imap_Client::MBOX_ALL, array('status' => $flags)) as $key => $val) {
                    if (isset($val['status'])) {
                        $ret[$key] += $val['status'];
                    }
                }
            } else {
                foreach ($this->_status($to_process, $flags) as $key => $val) {
                    $ret[$key] += $val;
                }
            }
        }

        if (!$opts['sort'] || (count($ret) === 1)) {
            return $ret_array
                ? $ret
                : reset($ret);
        }

        $list_ob = new Horde_Imap_Client_Mailbox_List(array_keys($ret));
        $sorted = $list_ob->sort(array(
            'delimiter' => $opts['sort_delimiter']
        ));

        $out = array();
        foreach ($sorted as $val) {
            $out[$val] = $ret[$val];
        }

        return $out;
    }

    /**
     * Obtain status information for mailboxes.
     *
     * @param array $mboxes   The list of mailbox objects to query.
     * @param integer $flags  A bitmask of information requested from the
     *                        server.
     *
     * @return array  See array return for status().
     *
     * @throws Horde_Imap_Client_Exception
     */
    abstract protected function _status($mboxes, $flags);

    /**
     * Perform a STATUS call on multiple mailboxes at the same time.
     *
     * This method leverages the LIST-EXTENDED and LIST-STATUS extensions on
     * the IMAP server to improve the efficiency of this operation.
     *
     * @deprecated  Use status() instead.
     *
     * @param array $mailboxes  The mailboxes to query. Either
     *                          Horde_Imap_Client_Mailbox objects, strings
     *                          (UTF-8), or a combination of the two.
     * @param integer $flags    See status().
     * @param array $opts       Additional options:
     *   - sort: (boolean) If true, sort the 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: '.'
     *
     * @return array  An array with the keys as the mailbox names (UTF-8) and
     *                the values as arrays with the requested keys (from the
     *                mask given in $flags).
     */
    public function statusMultiple($mailboxes,
                                   $flags = Horde_Imap_Client::STATUS_ALL,
                                   array $opts = array())
    {
        return $this->status($mailboxes, $flags, $opts);
    }

    /**
     * Append message(s) to a mailbox.
     *
     * @param mixed $mailbox  The mailbox to append the message(s) to. Either
     *                        a Horde_Imap_Client_Mailbox object or a string
     *                        (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. If a string or a stream resource,
     *           this will be used as the entire contents of a single message.
     *           If an array, will catenate all given parts into a single
     *           message. This array contains one or more arrays with
     *           two keys:
     *     - t: (string) Either 'url' or 'text'.
     *     - v: (mixed) If 't' is 'url', this is the IMAP URL to the message
     *          part to append. If 't' is 'text', this is either a string or
     *          resource representation of the message part data.
     *     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) The internaldate to set for the appended
     *                   message.
     *                   DEFAULT: internaldate will be the same date as when
     *                   the message was appended.
     * </pre>
     * @param array $options  Additonal options:
     * <pre>
     *   - create: (boolean) Try to create $mailbox if it does not exist?
     *             DEFAULT: No.
     * </pre>
     *
     * @return Horde_Imap_Client_Ids  The UIDs of the appended messages.
     *
     * @throws Horde_Imap_Client_Exception
     */
    public function append($mailbox, $data, array $options = array())
    {
        $this->login();

        $mailbox = Horde_Imap_Client_Mailbox::get($mailbox);

        $ret = $this->_append($mailbox, $data, $options);

        if ($ret instanceof Horde_Imap_Client_Ids) {
            return $ret;
        }

        $uids = $this->getIdsOb();

        foreach ($data as $val) {
            if (is_resource($val['data'])) {
                rewind($val['data']);
            }

            $uids->add($this->_getUidByMessageId(
                $mailbox,
                Horde_Mime_Headers::parseHeaders($val['data'])->getHeader('Message-ID')
            ));
        }

        return $uids;
    }

    /**
     * Append message(s) to a mailbox.
     *
     * @param Horde_Imap_Client_Mailbox $mailbox  The mailbox to append the
     *                                            message(s) to.
     * @param array $data                         The message data.
     * @param array $options                      Additional options.
     *
     * @return mixed  A Horde_Imap_Client_Ids object containing the UIDs of
     *                the appended messages (if server supports UIDPLUS
     *                extension) or true.
     *
     * @throws Horde_Imap_Client_Exception
     */
    abstract protected function _append(Horde_Imap_Client_Mailbox $mailbox,
                                        $data, $options);

    /**
     * Request a checkpoint of the currently selected mailbox (RFC 3501
     * [6.4.1]).
     *
     * @throws Horde_Imap_Client_Exception
     */
    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 Horde_Imap_Client_Exception
     */
    abstract protected function _check();

    /**
     * Close the connection to the currently selected mailbox, optionally
     * expunging all deleted messages (RFC 3501 [6.4.2]).
     *
     * @param array $options  Additional options:
     *   - expunge: (boolean) Expunge all messages flagged as deleted?
     *              DEFAULT: No
     *
     * @throws Horde_Imap_Client_Exception
     */
    public function close(array $options = array())
    {
        // This check catches the non-logged in case.
        if (is_null($this->_selected)) {
            return;
        }

        /* If we are caching, search for deleted messages. */
        if (!empty($options['expunge']) && $this->_initCache(true)) {
            /* Make sure mailbox is read-write to expunge. */
            $this->openMailbox($this->_selected, Horde_Imap_Client::OPEN_READWRITE);
            if ($this->_mode == Horde_Imap_Client::OPEN_READONLY) {
                throw new Horde_Imap_Client_Exception(
                    Horde_Imap_Client_Translation::r("Cannot expunge read-only mailbox."),
                    Horde_Imap_Client_Exception::MAILBOX_READONLY
                );
            }

            $search_query = new Horde_Imap_Client_Search_Query();
            $search_query->flag(Horde_Imap_Client::FLAG_DELETED, true);
            $search_res = $this->search($this->_selected, $search_query);
            $mbox = $this->_selected;
        } else {
            $search_res = null;
        }

        $this->_close($options);
        $this->_selected = null;
        $this->_mode = 0;

        if (!is_null($search_res)) {
            $this->_deleteMsgs($mbox, $search_res['match']);
        }
    }

    /**
     * Close the connection to the currently selected mailbox, optionally
     * expunging all deleted messages (RFC 3501 [6.4.2]).
     *
     * @param array $options  Additional options.
     *
     * @throws Horde_Imap_Client_Exception
     */
    abstract protected function _close($options);

    /**
     * Expunge deleted messages from the given mailbox.
     *
     * @param mixed $mailbox  The mailbox to expunge. Either a
     *                        Horde_Imap_Client_Mailbox object or a string
     *                        (UTF-8).
     * @param array $options  Additional options:
     *   - delete: (boolean) If true, will flag all messages in 'ids' as
     *             deleted (since 2.10.0).
     *             DEFAULT: false
     *   - ids: (Horde_Imap_Client_Ids) A list of messages to expunge. These
     *          messages must already be flagged as deleted (unless 'delete'
     *          is true).
     *          DEFAULT: All messages marked as deleted will be expunged.
     *   - list: (boolean) If true, returns the list of expunged messages
     *           (UIDs only).
     *           DEFAULT: false
     *
     * @return Horde_Imap_Client_Ids  If 'list' option is true, returns the
     *                                UID list of expunged messages.
     *
     * @throws Horde_Imap_Client_Exception
     */
    public function expunge($mailbox, array $options = array())
    {
        // Open mailbox call will handle the login.
        $this->openMailbox($mailbox, Horde_Imap_Client::OPEN_READWRITE);

        /* Don't expunge if the mailbox is readonly. */
        if ($this->_mode == Horde_Imap_Client::OPEN_READONLY) {
            throw new Horde_Imap_Client_Exception(
                Horde_Imap_Client_Translation::r("Cannot expunge read-only mailbox."),
                Horde_Imap_Client_Exception::MAILBOX_READONLY
            );
        }

        if (empty($options['ids'])) {
            $options['ids'] = $this->getIdsOb(Horde_Imap_Client_Ids::ALL);
        } elseif ($options['ids']->isEmpty()) {
            return $this->getIdsOb();
        }

        return $this->_expunge($options);
    }

    /**
     * Expunge all deleted messages from the given mailbox.
     *
     * @param array $options  Additional options.
     *
     * @return Horde_Imap_Client_Ids  If 'list' option is true, returns the
     *                                list of expunged messages.
     *
     * @throws Horde_Imap_Client_Exception
     */
    abstract protected function _expunge($options);

    /**
     * Search a mailbox.
     *
     * @param mixed $mailbox                         The mailbox to search.
     *                                               Either a
     *                                               Horde_Imap_Client_Mailbox
     *                                               object or a string
     *                                               (UTF-8).
     * @param Horde_Imap_Client_Search_Query $query  The search query.
     *                                               Defaults to an ALL
     *                                               search.
     * @param array $options                         Additional options:
     * <pre>
     *   - nocache: (boolean) Don't cache the results.
     *              DEFAULT: false (results cached, if possible)
     *   - partial: (mixed) The range of results to return (message sequence
     *              numbers) Only a single range is supported (represented by
     *              the minimum and maximum values contained in the range
     *              given).
     *              DEFAULT: All messages are returned.
     *   - results: (array) The data to return. Consists of zero or more of
     *              the following flags:
     *     - Horde_Imap_Client::SEARCH_RESULTS_COUNT
     *     - Horde_Imap_Client::SEARCH_RESULTS_MATCH (DEFAULT)
     *     - Horde_Imap_Client::SEARCH_RESULTS_MAX
     *     - Horde_Imap_Client::SEARCH_RESULTS_MIN
     *     - Horde_Imap_Client::SEARCH_RESULTS_SAVE
     *     - Horde_Imap_Client::SEARCH_RESULTS_RELEVANCY
     *   - 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. 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. The following sort
     *           criteria are available:
     *     - Horde_Imap_Client::SORT_ARRIVAL
     *     - Horde_Imap_Client::SORT_CC
     *     - Horde_Imap_Client::SORT_DATE
     *     - Horde_Imap_Client::SORT_DISPLAYFROM
     *       On servers that don't support SORT=DISPLAY, this criteria will
     *       fallback to doing client-side sorting.
     *     - Horde_Imap_Client::SORT_DISPLAYFROM_FALLBACK
     *       On servers that don't support SORT=DISPLAY, this criteria will
     *       fallback to Horde_Imap_Client::SORT_FROM [since 2.4.0].
     *     - Horde_Imap_Client::SORT_DISPLAYTO
     *       On servers that don't support SORT=DISPLAY, this criteria will
     *       fallback to doing client-side sorting.
     *     - Horde_Imap_Client::SORT_DISPLAYTO_FALLBACK
     *       On servers that don't support SORT=DISPLAY, this criteria will
     *       fallback to Horde_Imap_Client::SORT_TO [since 2.4.0].
     *     - Horde_Imap_Client::SORT_FROM
     *     - Horde_Imap_Client::SORT_SEQUENCE
     *     - Horde_Imap_Client::SORT_SIZE
     *     - Horde_Imap_Client::SORT_SUBJECT
     *     - Horde_Imap_Client::SORT_TO
     *
     *     [On servers that support SEARCH=FUZZY, this criteria is also
     *     available:]
     *     - Horde_Imap_Client::SORT_RELEVANCY
     * </pre>
     *
     * @return array  An array with the following keys:
     * <pre>
     *   - count: (integer) The number of messages that match the search
     *            criteria. Always returned.
     *   - match: (Horde_Imap_Client_Ids) The IDs that match $criteria, sorted
     *            if the 'sort' modifier was set. Returned if
     *            Horde_Imap_Client::SEARCH_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::SEARCH_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::SEARCH_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.
     *   - relevancy: (array) The list of relevancy scores. Returned if
     *                Horde_Imap_Client::SEARCH_RESULTS_RELEVANCY is set and
     *                the server supports FUZZY search matching.
     *   - save: (boolean) Whether the search results were saved. Returned if
     *           Horde_Imap_Client::SEARCH_RESULTS_SAVE is set.
     * </pre>
     *
     * @throws Horde_Imap_Client_Exception
     */
    public function search($mailbox, $query = null, array $options = array())
    {
        $this->login();

        if (empty($options['results'])) {
            $options['results'] = array(
                Horde_Imap_Client::SEARCH_RESULTS_MATCH,
                Horde_Imap_Client::SEARCH_RESULTS_COUNT
            );
        } elseif (!in_array(Horde_Imap_Client::SEARCH_RESULTS_COUNT, $options['results'])) {
            $options['results'][] = Horde_Imap_Client::SEARCH_RESULTS_COUNT;
        }

        // Default to an ALL search.
        if (is_null($query)) {
            $query = new Horde_Imap_Client_Search_Query();
        }

        // Check for SEARCHRES support.
        if ((($pos = array_search(Horde_Imap_Client::SEARCH_RESULTS_SAVE, $options['results'])) !== false) &&
            !$this->_capability('SEARCHRES')) {
            unset($options['results'][$pos]);
        }

        // Check for SORT-related options.
        if (!empty($options['sort'])) {
            foreach ($options['sort'] as $key => $val) {
                switch ($val) {
                case Horde_Imap_Client::SORT_DISPLAYFROM_FALLBACK:
                    $options['sort'][$key] = $this->_capability('SORT', 'DISPLAY')
                        ? Horde_Imap_Client::SORT_DISPLAYFROM
                        : Horde_Imap_Client::SORT_FROM;
                    break;

                case Horde_Imap_Client::SORT_DISPLAYTO_FALLBACK:
                    $options['sort'][$key] = $this->_capability('SORT', 'DISPLAY')
                        ? Horde_Imap_Client::SORT_DISPLAYTO
                        : Horde_Imap_Client::SORT_TO;
                    break;
                }
            }
        }

        /* Default search results. */
        $default_ret = array(
            'count' => 0,
            'match' => $this->getIdsOb(),
            'max' => null,
            'min' => null,
            'relevancy' => array()
        );

        /* Build search query. */
        $squery = $query->build($this);

        /* Check for query contents. If empty, this means that the query
         * object has identified that this query can NEVER return any results.
         * Immediately return now. */
        if (!count($squery['query'])) {
            return $default_ret;
        }

        // Check for supported charset.
        if (!is_null($squery['charset']) &&
            ($this->search_charset->query($squery['charset'], true) === false)) {
            foreach ($this->search_charset->charsets as $val) {
                try {
                    $new_query = clone $query;
                    $new_query->charset($val);
                    break;
                } catch (Horde_Imap_Client_Exception_SearchCharset $e) {
                    unset($new_query);
                }
            }

            if (!isset($new_query)) {
                throw $e;
            }

            $query = $new_query;
            $squery = $query->build($this);
        }

        // Store query in $options array to pass to child method.
        $options['_query'] = $squery;

        /* RFC 6203: MUST NOT request relevancy results if we are not using
         * FUZZY searching. */
        if (in_array(Horde_Imap_Client::SEARCH_RESULTS_RELEVANCY, $options['results']) &&
            !in_array('SEARCH=FUZZY', $squery['exts_used'])) {
            throw new InvalidArgumentException('Cannot specify RELEVANCY results if not doing a FUZZY search.');
        }

        /* Check for partial matching. */
        if (!empty($options['partial'])) {
            $pids = $this->getIdsOb($options['partial'], true)->range_string;
            if (!strlen($pids)) {
                throw new InvalidArgumentException('Cannot specify empty sequence range for a PARTIAL search.');
            }

            if (strpos($pids, ':') === false) {
                $pids .= ':' . $pids;
            }

            $options['partial'] = $pids;
        }

        /* 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']) == Horde_Imap_Client::SEARCH_RESULTS_COUNT)) {
            switch ($squery['query']) {
            case 'ALL':
                $ret = $this->status($mailbox, Horde_Imap_Client::STATUS_MESSAGES);
                return array('count' => $ret['messages']);

            case 'RECENT':
                $ret = $this->status($mailbox, Horde_Imap_Client::STATUS_RECENT);
                return array('count' => $ret['recent']);
            }
        }

        $this->openMailbox($mailbox, Horde_Imap_Client::OPEN_AUTO);

        /* Take advantage of search result caching.  If CONDSTORE available,
         * we can cache all queries and invalidate the cache when the MODSEQ
         * changes. If CONDSTORE not available, we can only store queries
         * that don't involve flags. We store results by hashing the options
         * array. */
        $cache = null;
        if (empty($options['nocache']) &&
            $this->_initCache(true) &&
            ($this->_capability()->isEnabled('CONDSTORE') ||
             !$query->flagSearch())) {
            $cache = $this->_getSearchCache('search', $options);
            if (isset($cache['data'])) {
                if (isset($cache['data']['match'])) {
                    $cache['data']['match'] = $this->getIdsOb($cache['data']['match']);
                }
                return $cache['data'];
            }
        }

        /* Optimization: Catch when there are no messages in a mailbox. */
        $status_res = $this->status($this->_selected, Horde_Imap_Client::STATUS_MESSAGES | Horde_Imap_Client::STATUS_HIGHESTMODSEQ);
        if ($status_res['messages'] ||
            in_array(Horde_Imap_Client::SEARCH_RESULTS_SAVE, $options['results'])) {
            /* RFC 7162 [3.1.2.2] - trying to do a MODSEQ SEARCH on a mailbox
             * that doesn't support it will return BAD. */
            if (in_array('CONDSTORE', $squery['exts']) &&
                !$this->_mailboxOb()->getStatus(Horde_Imap_Client::STATUS_HIGHESTMODSEQ)) {
                throw new Horde_Imap_Client_Exception(
                    Horde_Imap_Client_Translation::r("Mailbox does not support mod-sequences."),
                    Horde_Imap_Client_Exception::MBOXNOMODSEQ
                );
            }

            $ret = $this->_search($query, $options);
        } else {
            $ret = $default_ret;
            if (isset($status_res['highestmodseq'])) {
                $ret['modseq'] = $status_res['highestmodseq'];
            }
        }

        if ($cache) {
            $save = $ret;
            if (isset($save['match'])) {
                $save['match'] = strval($ret['match']);
            }
            $this->_setSearchCache($save, $cache);
        }

        return $ret;
    }

    /**
     * Search a mailbox.
     *
     * @param object $query   The search query.
     * @param array $options  Additional options. The '_query' key contains
     *                        the value of $query->build().
     *
     * @return Horde_Imap_Client_Ids  An array of IDs.
     *
     * @throws Horde_Imap_Client_Exception
     */
    abstract protected function _search($query, $options);

    /**
     * Set the comparator to use for searching/sorting (RFC 5255).
     *
     * @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.
     *
     * @throws Horde_Imap_Client_Exception
     * @throws Horde_Imap_Client_Exception_NoSupportExtension
     */
    public function setComparator($comparator = null)
    {
        $comp = is_null($comparator)
            ? $this->getParam('comparator')
            : $comparator;
        if (is_null($comp)) {
            return;
        }

        $this->login();

        if (!$this->_capability('I18NLEVEL', '2')) {
            throw new Horde_Imap_Client_Exception_NoSupportExtension(
                'I18NLEVEL',
                'The IMAP server does not support changing SEARCH/SORT comparators.'
            );
        }

        $this->_setComparator($comp);
    }

    /**
     * Set the comparator to use for searching/sorting (RFC 5255).
     *
     * @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.
     *
     * @throws Horde_Imap_Client_Exception
     */
    abstract protected function _setComparator($comparator);

    /**
     * Get the comparator used for searching/sorting (RFC 5255).
     *
     * @return mixed  Null if the default comparator is being used, or an
     *                array of comparator information (see RFC 5255 [4.8]).
     *
     * @throws Horde_Imap_Client_Exception
     */
    public function getComparator()
    {
        $this->login();

        return $this->_capability('I18NLEVEL', '2')
            ? $this->_getComparator()
            : null;
    }

    /**
     * Get the comparator used for searching/sorting (RFC 5255).
     *
     * @return mixed  Null if the default comparator is being used, or an
     *                array of comparator information (see RFC 5255 [4.8]).
     *
     * @throws Horde_Imap_Client_Exception
     */
    abstract protected function _getComparator();

    /**
     * Thread sort a given list of messages (RFC 5256).
     *
     * @param mixed $mailbox  The mailbox to query. Either a
     *                        Horde_Imap_Client_Mailbox object or a string
     *                        (UTF-8).
     * @param array $options  Additional options:
     * <pre>
     *   - criteria: (mixed) The following thread criteria are available:
     *     - Horde_Imap_Client::THREAD_ORDEREDSUBJECT
     *     - Horde_Imap_Client::THREAD_REFERENCES
     *     - Horde_Imap_Client::THREAD_REFS
     *       Other algorithms can be explicitly specified by passing the IMAP
     *       thread algorithm in as a string value.
     *     DEFAULT: Horde_Imap_Client::THREAD_ORDEREDSUBJECT
     *   - search: (Horde_Imap_Client_Search_Query) The search query.
     *             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_Data_Thread  A thread data object.
     *
     * @throws Horde_Imap_Client_Exception
     */
    public function thread($mailbox, array $options = array())
    {
        // Open mailbox call will handle the login.
        $this->openMailbox($mailbox, Horde_Imap_Client::OPEN_AUTO);

        /* Take advantage of search result caching.  If CONDSTORE available,
         * we can cache all queries and invalidate the cache when the MODSEQ
         * changes. If CONDSTORE not available, we can only store queries
         * that don't involve flags. See search() for similar caching. */
        $cache = null;
        if ($this->_initCache(true) &&
            ($this->_capability()->isEnabled('CONDSTORE') ||
             empty($options['search']) ||
             !$options['search']->flagSearch())) {
            $cache = $this->_getSearchCache('thread', $options);
            if (isset($cache['data']) &&
                ($cache['data'] instanceof Horde_Imap_Client_Data_Thread)) {
                return $cache['data'];
            }
        }

        $status_res = $this->status($this->_selected, Horde_Imap_Client::STATUS_MESSAGES);

        $ob = $status_res['messages']
            ? $this->_thread($options)
            : new Horde_Imap_Client_Data_Thread(array(), empty($options['sequence']) ? 'uid' : 'sequence');

        if ($cache) {
            $this->_setSearchCache($ob, $cache);
        }

        return $ob;
    }

    /**
     * Thread sort a given list of messages (RFC 5256).
     *
     * @param array $options  Additional options. See thread().
     *
     * @return Horde_Imap_Client_Data_Thread  A thread data object.
     *
     * @throws Horde_Imap_Client_Exception
     */
    abstract protected function _thread($options);

    /**
     * Fetch message data (see RFC 3501 [6.4.5]).
     *
     * @param mixed $mailbox                        The mailbox to search.
     *                                              Either a
     *                                              Horde_Imap_Client_Mailbox
     *                                              object or a string (UTF-8).
     * @param Horde_Imap_Client_Fetch_Query $query  Fetch query object.
     * @param array $options                        Additional options:
     *   - 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
     *                   mod-sequence fetch criteria to the fetch command.
     *                   DEFAULT: Mod-sequence values are ignored.
     *   - exists: (boolean) Ensure that all ids returned exist on the server.
     *             If false, the list of ids returned in the results object
     *             is not guaranteed to reflect the current state of the
     *             remote mailbox.
     *             DEFAULT: false
     *   - ids: (Horde_Imap_Client_Ids) A list of messages to fetch data from.
     *          DEFAULT: All messages in $mailbox will be fetched.
     *   - nocache: (boolean) If true, will not cache the results (previously
     *              cached data will still be used to generate results) [since
     *              2.8.0].
     *              DEFAULT: false
     *
     * @return Horde_Imap_Client_Fetch_Results  A results object.
     *
     * @throws Horde_Imap_Client_Exception
     * @throws Horde_Imap_Client_Exception_NoSupportExtension
     */
    public function fetch($mailbox, $query, array $options = array())
    {
        try {
            $ret = $this->_fetchWrapper($mailbox, $query, $options);
            unset($this->_temp['fetch_nocache']);
            return $ret;
        } catch (Exception $e) {
            unset($this->_temp['fetch_nocache']);
            throw $e;
        }
    }

    /**
     * Wrapper for fetch() to allow internal state to be reset on exception.
     *
     * @internal
     * @see fetch()
     */
    private function _fetchWrapper($mailbox, $query, $options)
    {
        $this->login();

        $query = clone $query;

        $cache_array = $header_cache = $new_query = array();

        if (empty($options['ids'])) {
            $options['ids'] = $this->getIdsOb(Horde_Imap_Client_Ids::ALL);
        } elseif ($options['ids']->isEmpty()) {
            return new Horde_Imap_Client_Fetch_Results($this->_fetchDataClass);
        } elseif ($options['ids']->search_res &&
                  !$this->_capability('SEARCHRES')) {
            /* SEARCHRES requires server support. */
            throw new Horde_Imap_Client_Exception_NoSupportExtension('SEARCHRES');
        }

        $this->openMailbox($mailbox, Horde_Imap_Client::OPEN_AUTO);
        $mbox_ob = $this->_mailboxOb();

        if (!empty($options['nocache'])) {
            $this->_temp['fetch_nocache'] = true;
        }

        $cf = $this->_initCache(true)
            ? $this->_cacheFields()
            : array();

        if (!empty($cf)) {
            /* If using cache, we store by UID so we need to return UIDs. */
            $query->uid();
        }

        $modseq_check = !empty($options['changedsince']);
        if ($query->contains(Horde_Imap_Client::FETCH_MODSEQ)) {
            if (!$this->_capability()->isEnabled('CONDSTORE')) {
                unset($query[Horde_Imap_Client::FETCH_MODSEQ]);
            } elseif (empty($options['changedsince'])) {
                $modseq_check = true;
            }
        }

        if ($modseq_check &&
            !$mbox_ob->getStatus(Horde_Imap_Client::STATUS_HIGHESTMODSEQ)) {
            /* RFC 7162 [3.1.2.2] - trying to do a MODSEQ FETCH on a mailbox
             * that doesn't support it will return BAD. */
            throw new Horde_Imap_Client_Exception(
                Horde_Imap_Client_Translation::r("Mailbox does not support mod-sequences."),
                Horde_Imap_Client_Exception::MBOXNOMODSEQ
            );
        }

        /* Determine if caching is available and if anything in $query is
         * cacheable. */
        foreach ($cf as $k => $v) {
            if (isset($query[$k])) {
                switch ($k) {
                case Horde_Imap_Client::FETCH_ENVELOPE:
                case Horde_Imap_Client::FETCH_FLAGS:
                case Horde_Imap_Client::FETCH_IMAPDATE:
                case Horde_Imap_Client::FETCH_SIZE:
                case Horde_Imap_Client::FETCH_STRUCTURE:
                    $cache_array[$k] = $v;
                    break;

                case Horde_Imap_Client::FETCH_HEADERS:
                    $this->_temp['headers_caching'] = array();

                    foreach ($query[$k] as $key => $val) {
                        /* Only cache if directly requested.  Iterate through
                         * requests to ensure at least one can be cached. */
                        if (!empty($val['cache']) && !empty($val['peek'])) {
                            $cache_array[$k] = $v;
                            ksort($val);
                            $header_cache[$key] = hash('md5', serialize($val));
                        }
                    }
                    break;
                }
            }
        }

        $ret = new Horde_Imap_Client_Fetch_Results(
            $this->_fetchDataClass,
            $options['ids']->sequence ? Horde_Imap_Client_Fetch_Results::SEQUENCE : Horde_Imap_Client_Fetch_Results::UID
        );

        /* If nothing is cacheable, we can do a straight search. */
        if (empty($cache_array)) {
            $options['_query'] = $query;
            $this->_fetch($ret, array($options));
            return $ret;
        }

        $cs_ret = empty($options['changedsince'])
            ? null
            : clone $ret;

        /* Convert special searches to UID lists and create mapping. */
        $ids = $this->resolveIds(
            $this->_selected,
            $options['ids'],
            empty($options['exists']) ? 1 : 2
        );

        /* Add non-user settable cache fields. */
        $cache_array[Horde_Imap_Client::FETCH_DOWNGRADED] = self::CACHE_DOWNGRADED;

        /* Get the cached values. */
        $data = $this->_cache->get(
            $this->_selected,
            $ids->ids,
            array_values($cache_array),
            $mbox_ob->getStatus(Horde_Imap_Client::STATUS_UIDVALIDITY)
        );

        /* Build a list of what we still need. */
        $map = array_flip($mbox_ob->map->map);
        $sequence = $options['ids']->sequence;
        foreach ($ids as $uid) {
            $crit = clone $query;

            if ($sequence) {
                if (!isset($map[$uid])) {
                    continue;
                }
                $entry_idx = $map[$uid];
            } else {
                $entry_idx = $uid;
                unset($crit[Horde_Imap_Client::FETCH_UID]);
            }

            $entry = $ret->get($entry_idx);

            if (isset($map[$uid])) {
                $entry->setSeq($map[$uid]);
                unset($crit[Horde_Imap_Client::FETCH_SEQ]);
            }

            $entry->setUid($uid);

            foreach ($cache_array as $key => $cid) {
                switch ($key) {
                case Horde_Imap_Client::FETCH_DOWNGRADED:
                    if (!empty($data[$uid][$cid])) {
                        $entry->setDowngraded(true);
                    }
                    break;

                case Horde_Imap_Client::FETCH_ENVELOPE:
                    if (isset($data[$uid][$cid]) &&
                        ($data[$uid][$cid] instanceof Horde_Imap_Client_Data_Envelope)) {
                        $entry->setEnvelope($data[$uid][$cid]);
                        unset($crit[$key]);
                    }
                    break;

                case Horde_Imap_Client::FETCH_FLAGS:
                    if (isset($data[$uid][$cid]) &&
                        is_array($data[$uid][$cid])) {
                        $entry->setFlags($data[$uid][$cid]);
                        unset($crit[$key]);
                    }
                    break;

                case Horde_Imap_Client::FETCH_HEADERS:
                    foreach ($header_cache as $hkey => $hval) {
                        if (isset($data[$uid][$cid][$hval])) {
                            /* We have found a cached entry with the same
                             * MD5 sum. */
                            $entry->setHeaders($hkey, $data[$uid][$cid][$hval]);
                            $crit->remove($key, $hkey);
                        } else {
                            $this->_temp['headers_caching'][$hkey] = $hval;
                        }
                    }
                    break;

                case Horde_Imap_Client::FETCH_IMAPDATE:
                    if (isset($data[$uid][$cid]) &&
                        ($data[$uid][$cid] instanceof Horde_Imap_Client_DateTime)) {
                        $entry->setImapDate($data[$uid][$cid]);
                        unset($crit[$key]);
                    }
                    break;

                case Horde_Imap_Client::FETCH_SIZE:
                    if (isset($data[$uid][$cid])) {
                        $entry->setSize($data[$uid][$cid]);
                        unset($crit[$key]);
                    }
                    break;

                case Horde_Imap_Client::FETCH_STRUCTURE:
                    if (isset($data[$uid][$cid]) &&
                        ($data[$uid][$cid] instanceof Horde_Mime_Part)) {
                        $entry->setStructure($data[$uid][$cid]);
                        unset($crit[$key]);
                    }
                    break;
                }
            }

            if (count($crit)) {
                $sig = $crit->hash();
                if (isset($new_query[$sig])) {
                    $new_query[$sig]['i'][] = $entry_idx;
                } else {
                    $new_query[$sig] = array(
                        'c' => $crit,
                        'i' => array($entry_idx)
                    );
                }
            }
        }

        $to_fetch = array();
        foreach ($new_query as $val) {
            $ids_ob = $this->getIdsOb(null, $sequence);
            $ids_ob->duplicates = true;
            $ids_ob->add($val['i']);
            $to_fetch[] = array_merge($options, array(
                '_query' => $val['c'],
                'ids' => $ids_ob
            ));
        }

        if (!empty($to_fetch)) {
            $this->_fetch(is_null($cs_ret) ? $ret : $cs_ret, $to_fetch);
        }

        if (is_null($cs_ret)) {
            return $ret;
        }

        /* If doing changedsince query, and all other data is cached, we still
         * need to hit IMAP server to determine proper results set. */
        if (empty($new_query)) {
            $squery = new Horde_Imap_Client_Search_Query();
            $squery->modseq($options['changedsince'] + 1);
            $squery->ids($options['ids']);

            $cs = $this->search($this->_selected, $squery, array(
                'sequence' => $sequence
            ));

            foreach ($cs['match'] as $val) {
                $entry = $ret->get($val);
                if ($sequence) {
                    $entry->setSeq($val);
                } else {
                    $entry->setUid($val);
                }
                $cs_ret[$val] = $entry;
            }
        } else {
            foreach ($cs_ret as $key => $val) {
                $val->merge($ret->get($key));
            }
        }

        return $cs_ret;
    }

    /**
     * Fetch message data.
     *
     * Fetch queries should be grouped in the $queries argument. Each value
     * is an array of fetch options, with the fetch query stored in the
     * '_query' parameter. IMPORTANT: All queries must have the same ID
     * type (either sequence or UID).
     *
     * @param Horde_Imap_Client_Fetch_Results $results  Fetch results.
     * @param array $queries                            The list of queries.
     *
     * @throws Horde_Imap_Client_Exception
     */
    abstract protected function _fetch(Horde_Imap_Client_Fetch_Results $results,
                                       $queries);

    /**
     * Get the list of vanished messages (UIDs that have been expunged since a
     * given mod-sequence value).
     *
     * @param mixed $mailbox   The mailbox to query. Either a
     *                         Horde_Imap_Client_Mailbox object or a string
     *                         (UTF-8).
     * @param integer $modseq  Search for expunged messages after this
     *                         mod-sequence value.
     * @param array $opts      Additional options:
     *   - ids: (Horde_Imap_Client_Ids)  Restrict to these UIDs.
     *          DEFAULT: Returns full list of UIDs vanished (QRESYNC only).
     *                   This option is REQUIRED for non-QRESYNC servers or
     *                   else an empty list will be returned.
     *
     * @return Horde_Imap_Client_Ids  List of UIDs that have vanished.
     *
     * @throws Horde_Imap_Client_NoSupportExtension
     */
    public function vanished($mailbox, $modseq, array $opts = array())
    {
        $this->login();

        if (empty($opts['ids'])) {
            if (!$this->_capability()->isEnabled('QRESYNC')) {
                return $this->getIdsOb();
            }
            $opts['ids'] = $this->getIdsOb(Horde_Imap_Client_Ids::ALL);
        } elseif ($opts['ids']->isEmpty()) {
            return $this->getIdsOb();
        } elseif ($opts['ids']->sequence) {
            throw new InvalidArgumentException('Vanished requires UIDs.');
        }

        $this->openMailbox($mailbox, Horde_Imap_Client::OPEN_AUTO);

        if ($this->_capability()->isEnabled('QRESYNC')) {
            if (!$this->_mailboxOb()->getStatus(Horde_Imap_Client::STATUS_HIGHESTMODSEQ)) {
                throw new Horde_Imap_Client_Exception(
                    Horde_Imap_Client_Translation::r("Mailbox does not support mod-sequences."),
                    Horde_Imap_Client_Exception::MBOXNOMODSEQ
                );
            }

            return $this->_vanished(max(1, $modseq), $opts['ids']);
        }

        $ids = $this->resolveIds($mailbox, $opts['ids']);

        $squery = new Horde_Imap_Client_Search_Query();
        $squery->ids($ids);
        $search = $this->search($mailbox, $squery, array(
            'nocache' => true
        ));

        return $this->getIdsOb(array_diff($ids->ids, $search['match']->ids));
    }

    /**
     * Get the list of vanished messages.
     *
     * @param integer $modseq             Mod-sequence value.
     * @param Horde_Imap_Client_Ids $ids  UIDs.
     *
     * @return Horde_Imap_Client_Ids  List of UIDs that have vanished.
     */
    abstract protected function _vanished($modseq, Horde_Imap_Client_Ids $ids);

    /**
     * Store message flag data (see RFC 3501 [6.4.6]).
     *
     * @param mixed $mailbox  The mailbox containing the messages to modify.
     *                        Either a Horde_Imap_Client_Mailbox object or a
     *                        string (UTF-8).
     * @param array $options  Additional options:
     *   - add: (array) An array of flags to add.
     *          DEFAULT: No flags added.
     *   - ids: (Horde_Imap_Client_Ids) 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.
     *   - 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
     *
     * @return Horde_Imap_Client_Ids  A Horde_Imap_Client_Ids object
     *                                containing the list of IDs that failed
     *                                the 'unchangedsince' test.
     *
     * @throws Horde_Imap_Client_Exception
     * @throws Horde_Imap_Client_Exception_NoSupportExtension
     */
    public function store($mailbox, array $options = array())
    {
        // Open mailbox call will handle the login.
        $this->openMailbox($mailbox, Horde_Imap_Client::OPEN_READWRITE);

        /* SEARCHRES requires server support. */
        if (empty($options['ids'])) {
            $options['ids'] = $this->getIdsOb(Horde_Imap_Client_Ids::ALL);
        } elseif ($options['ids']->isEmpty()) {
            return $this->getIdsOb();
        } elseif ($options['ids']->search_res &&
                  !$this->_capability('SEARCHRES')) {
            throw new Horde_Imap_Client_Exception_NoSupportExtension('SEARCHRES');
        }

        if (!empty($options['unchangedsince'])) {
            if (!$this->_capability()->isEnabled('CONDSTORE')) {
                throw new Horde_Imap_Client_Exception_NoSupportExtension('CONDSTORE');
            }

            /* RFC 7162 [3.1.2.2] - trying to do a UNCHANGEDSINCE STORE on a
             * mailbox that doesn't support it will return BAD. */
            if (!$this->_mailboxOb()->getStatus(Horde_Imap_Client::STATUS_HIGHESTMODSEQ)) {
                throw new Horde_Imap_Client_Exception(
                    Horde_Imap_Client_Translation::r("Mailbox does not support mod-sequences."),
                    Horde_Imap_Client_Exception::MBOXNOMODSEQ
                );
            }
        }

        return $this->_store($options);
    }

    /**
     * Store message flag data.
     *
     * @param array $options  Additional options.
     *
     * @return Horde_Imap_Client_Ids  A Horde_Imap_Client_Ids object
     *                                containing the list of IDs that failed
     *                                the 'unchangedsince' test.
     *
     * @throws Horde_Imap_Client_Exception
     */
    abstract protected function _store($options);

    /**
     * Copy messages to another mailbox.
     *
     * @param mixed $source   The source mailbox. Either a
     *                        Horde_Imap_Client_Mailbox object or a string
     *                        (UTF-8).
     * @param mixed $dest     The destination mailbox. Either a
     *                        Horde_Imap_Client_Mailbox object or a string
     *                        (UTF-8).
     * @param array $options  Additional options:
     *   - create: (boolean) Try to create $dest if it does not exist?
     *             DEFAULT: No.
     *   - force_map: (boolean) Forces the array mapping to always be
     *                returned. [@since 2.19.0]
     *   - ids: (Horde_Imap_Client_Ids) 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.
     *
     * @return mixed  An array mapping old UIDs (keys) to new UIDs (values) on
     *                success (only guaranteed if 'force_map' is true) or
     *                true.
     *
     * @throws Horde_Imap_Client_Exception
     * @throws Horde_Imap_Client_Exception_NoSupportExtension
     */
    public function copy($source, $dest, array $options = array())
    {
        // Open mailbox call will handle the login.
        $this->openMailbox($source, empty($options['move']) ? Horde_Imap_Client::OPEN_AUTO : Horde_Imap_Client::OPEN_READWRITE);

        /* SEARCHRES requires server support. */
        if (empty($options['ids'])) {
            $options['ids'] = $this->getIdsOb(Horde_Imap_Client_Ids::ALL);
        } elseif ($options['ids']->isEmpty()) {
            return array();
        } elseif ($options['ids']->search_res &&
                  !$this->_capability('SEARCHRES')) {
            throw new Horde_Imap_Client_Exception_NoSupportExtension('SEARCHRES');
        }

        $dest = Horde_Imap_Client_Mailbox::get($dest);
        $res = $this->_copy($dest, $options);

        if (($res === true) && !empty($options['force_map'])) {
            /* Need to manually create mapping from Message-ID data. */
            $query = new Horde_Imap_Client_Fetch_Query();
            $query->envelope();
            $fetch = $this->fetch($source, $query, array(
                'ids' => $options['ids']
            ));

            $res = array();
            foreach ($fetch as $val) {
                if ($uid = $this->_getUidByMessageId($dest, $val->getEnvelope()->message_id)) {
                    $res[$val->getUid()] = $uid;
                }
            }
        }

        return $res;
    }

    /**
     * Copy messages to another mailbox.
     *
     * @param Horde_Imap_Client_Mailbox $dest  The destination mailbox.
     * @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.
     *
     * @throws Horde_Imap_Client_Exception
     */
    abstract protected function _copy(Horde_Imap_Client_Mailbox $dest,
                                      $options);

    /**
     * Set quota limits. The server must support the IMAP QUOTA extension
     * (RFC 2087).
     *
     * @param mixed $root       The quota root. Either a
     *                          Horde_Imap_Client_Mailbox object or a string
     *                          (UTF-8).
     * @param array $resources  The resource values to set. Keys are the
     *                          resource atom name; value is the resource
     *                          value.
     *
     * @throws Horde_Imap_Client_Exception
     * @throws Horde_Imap_Client_Exception_NoSupportExtension
     */
    public function setQuota($root, array $resources = array())
    {
        $this->login();

        if (!$this->_capability('QUOTA')) {
            throw new Horde_Imap_Client_Exception_NoSupportExtension('QUOTA');
        }

        if (!empty($resources)) {
            $this->_setQuota(Horde_Imap_Client_Mailbox::get($root), $resources);
        }
    }

    /**
     * Set quota limits.
     *
     * @param Horde_Imap_Client_Mailbox $root  The quota root.
     * @param array $resources                 The resource values to set.
     *
     * @return boolean  True on success.
     *
     * @throws Horde_Imap_Client_Exception
     */
    abstract protected function _setQuota(Horde_Imap_Client_Mailbox $root,
                                          $resources);

    /**
     * Get quota limits. The server must support the IMAP QUOTA extension
     * (RFC 2087).
     *
     * @param mixed $root  The quota root. Either a Horde_Imap_Client_Mailbox
     *                     object or a string (UTF-8).
     *
     * @return mixed  An array with resource keys. Each key holds an array
     *                with 2 values: 'limit' and 'usage'.
     *
     * @throws Horde_Imap_Client_Exception
     * @throws Horde_Imap_Client_Exception_NoSupportExtension
     */
    public function getQuota($root)
    {
        $this->login();

        if (!$this->_capability('QUOTA')) {
            throw new Horde_Imap_Client_Exception_NoSupportExtension('QUOTA');
        }

        return $this->_getQuota(Horde_Imap_Client_Mailbox::get($root));
    }

    /**
     * Get quota limits.
     *
     * @param Horde_Imap_Client_Mailbox $root  The quota root.
     *
     * @return mixed  An array with resource keys. Each key holds an array
     *                with 2 values: 'limit' and 'usage'.
     *
     * @throws Horde_Imap_Client_Exception
     */
    abstract protected function _getQuota(Horde_Imap_Client_Mailbox $root);

    /**
     * Get quota limits for a mailbox. The server must support the IMAP QUOTA
     * extension (RFC 2087).
     *
     * @param mixed $mailbox  A mailbox. Either a Horde_Imap_Client_Mailbox
     *                        object or a string (UTF-8).
     *
     * @return mixed  An array with the keys being the quota roots. Each key
     *                holds an array with resource keys: each of these keys
     *                holds an array with 2 values: 'limit' and 'usage'.
     *
     * @throws Horde_Imap_Client_Exception
     * @throws Horde_Imap_Client_Exception_NoSupportExtension
     */
    public function getQuotaRoot($mailbox)
    {
        $this->login();

        if (!$this->_capability('QUOTA')) {
            throw new Horde_Imap_Client_Exception_NoSupportExtension('QUOTA');
        }

        return $this->_getQuotaRoot(Horde_Imap_Client_Mailbox::get($mailbox));
    }

    /**
     * Get quota limits for a mailbox.
     *
     * @param Horde_Imap_Client_Mailbox $mailbox  A mailbox.
     *
     * @return mixed  An array with the keys being the quota roots. Each key
     *                holds an array with resource keys: each of these keys
     *                holds an array with 2 values: 'limit' and 'usage'.
     *
     * @throws Horde_Imap_Client_Exception
     */
    abstract protected function _getQuotaRoot(Horde_Imap_Client_Mailbox $mailbox);

    /**
     * Get the ACL rights for a given mailbox. The server must support the
     * IMAP ACL extension (RFC 2086/4314).
     *
     * @param mixed $mailbox  A mailbox. Either a Horde_Imap_Client_Mailbox
     *                        object or a string (UTF-8).
     *
     * @return array  An array with identifiers as the keys and
     *                Horde_Imap_Client_Data_Acl objects as the values.
     *
     * @throws Horde_Imap_Client_Exception
     */
    public function getACL($mailbox)
    {
        $this->login();
        return $this->_getACL(Horde_Imap_Client_Mailbox::get($mailbox));
    }

    /**
     * Get ACL rights for a given mailbox.
     *
     * @param Horde_Imap_Client_Mailbox $mailbox  A mailbox.
     *
     * @return array  An array with identifiers as the keys and
     *                Horde_Imap_Client_Data_Acl objects as the values.
     *
     * @throws Horde_Imap_Client_Exception
     */
    abstract protected function _getACL(Horde_Imap_Client_Mailbox $mailbox);

    /**
     * Set ACL rights for a given mailbox/identifier.
     *
     * @param mixed $mailbox      A mailbox. Either a Horde_Imap_Client_Mailbox
     *                            object or a string (UTF-8).
     * @param string $identifier  The identifier to alter (UTF-8).
     * @param array $options      Additional options:
     *   - rights: (string) The rights to alter or set.
     *   - action: (string, optional) If 'add' or 'remove', adds or removes the
     *             specified rights. Sets the rights otherwise.
     *
     * @throws Horde_Imap_Client_Exception
     * @throws Horde_Imap_Client_Exception_NoSupportExtension
     */
    public function setACL($mailbox, $identifier, $options)
    {
        $this->login();

        if (!$this->_capability('ACL')) {
            throw new Horde_Imap_Client_Exception_NoSupportExtension('ACL');
        }

        if (empty($options['rights'])) {
            if (!isset($options['action']) ||
                (($options['action'] != 'add') &&
                 $options['action'] != 'remove')) {
                $this->_deleteACL(
                    Horde_Imap_Client_Mailbox::get($mailbox),
                    Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($identifier)
                );
            }
            return;
        }

        $acl = ($options['rights'] instanceof Horde_Imap_Client_Data_Acl)
            ? $options['rights']
            : new Horde_Imap_Client_Data_Acl(strval($options['rights']));

        $options['rights'] = $acl->getString(
            $this->_capability('RIGHTS')
                ? Horde_Imap_Client_Data_AclCommon::RFC_4314
                : Horde_Imap_Client_Data_AclCommon::RFC_2086
        );
        if (isset($options['action'])) {
            switch ($options['action']) {
            case 'add':
                $options['rights'] = '+' . $options['rights'];
                break;
            case 'remove':
                $options['rights'] = '-' . $options['rights'];
                break;
            }
        }

        $this->_setACL(
            Horde_Imap_Client_Mailbox::get($mailbox),
            Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($identifier),
            $options
        );
    }

    /**
     * Set ACL rights for a given mailbox/identifier.
     *
     * @param Horde_Imap_Client_Mailbox $mailbox  A mailbox.
     * @param string $identifier                  The identifier to alter
     *                                            (UTF7-IMAP).
     * @param array $options                      Additional options. 'rights'
     *                                            contains the string of
     *                                            rights to set on the server.
     *
     * @throws Horde_Imap_Client_Exception
     */
    abstract protected function _setACL(Horde_Imap_Client_Mailbox $mailbox,
                                        $identifier, $options);

    /**
     * Deletes ACL rights for a given mailbox/identifier.
     *
     * @param mixed $mailbox      A mailbox. Either a Horde_Imap_Client_Mailbox
     *                            object or a string (UTF-8).
     * @param string $identifier  The identifier to delete (UTF-8).
     *
     * @throws Horde_Imap_Client_Exception
     * @throws Horde_Imap_Client_Exception_NoSupportExtension
     */
    public function deleteACL($mailbox, $identifier)
    {
        $this->login();

        if (!$this->_capability('ACL')) {
            throw new Horde_Imap_Client_Exception_NoSupportExtension('ACL');
        }

        $this->_deleteACL(
            Horde_Imap_Client_Mailbox::get($mailbox),
            Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($identifier)
        );
    }

    /**
     * Deletes ACL rights for a given mailbox/identifier.
     *
     * @param Horde_Imap_Client_Mailbox $mailbox  A mailbox.
     * @param string $identifier                  The identifier to delete
     *                                            (UTF7-IMAP).
     *
     * @throws Horde_Imap_Client_Exception
     */
    abstract protected function _deleteACL(Horde_Imap_Client_Mailbox $mailbox,
                                           $identifier);

    /**
     * List the ACL rights for a given mailbox/identifier. The server must
     * support the IMAP ACL extension (RFC 2086/4314).
     *
     * @param mixed $mailbox      A mailbox. Either a Horde_Imap_Client_Mailbox
     *                            object or a string (UTF-8).
     * @param string $identifier  The identifier to query (UTF-8).
     *
     * @return Horde_Imap_Client_Data_AclRights  An ACL data rights object.
     *
     * @throws Horde_Imap_Client_Exception
     * @throws Horde_Imap_Client_Exception_NoSupportExtension
     */
    public function listACLRights($mailbox, $identifier)
    {
        $this->login();

        if (!$this->_capability('ACL')) {
            throw new Horde_Imap_Client_Exception_NoSupportExtension('ACL');
        }

        return $this->_listACLRights(
            Horde_Imap_Client_Mailbox::get($mailbox),
            Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($identifier)
        );
    }

    /**
     * Get ACL rights for a given mailbox/identifier.
     *
     * @param Horde_Imap_Client_Mailbox $mailbox  A mailbox.
     * @param string $identifier                  The identifier to query
     *                                            (UTF7-IMAP).
     *
     * @return Horde_Imap_Client_Data_AclRights  An ACL data rights object.
     *
     * @throws Horde_Imap_Client_Exception
     */
    abstract protected function _listACLRights(Horde_Imap_Client_Mailbox $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).
     *
     * @param mixed $mailbox  A mailbox. Either a Horde_Imap_Client_Mailbox
     *                        object or a string (UTF-8).
     *
     * @return Horde_Imap_Client_Data_Acl  An ACL data object.
     *
     * @throws Horde_Imap_Client_Exception
     * @throws Horde_Imap_Client_Exception_NoSupportExtension
     */
    public function getMyACLRights($mailbox)
    {
        $this->login();

        if (!$this->_capability('ACL')) {
            throw new Horde_Imap_Client_Exception_NoSupportExtension('ACL');
        }

        return $this->_getMyACLRights(Horde_Imap_Client_Mailbox::get($mailbox));
    }

    /**
     * Get the ACL rights for the current user for a given mailbox.
     *
     * @param Horde_Imap_Client_Mailbox $mailbox  A mailbox.
     *
     * @return Horde_Imap_Client_Data_Acl  An ACL data object.
     *
     * @throws Horde_Imap_Client_Exception
     */
    abstract protected function _getMyACLRights(Horde_Imap_Client_Mailbox $mailbox);

    /**
     * Return master list of ACL rights available on the server.
     *
     * @return array  A list of ACL rights.
     */
    public function allAclRights()
    {
        $this->login();

        $rights = array(
            Horde_Imap_Client::ACL_LOOKUP,
            Horde_Imap_Client::ACL_READ,
            Horde_Imap_Client::ACL_SEEN,
            Horde_Imap_Client::ACL_WRITE,
            Horde_Imap_Client::ACL_INSERT,
            Horde_Imap_Client::ACL_POST,
            Horde_Imap_Client::ACL_ADMINISTER
        );

        if ($capability = $this->_capability()->getParams('RIGHTS')) {
            // Add rights defined in CAPABILITY string (RFC 4314).
            return array_merge($rights, str_split(reset($capability)));
        }

        // Add RFC 2086 rights (deprecated by RFC 4314, but need to keep for
        // compatibility with old servers).
        return array_merge($rights, array(
            Horde_Imap_Client::ACL_CREATE,
            Horde_Imap_Client::ACL_DELETE
        ));
    }

    /**
     * Get metadata for a given mailbox. The server must support either the
     * IMAP METADATA extension (RFC 5464) or the ANNOTATEMORE extension
     * (http://ietfreport.isoc.org/idref/draft-daboo-imap-annotatemore/).
     *
     * @param mixed $mailbox  A mailbox. Either a Horde_Imap_Client_Mailbox
     *                        object or a string (UTF-8).
     * @param array $entries  The entries to fetch (UTF-8 strings).
     * @param array $options  Additional options:
     *   - depth: (string) Either "0", "1" or "infinity". Returns only the
     *            given value (0), only values one level below the specified
     *            value (1) or all entries below the specified value
     *            (infinity).
     *   - maxsize: (integer) The maximal size the returned values may have.
     *              DEFAULT: No maximal size.
     *
     * @return array  An array with metadata names as the keys and metadata
     *                values as the values. If 'maxsize' is set, and entries
     *                exist on the server larger than this size, the size will
     *                be returned in the key '*longentries'.
     *
     * @throws Horde_Imap_Client_Exception
     */
    public function getMetadata($mailbox, $entries, array $options = array())
    {
        $this->login();

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

        return $this->_getMetadata(Horde_Imap_Client_Mailbox::get($mailbox), array_map(array('Horde_Imap_Client_Utf7imap', 'Utf8ToUtf7Imap'), $entries), $options);
    }

    /**
     * Get metadata for a given mailbox.
     *
     * @param Horde_Imap_Client_Mailbox $mailbox  A mailbox.
     * @param array $entries                      The entries to fetch
     *                                            (UTF7-IMAP strings).
     * @param array $options                      Additional options.
     *
     * @return array  An array with metadata names as the keys and metadata
     *                values as the values.
     *
     * @throws Horde_Imap_Client_Exception
     */
    abstract protected function _getMetadata(Horde_Imap_Client_Mailbox $mailbox,
                                             $entries, $options);

    /**
     * Set metadata for a given mailbox/identifier.
     *
     * @param mixed $mailbox  A mailbox. Either a Horde_Imap_Client_Mailbox
     *                        object or a string (UTF-8). If empty, sets a
     *                        server annotation.
     * @param array $data     A set of data values. The metadata values
     *                        corresponding to the keys of the array will
     *                        be set to the values in the array.
     *
     * @throws Horde_Imap_Client_Exception
     */
    public function setMetadata($mailbox, $data)
    {
        $this->login();
        $this->_setMetadata(Horde_Imap_Client_Mailbox::get($mailbox), $data);
    }

    /**
     * Set metadata for a given mailbox/identifier.
     *
     * @param Horde_Imap_Client_Mailbox $mailbox  A mailbox.
     * @param array $data                         A set of data values. See
     *                                            setMetadata() for format.
     *
     * @throws Horde_Imap_Client_Exception
     */
    abstract protected function _setMetadata(Horde_Imap_Client_Mailbox $mailbox,
                                             $data);

    /* Public utility functions. */

    /**
     * Returns a unique identifier for the current mailbox status.
     *
     * @deprecated
     *
     * @param mixed $mailbox  A mailbox. Either a Horde_Imap_Client_Mailbox
     *                        object or a string (UTF-8).
     * @param array $addl     Additional cache info to add to the cache ID
     *                        string.
     *
     * @return string  The cache ID string, which will change when the
     *                 composition of the mailbox changes. The uidvalidity
     *                 will always be the first element, and will be delimited
     *                 by the '|' character.
     *
     * @throws Horde_Imap_Client_Exception
     */
    public function getCacheId($mailbox, array $addl = array())
    {
        return Horde_Imap_Client_Base_Deprecated::getCacheId($this, $mailbox, $this->_capability()->isEnabled('CONDSTORE'), $addl);
    }

    /**
     * Parses a cacheID created by getCacheId().
     *
     * @deprecated
     *
     * @param string $id  The cache ID.
     *
     * @return array  An array with the following information:
     *   - highestmodseq: (integer)
     *   - messages: (integer)
     *   - uidnext: (integer)
     *   - uidvalidity: (integer) Always present
     */
    public function parseCacheId($id)
    {
        return Horde_Imap_Client_Base_Deprecated::parseCacheId($id);
    }

    /**
     * Resolves an IDs object into a list of IDs.
     *
     * @param Horde_Imap_Client_Mailbox $mailbox  The mailbox.
     * @param Horde_Imap_Client_Ids $ids          The Ids object.
     * @param integer $convert                    Convert to UIDs?
     *   - 0: No
     *   - 1: Only if $ids is not already a UIDs object
     *   - 2: Always
     *
     * @return Horde_Imap_Client_Ids  The list of IDs.
     */
    public function resolveIds(Horde_Imap_Client_Mailbox $mailbox,
                               Horde_Imap_Client_Ids $ids, $convert = 0)
    {
        $map = $this->_mailboxOb($mailbox)->map;

        if ($ids->special) {
            /* Optimization for ALL sequence searches. */
            if (!$convert && $ids->all && $ids->sequence) {
                $res = $this->status($mailbox, Horde_Imap_Client::STATUS_MESSAGES);
                return $this->getIdsOb($res['messages'] ? ('1:' . $res['messages']) : array(), true);
            }

            $convert = 2;
        } elseif (!$convert ||
                  (!$ids->sequence && ($convert == 1)) ||
                  $ids->isEmpty()) {
            return clone $ids;
        } else {
            /* Do an all or nothing: either we have all the numbers/UIDs in
             * memory and can return, or just send the whole ID query to the
             * server. Any advantage we would get by a partial search are
             * outweighed by the complexities needed to make the search and
             * then merge back into the original results. */
            $lookup = $map->lookup($ids);
            if (count($lookup) === count($ids)) {
                return $this->getIdsOb(array_values($lookup));
            }
        }

        $query = new Horde_Imap_Client_Search_Query();
        $query->ids($ids);

        $res = $this->search($mailbox, $query, array(
            'results' => array(
                Horde_Imap_Client::SEARCH_RESULTS_MATCH,
                Horde_Imap_Client::SEARCH_RESULTS_SAVE
            ),
            'sequence' => (!$convert && $ids->sequence),
            'sort' => array(Horde_Imap_Client::SORT_SEQUENCE)
        ));

        /* Update mapping. */
        if ($convert) {
            if ($ids->all) {
                $ids = $this->getIdsOb('1:' . count($res['match']));
            } elseif ($ids->special) {
                return $res['match'];
            }

            /* Sanity checking (Bug #12911). */
            $list1 = array_slice($ids->ids, 0, count($res['match']));
            $list2 = $res['match']->ids;
            if (!empty($list1) &&
                !empty($list2) &&
                (count($list1) === count($list2))) {
                $map->update(array_combine($list1, $list2));
            }
        }

        return $res['match'];
    }

    /**
     * Determines if the given charset is valid for search-related queries.
     * This check pertains just to the basic IMAP SEARCH command.
     *
     * @deprecated Use $search_charset property instead.
     *
     * @param string $charset  The query charset.
     *
     * @return boolean  True if server supports this charset.
     */
    public function validSearchCharset($charset)
    {
        return $this->search_charset->query($charset);
    }

    /* Mailbox syncing functions. */

    /**
     * Returns a unique token for the current mailbox synchronization status.
     *
     * @since 2.2.0
     *
     * @param mixed $mailbox  A mailbox. Either a Horde_Imap_Client_Mailbox
     *                        object or a string (UTF-8).
     *
     * @return string  The sync token.
     *
     * @throws Horde_Imap_Client_Exception
     */
    public function getSyncToken($mailbox)
    {
        $out = array();

        foreach ($this->_syncStatus($mailbox) as $key => $val) {
            $out[] = $key . $val;
        }

        return base64_encode(implode(',', $out));
    }

    /**
     * Synchronize a mailbox from a sync token.
     *
     * @since 2.2.0
     *
     * @param mixed $mailbox  A mailbox. Either a Horde_Imap_Client_Mailbox
     *                        object or a string (UTF-8).
     * @param string $token   A sync token generated by getSyncToken().
     * @param array $opts     Additional options:
     *   - criteria: (integer) Mask of Horde_Imap_Client::SYNC_* criteria to
     *               return. Defaults to SYNC_ALL.
     *   - ids: (Horde_Imap_Client_Ids) A cached list of UIDs. Unless QRESYNC
     *          is available on the server, failure to specify this option
     *          means SYNC_VANISHEDUIDS information cannot be returned.
     *
     * @return Horde_Imap_Client_Data_Sync  A sync object.
     *
     * @throws Horde_Imap_Client_Exception
     * @throws Horde_Imap_Client_Exception_Sync
     */
    public function sync($mailbox, $token, array $opts = array())
    {
        if (($token = base64_decode($token, true)) === false) {
            throw new Horde_Imap_Client_Exception_Sync('Bad token.', Horde_Imap_Client_Exception_Sync::BAD_TOKEN);
        }

        $sync = array();
        foreach (explode(',', $token) as $val) {
            $sync[substr($val, 0, 1)] = substr($val, 1);
        }

        return new Horde_Imap_Client_Data_Sync(
            $this,
            $mailbox,
            $sync,
            $this->_syncStatus($mailbox),
            (isset($opts['criteria']) ? $opts['criteria'] : Horde_Imap_Client::SYNC_ALL),
            (isset($opts['ids']) ? $opts['ids'] : null)
        );
    }

    /* Private utility functions. */

    /**
     * Store FETCH data in cache.
     *
     * @param Horde_Imap_Client_Fetch_Results $data  The fetch results.
     *
     * @throws Horde_Imap_Client_Exception
     */
    protected function _updateCache(Horde_Imap_Client_Fetch_Results $data)
    {
        if (!empty($this->_temp['fetch_nocache']) ||
            empty($this->_selected) ||
            !count($data) ||
            !$this->_initCache(true)) {
            return;
        }

        $c = $this->getParam('cache');
        if (in_array(strval($this->_selected), $c['fetch_ignore'])) {
            $this->_debug->info(sprintf(
                'CACHE: Ignoring FETCH data [%s]',
                $this->_selected
            ));
            return;
        }

        /* Optimization: we can directly use getStatus() here since we know
         * these values are initialized. */
        $mbox_ob = $this->_mailboxOb();
        $highestmodseq = $mbox_ob->getStatus(Horde_Imap_Client::STATUS_HIGHESTMODSEQ);
        $uidvalidity = $mbox_ob->getStatus(Horde_Imap_Client::STATUS_UIDVALIDITY);

        $mapping = $modseq = $tocache = array();
        if (count($data)) {
            $cf = $this->_cacheFields();
        }

        foreach ($data as $v) {
            /* It is possible that we received FETCH information that doesn't
             * contain UID data. This is uncacheable so don't process. */
            if (!($uid = $v->getUid())) {
                return;
            }

            $tmp = array();

            if ($v->isDowngraded()) {
                $tmp[self::CACHE_DOWNGRADED] = true;
            }

            foreach ($cf as $key => $val) {
                if ($v->exists($key)) {
                    switch ($key) {
                    case Horde_Imap_Client::FETCH_ENVELOPE:
                        $tmp[$val] = $v->getEnvelope();
                        break;

                    case Horde_Imap_Client::FETCH_FLAGS:
                        if ($highestmodseq) {
                            $modseq[$uid] = $v->getModSeq();
                            $tmp[$val] = $v->getFlags();
                        }
                        break;

                    case Horde_Imap_Client::FETCH_HEADERS:
                        foreach ($this->_temp['headers_caching'] as $label => $hash) {
                            if ($hdr = $v->getHeaders($label)) {
                                $tmp[$val][$hash] = $hdr;
                            }
                        }
                        break;

                    case Horde_Imap_Client::FETCH_IMAPDATE:
                        $tmp[$val] = $v->getImapDate();
                        break;

                    case Horde_Imap_Client::FETCH_SIZE:
                        $tmp[$val] = $v->getSize();
                        break;

                    case Horde_Imap_Client::FETCH_STRUCTURE:
                        $tmp[$val] = clone $v->getStructure();
                        break;
                    }
                }
            }

            if (!empty($tmp)) {
                $tocache[$uid] = $tmp;
            }

            $mapping[$v->getSeq()] = $uid;
        }

        if (!empty($mapping)) {
            if (!empty($tocache)) {
                $this->_cache->set($this->_selected, $tocache, $uidvalidity);
            }

            $this->_mailboxOb()->map->update($mapping);
        }

        if (!empty($modseq)) {
            $this->_updateModSeq(max(array_merge($modseq, array($highestmodseq))));
            $mbox_ob->setStatus(Horde_Imap_Client::STATUS_SYNCFLAGUIDS, array_keys($modseq));
        }
    }

    /**
     * Moves cache entries from the current mailbox to another mailbox.
     *
     * @param Horde_Imap_Client_Mailbox $to  The destination mailbox.
     * @param array $map                     Mapping of source UIDs (keys) to
     *                                       destination UIDs (values).
     * @param string $uidvalid               UIDVALIDITY of destination
     *                                       mailbox.
     *
     * @throws Horde_Imap_Client_Exception
     */
    protected function _moveCache(Horde_Imap_Client_Mailbox $to, $map,
                                  $uidvalid)
    {
        if (!$this->_initCache()) {
            return;
        }

        $c = $this->getParam('cache');
        if (in_array(strval($to), $c['fetch_ignore'])) {
            $this->_debug->info(sprintf(
                'CACHE: Ignoring moving FETCH data (%s => %s)',
                $this->_selected,
                $to
            ));
            return;
        }

        $old = $this->_cache->get($this->_selected, array_keys($map), null);
        $new = array();

        foreach ($map as $key => $val) {
            if (!empty($old[$key])) {
                $new[$val] = $old[$key];
            }
        }

        if (!empty($new)) {
            $this->_cache->set($to, $new, $uidvalid);
        }
    }

    /**
     * Delete messages in the cache.
     *
     * @param Horde_Imap_Client_Mailbox $mailbox  The mailbox.
     * @param Horde_Imap_Client_Ids $ids          The list of IDs to delete in
     *                                            $mailbox.
     * @param array $opts                         Additional options (not used
     *                                            in base class).
     *
     * @return Horde_Imap_Client_Ids  UIDs that were deleted.
     * @throws Horde_Imap_Client_Exception
     */
    protected function _deleteMsgs(Horde_Imap_Client_Mailbox $mailbox,
                                   Horde_Imap_Client_Ids $ids,
                                   array $opts = array())
    {
        if (!$this->_initCache()) {
            return $ids;
        }

        $mbox_ob = $this->_mailboxOb();
        $ids_ob = $ids->sequence
            ? $this->getIdsOb($mbox_ob->map->lookup($ids))
            : $ids;

        $this->_cache->deleteMsgs($mailbox, $ids_ob->ids);
        $mbox_ob->setStatus(Horde_Imap_Client::STATUS_SYNCVANISHED, $ids_ob->ids);
        $mbox_ob->map->remove($ids);

        return $ids_ob;
    }

    /**
     * Retrieve data from the search cache.
     *
     * @param string $type    The cache type ('search' or 'thread').
     * @param array $options  The options array of the calling function.
     *
     * @return mixed  Returns search cache metadata. If search was retrieved,
     *                data is in key 'data'.
     *                Returns null if caching is not available.
     */
    protected function _getSearchCache($type, $options)
    {
        $status = $this->status($this->_selected, Horde_Imap_Client::STATUS_HIGHESTMODSEQ | Horde_Imap_Client::STATUS_UIDVALIDITY);

        /* Search caching requires MODSEQ, which may not be active for a
         * mailbox. */
        if (empty($status['highestmodseq'])) {
            return null;
        }

        ksort($options);
        $cache = hash('md5', $type . serialize($options));
        $cacheid = $this->getSyncToken($this->_selected);
        $ret = array();

        $md = $this->_cache->getMetaData(
            $this->_selected,
            $status['uidvalidity'],
            array(self::CACHE_SEARCH, self::CACHE_SEARCHID)
        );

        if (!isset($md[self::CACHE_SEARCHID]) ||
            ($md[self::CACHE_SEARCHID] != $cacheid)) {
            $md[self::CACHE_SEARCH] = array();
            $md[self::CACHE_SEARCHID] = $cacheid;
            if ($this->_debug->debug &&
                !isset($this->_temp['searchcacheexpire'][strval($this->_selected)])) {
                $this->_debug->info(sprintf(
                    'SEARCH: Expired from cache [%s]',
                    $this->_selected
                ));
                $this->_temp['searchcacheexpire'][strval($this->_selected)] = true;
            }
        } elseif (isset($md[self::CACHE_SEARCH][$cache])) {
            $this->_debug->info(sprintf(
                'SEARCH: Retrieved %s from cache (%s [%s])',
                $type,
                $cache,
                $this->_selected
            ));
            $ret['data'] = $md[self::CACHE_SEARCH][$cache];
            unset($md[self::CACHE_SEARCHID]);
        }

        return array_merge($ret, array(
            'id' => $cache,
            'metadata' => $md,
            'type' => $type
        ));
    }

    /**
     * Set data in the search cache.
     *
     * @param mixed $data    The cache data to store.
     * @param string $sdata  The search data returned from _getSearchCache().
     */
    protected function _setSearchCache($data, $sdata)
    {
        $sdata['metadata'][self::CACHE_SEARCH][$sdata['id']] = $data;

        $this->_cache->setMetaData($this->_selected, null, $sdata['metadata']);

        if ($this->_debug->debug) {
            $this->_debug->info(sprintf(
                'SEARCH: Saved %s to cache (%s [%s])',
                $sdata['type'],
                $sdata['id'],
                $this->_selected
            ));
            unset($this->_temp['searchcacheexpire'][strval($this->_selected)]);
        }
    }

    /**
     * Updates the cached MODSEQ value.
     *
     * @param integer $modseq  MODSEQ value to store.
     *
     * @return mixed  The MODSEQ of the old value if it was replaced (or false
     *                if it didn't exist or is the same).
     */
    protected function _updateModSeq($modseq)
    {
        if (!$this->_initCache(true)) {
            return false;
        }

        $mbox_ob = $this->_mailboxOb();
        $uidvalid = $mbox_ob->getStatus(Horde_Imap_Client::STATUS_UIDVALIDITY);
        $md = $this->_cache->getMetaData($this->_selected, $uidvalid, array(self::CACHE_MODSEQ));

        if (isset($md[self::CACHE_MODSEQ])) {
            if ($md[self::CACHE_MODSEQ] < $modseq) {
                $set = true;
                $sync = $md[self::CACHE_MODSEQ];
            } else {
                $set = false;
                $sync = 0;
            }
            $mbox_ob->setStatus(Horde_Imap_Client::STATUS_SYNCMODSEQ, $md[self::CACHE_MODSEQ]);
        } else {
            $set = true;
            $sync = 0;
        }

        /* $modseq can be 0 - NOMODSEQ - so don't store in that case. */
        if ($set && $modseq) {
            $this->_cache->setMetaData($this->_selected, $uidvalid, array(
                self::CACHE_MODSEQ => $modseq
            ));
        }

        return $sync;
    }

    /**
     * Synchronizes the current mailbox cache with the server (using CONDSTORE
     * or QRESYNC).
     */
    protected function _condstoreSync()
    {
        $mbox_ob = $this->_mailboxOb();

        /* Check that modseqs are available in mailbox. */
        if (!($highestmodseq = $mbox_ob->getStatus(Horde_Imap_Client::STATUS_HIGHESTMODSEQ)) ||
            !($modseq = $this->_updateModSeq($highestmodseq))) {
            $mbox_ob->sync = true;
        }

        if ($mbox_ob->sync) {
            return;
        }

        $uids_ob = $this->getIdsOb($this->_cache->get(
            $this->_selected,
            array(),
            array(),
            $mbox_ob->getStatus(Horde_Imap_Client::STATUS_UIDVALIDITY)
        ));

        if (!count($uids_ob)) {
            $mbox_ob->sync = true;
            return;
        }

        /* Are we caching flags? */
        if (array_key_exists(Horde_Imap_Client::FETCH_FLAGS, $this->_cacheFields())) {
            $fquery = new Horde_Imap_Client_Fetch_Query();
            $fquery->flags();

            /* Update flags in cache. Cache will be updated in _fetch(). */
            $this->_fetch(new Horde_Imap_Client_Fetch_Results(), array(
                array(
                    '_query' => $fquery,
                    'changedsince' => $modseq,
                    'ids' => $uids_ob
                )
            ));
        }

        /* Search for deleted messages, and remove from cache. */
        $vanished = $this->vanished($this->_selected, $modseq, array(
            'ids' => $uids_ob
        ));
        if (!empty($vanished->ids)) {
            $this->_deleteMsgs($this->_selected, $this->getIdsOb($vanished->ids));
        }

        $mbox_ob->sync = true;
    }

    /**
     * Provide the list of available caching fields.
     *
     * @return array  The list of available caching fields (fields are in the
     *                key).
     */
    protected function _cacheFields()
    {
        $c = $this->getParam('cache');
        $out = $c['fields'];

        if (!$this->_capability()->isEnabled('CONDSTORE')) {
            unset($out[Horde_Imap_Client::FETCH_FLAGS]);
        }

        return $out;
    }

    /**
     * Return the current mailbox synchronization status.
     *
     * @param mixed $mailbox  A mailbox. Either a Horde_Imap_Client_Mailbox
     *                        object or a string (UTF-8).
     *
     * @return array  An array with status data. (This data is not guaranteed
     *                to have any specific format).
     */
    protected function _syncStatus($mailbox)
    {
        $status = $this->status(
            $mailbox,
            Horde_Imap_Client::STATUS_HIGHESTMODSEQ |
            Horde_Imap_Client::STATUS_MESSAGES |
            Horde_Imap_Client::STATUS_UIDNEXT_FORCE |
            Horde_Imap_Client::STATUS_UIDVALIDITY
        );

        $fields = array('uidnext', 'uidvalidity');
        if (empty($status['highestmodseq'])) {
            $fields[] = 'messages';
        } else {
            $fields[] = 'highestmodseq';
        }

        $out = array();
        $sync_map = array_flip(Horde_Imap_Client_Data_Sync::$map);

        foreach ($fields as $val) {
            $out[$sync_map[$val]] = $status[$val];
        }

        return array_filter($out);
    }

    /**
     * Get a message UID by the Message-ID. Returns the last message in a
     * mailbox that matches.
     *
     * @param Horde_Imap_Client_Mailbox $mailbox  The mailbox to search
     * @param string $msgid                       Message-ID.
     *
     * @return string  UID (null if not found).
     */
    protected function _getUidByMessageId($mailbox, $msgid)
    {
        if (!$msgid) {
            return null;
        }

        $query = new Horde_Imap_Client_Search_Query();
        $query->headerText('Message-ID', $msgid);
        $res = $this->search($mailbox, $query, array(
            'results' => array(Horde_Imap_Client::SEARCH_RESULTS_MAX)
        ));

        return $res['max'];
    }

}
