Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.
<?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(array( < 'i' => $this->_init, < 'p' => $this->_params, < 'v' => self::VERSION < ));
> return serialize($this->__serialize());
} /** */ public function unserialize($data) { $data = @unserialize($data);
< if (!is_array($data) || < !isset($data['v']) || < ($data['v'] != self::VERSION)) {
> 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 ));
< $disappear = array_diff($uids_ob->ids, $vanished->ids); < if (!empty($disappear)) { < $this->_deleteMsgs($this->_selected, $this->getIdsOb($disappear));
> 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']; } }