1<?php
2/**
3 * Copyright 2008-2017 Horde LLC (http://www.horde.org/)
4 *
5 * See the enclosed file LICENSE for license information (LGPL). If you
6 * did not receive this file, see http://www.horde.org/licenses/lgpl21.
7 *
8 * @category  Horde
9 * @copyright 2008-2017 Horde LLC
10 * @license   http://www.horde.org/licenses/lgpl21 LGPL 2.1
11 * @package   Imap_Client
12 */
13
14/**
15 * An abstracted API interface to IMAP backends supporting the IMAP4rev1
16 * protocol (RFC 3501).
17 *
18 * @author    Michael Slusarz <slusarz@horde.org>
19 * @category  Horde
20 * @copyright 2008-2017 Horde LLC
21 * @license   http://www.horde.org/licenses/lgpl21 LGPL 2.1
22 * @package   Imap_Client
23 *
24 * @property-read Horde_Imap_Client_Base_Alert $alerts_ob
25                  The alert reporting object (@since 2.26.0)
26 * @property-read Horde_Imap_Client_Data_Capability $capability
27 *                A capability object. (@since 2.24.0)
28 * @property-read Horde_Imap_Client_Data_SearchCharset $search_charset
29 *                A search charset object. (@since 2.24.0)
30 * @property-read Horde_Imap_Client_Url $url  The URL object for the current
31 *                connection parameters (@since 2.24.0)
32 */
33abstract class Horde_Imap_Client_Base
34implements Serializable, SplObserver
35{
36    /** Serialized version. */
37    const VERSION = 3;
38
39    /** Cache names for miscellaneous data. */
40    const CACHE_MODSEQ = '_m';
41    const CACHE_SEARCH = '_s';
42    /* @since 2.9.0 */
43    const CACHE_SEARCHID = '_i';
44
45    /** Cache names used exclusively within this class. @since 2.11.0 */
46    const CACHE_DOWNGRADED = 'HICdg';
47
48    /**
49     * The list of fetch fields that can be cached, and their cache names.
50     *
51     * @var array
52     */
53    public $cacheFields = array(
54        Horde_Imap_Client::FETCH_ENVELOPE => 'HICenv',
55        Horde_Imap_Client::FETCH_FLAGS => 'HICflags',
56        Horde_Imap_Client::FETCH_HEADERS => 'HIChdrs',
57        Horde_Imap_Client::FETCH_IMAPDATE => 'HICdate',
58        Horde_Imap_Client::FETCH_SIZE => 'HICsize',
59        Horde_Imap_Client::FETCH_STRUCTURE => 'HICstruct'
60    );
61
62    /**
63     * Has the internal configuration changed?
64     *
65     * @var boolean
66     */
67    public $changed = false;
68
69    /**
70     * Horde_Imap_Client is optimized for short (i.e. 1 seconds) scripts. It
71     * makes heavy use of mailbox caching to save on server accesses. This
72     * property should be set to false for long-running scripts, or else
73     * status() data may not reflect the current state of the mailbox on the
74     * server.
75     *
76     * @since 2.14.0
77     *
78     * @var boolean
79     */
80    public $statuscache = true;
81
82    /**
83     * Alerts reporting object.
84     *
85     * @var Horde_Imap_Client_Base_Alerts
86     */
87    protected $_alerts;
88
89    /**
90     * The Horde_Imap_Client_Cache object.
91     *
92     * @var Horde_Imap_Client_Cache
93     */
94    protected $_cache = null;
95
96    /**
97     * Connection to the IMAP server.
98     *
99     * @var Horde\Socket\Client
100     */
101    protected $_connection = null;
102
103    /**
104     * The debug object.
105     *
106     * @var Horde_Imap_Client_Base_Debug
107     */
108    protected $_debug = null;
109
110    /**
111     * The default ports to use for a connection.
112     * First element is non-secure, second is SSL.
113     *
114     * @var array
115     */
116    protected $_defaultPorts = array();
117
118    /**
119     * The fetch data object type to return.
120     *
121     * @var string
122     */
123    protected $_fetchDataClass = 'Horde_Imap_Client_Data_Fetch';
124
125    /**
126     * Cached server data.
127     *
128     * @var array
129     */
130    protected $_init;
131
132    /**
133     * Is there an active authenticated connection to the IMAP Server?
134     *
135     * @var boolean
136     */
137    protected $_isAuthenticated = false;
138
139    /**
140     * The current mailbox selection mode.
141     *
142     * @var integer
143     */
144    protected $_mode = 0;
145
146    /**
147     * Hash containing connection parameters.
148     * This hash never changes.
149     *
150     * @var array
151     */
152    protected $_params = array();
153
154    /**
155     * The currently selected mailbox.
156     *
157     * @var Horde_Imap_Client_Mailbox
158     */
159    protected $_selected = null;
160
161    /**
162     * Temp array (destroyed at end of process).
163     *
164     * @var array
165     */
166    protected $_temp = array();
167
168    /**
169     * Constructor.
170     *
171     * @param array $params   Configuration parameters:
172     * <pre>
173     * - cache: (array) If set, caches data from fetch(), search(), and
174     *          thread() calls. Requires the horde/Cache package to be
175     *          installed. The array can contain the following keys (see
176     *          Horde_Imap_Client_Cache for default values):
177     *   - backend: [REQUIRED (or cacheob)] (Horde_Imap_Client_Cache_Backend)
178     *              Backend cache driver [@since 2.9.0].
179     *   - fetch_ignore: (array) A list of mailboxes to ignore when storing
180     *                   fetch data.
181     *   - fields: (array) The fetch criteria to cache. If not defined, all
182     *             cacheable data is cached. The following is a list of
183     *             criteria that can be cached:
184     *     - Horde_Imap_Client::FETCH_ENVELOPE
185     *     - Horde_Imap_Client::FETCH_FLAGS
186     *       Only if server supports CONDSTORE extension
187     *     - Horde_Imap_Client::FETCH_HEADERS
188     *       Only for queries that specifically request caching
189     *     - Horde_Imap_Client::FETCH_IMAPDATE
190     *     - Horde_Imap_Client::FETCH_SIZE
191     *     - Horde_Imap_Client::FETCH_STRUCTURE
192     * - capability_ignore: (array) A list of IMAP capabilites to ignore, even
193     *                      if they are supported on the server.
194     *                      DEFAULT: No supported capabilities are ignored.
195     * - comparator: (string) The search comparator to use instead of the
196     *               default server comparator. See setComparator() for
197     *               format.
198     *               DEFAULT: Use the server default
199     * - context: (array) Any context parameters passed to
200     *            stream_create_context(). @since 2.27.0
201     * - debug: (string) If set, will output debug information to the stream
202     *          provided. The value can be any PHP supported wrapper that can
203     *          be opened via PHP's fopen() function.
204     *          DEFAULT: No debug output
205     * - hostspec: (string) The hostname or IP address of the server.
206     *             DEFAULT: 'localhost'
207     * - id: (array) Send ID information to the server (only if server
208     *       supports the ID extension). An array with the keys as the fields
209     *       to send and the values being the associated values. See RFC 2971
210     *       [3.3] for a list of standard field values.
211     *       DEFAULT: No info sent to server
212     * - lang: (array) A list of languages (in priority order) to be used to
213     *         display human readable messages.
214     *         DEFAULT: Messages output in IMAP server default language
215     * - password: (mixed) The user password. Either a string or a
216     *             Horde_Imap_Client_Base_Password object [@since 2.14.0].
217     * - port: (integer) The server port to which we will connect.
218     *         DEFAULT: 143 (imap or imap w/TLS) or 993 (imaps)
219     * - secure: (string) Use SSL or TLS to connect. Values:
220     *   - false (No encryption)
221     *   - 'ssl' (Auto-detect SSL version)
222     *   - 'sslv2' (Force SSL version 3)
223     *   - 'sslv3' (Force SSL version 2)
224     *   - 'tls' (TLS; started via protocol-level negotation over
225     *     unencrypted channel; RECOMMENDED way of initiating secure
226     *     connection)
227     *   - 'tlsv1' (TLS direct version 1.x connection to server) [@since
228     *     2.16.0]
229     *   - true (TLS if available/necessary) [@since 2.15.0]
230     *     DEFAULT: false
231     * - timeout: (integer)  Connection timeout, in seconds.
232     *            DEFAULT: 30 seconds
233     * - username: (string) [REQUIRED] The username.
234     * - authusername (string) The username used for SASL authentication.
235     * 	 If specified this is the user name whose password is used
236     * 	 (e.g. administrator).
237     * 	 Only valid for RFC 2595/4616 - PLAIN SASL mechanism.
238     * 	 DEFAULT: the same value provided in the username parameter.
239     * </pre>
240     */
241    public function __construct(array $params = array())
242    {
243        if (!isset($params['username'])) {
244            throw new InvalidArgumentException('Horde_Imap_Client requires a username.');
245        }
246
247        $this->_setInit();
248
249        // Default values.
250        $params = array_merge(array(
251            'context' => array(),
252            'hostspec' => 'localhost',
253            'secure' => false,
254            'timeout' => 30
255        ), array_filter($params));
256
257        if (!isset($params['port']) && strpos($params['hostspec'], 'unix://') !== 0) {
258            $params['port'] = (!empty($params['secure']) && in_array($params['secure'], array('ssl', 'sslv2', 'sslv3'), true))
259                ? $this->_defaultPorts[1]
260                : $this->_defaultPorts[0];
261        }
262
263        if (empty($params['cache'])) {
264            $params['cache'] = array('fields' => array());
265        } elseif (empty($params['cache']['fields'])) {
266            $params['cache']['fields'] = $this->cacheFields;
267        } else {
268            $params['cache']['fields'] = array_flip($params['cache']['fields']);
269        }
270
271        if (empty($params['cache']['fetch_ignore'])) {
272            $params['cache']['fetch_ignore'] = array();
273        }
274
275        $this->_params = $params;
276        if (isset($params['password'])) {
277            $this->setParam('password', $params['password']);
278        }
279
280        $this->changed = true;
281        $this->_initOb();
282    }
283
284    /**
285     * Get encryption key.
286     *
287     * @deprecated  Pass callable into 'password' parameter instead.
288     *
289     * @return string  The encryption key.
290     */
291    protected function _getEncryptKey()
292    {
293        if (is_callable($ekey = $this->getParam('encryptKey'))) {
294            return call_user_func($ekey);
295        }
296
297        throw new InvalidArgumentException('encryptKey parameter is not a valid callback.');
298    }
299
300    /**
301     * Do initialization tasks.
302     */
303    protected function _initOb()
304    {
305        register_shutdown_function(array($this, 'shutdown'));
306
307        $this->_alerts = new Horde_Imap_Client_Base_Alerts();
308        // @todo: Remove (BC)
309        $this->_alerts->attach($this);
310
311        $this->_debug = ($debug = $this->getParam('debug'))
312            ? new Horde_Imap_Client_Base_Debug($debug)
313            : new Horde_Support_Stub();
314
315        // @todo: Remove (BC purposes)
316        if (isset($this->_init['capability']) &&
317            !is_object($this->_init['capability'])) {
318            $this->_setInit('capability');
319        }
320
321        foreach (array('capability', 'search_charset') as $val) {
322            if (isset($this->_init[$val])) {
323                $this->_init[$val]->attach($this);
324            }
325        }
326    }
327
328    /**
329     * Shutdown actions.
330     */
331    public function shutdown()
332    {
333        try {
334            $this->logout();
335        } catch (Horde_Imap_Client_Exception $e) {
336        }
337    }
338
339    /**
340     * This object can not be cloned.
341     */
342    public function __clone()
343    {
344        throw new LogicException('Object cannot be cloned.');
345    }
346
347    /**
348     */
349    public function update(SplSubject $subject)
350    {
351        if (($subject instanceof Horde_Imap_Client_Data_Capability) ||
352            ($subject instanceof Horde_Imap_Client_Data_SearchCharset)) {
353            $this->changed = true;
354        }
355
356        /* @todo: BC - remove */
357        if ($subject instanceof Horde_Imap_Client_Base_Alerts) {
358            $this->_temp['alerts'][] = $subject->getLast()->alert;
359        }
360    }
361
362    /**
363     */
364    public function serialize()
365    {
366        return serialize(array(
367            'i' => $this->_init,
368            'p' => $this->_params,
369            'v' => self::VERSION
370        ));
371    }
372
373    /**
374     */
375    public function unserialize($data)
376    {
377        $data = @unserialize($data);
378        if (!is_array($data) ||
379            !isset($data['v']) ||
380            ($data['v'] != self::VERSION)) {
381            throw new Exception('Cache version change');
382        }
383
384        $this->_init = $data['i'];
385        $this->_params = $data['p'];
386
387        $this->_initOb();
388    }
389
390    /**
391     */
392    public function __get($name)
393    {
394        switch ($name) {
395        case 'alerts_ob':
396            return $this->_alerts;
397
398        case 'capability':
399            return $this->_capability();
400
401        case 'search_charset':
402            if (!isset($this->_init['search_charset'])) {
403                $this->_init['search_charset'] = new Horde_Imap_Client_Data_SearchCharset();
404                $this->_init['search_charset']->attach($this);
405            }
406            $this->_init['search_charset']->setBaseOb($this);
407            return $this->_init['search_charset'];
408
409        case 'url':
410            $url = new Horde_Imap_Client_Url();
411            $url->hostspec = $this->getParam('hostspec');
412            $url->port = $this->getParam('port');
413            $url->protocol = 'imap';
414            return $url;
415        }
416    }
417
418    /**
419     * Set an initialization value.
420     *
421     * @param string $key  The initialization key. If null, resets all keys.
422     * @param mixed $val   The cached value. If null, removes the key.
423     */
424    public function _setInit($key = null, $val = null)
425    {
426        if (is_null($key)) {
427            $this->_init = array();
428        } elseif (is_null($val)) {
429            unset($this->_init[$key]);
430        } else {
431            switch ($key) {
432            case 'capability':
433                if ($ci = $this->getParam('capability_ignore')) {
434                    $ignored = array();
435
436                    foreach ($ci as $val2) {
437                        $c = explode('=', $val2);
438
439                        if ($val->query($c[0], isset($c[1]) ? $c[1] : null)) {
440                            $ignored[] = $val2;
441                            $val->remove($c[0], isset($c[1]) ? $c[1] : null);
442                        }
443                    }
444
445                    if ($this->_debug->debug && !empty($ignored)) {
446                        $this->_debug->info(sprintf(
447                            'CONFIG: IGNORING these IMAP capabilities: %s',
448                            implode(', ', $ignored)
449                        ));
450                    }
451                }
452
453                $val->attach($this);
454                break;
455            }
456
457            /* Nothing has changed. */
458            if (isset($this->_init[$key]) && ($this->_init[$key] === $val)) {
459                return;
460            }
461
462            $this->_init[$key] = $val;
463        }
464
465        $this->changed = true;
466    }
467
468    /**
469     * Initialize the Horde_Imap_Client_Cache object, if necessary.
470     *
471     * @param boolean $current  If true, we are going to update the currently
472     *                          selected mailbox. Add an additional check to
473     *                          see if caching is available in current
474     *                          mailbox.
475     *
476     * @return boolean  Returns true if caching is enabled.
477     */
478    protected function _initCache($current = false)
479    {
480        $c = $this->getParam('cache');
481
482        if (empty($c['fields'])) {
483            return false;
484        }
485
486        if (is_null($this->_cache)) {
487            if (isset($c['backend'])) {
488                $backend = $c['backend'];
489            } elseif (isset($c['cacheob'])) {
490                /* Deprecated */
491                $backend = new Horde_Imap_Client_Cache_Backend_Cache($c);
492            } else {
493                return false;
494            }
495
496            $this->_cache = new Horde_Imap_Client_Cache(array(
497                'backend' => $backend,
498                'baseob' => $this,
499                'debug' => $this->_debug
500            ));
501        }
502
503        return $current
504            /* If UIDs are labeled as not sticky, don't cache since UIDs will
505             * change on every access. */
506            ? !($this->_mailboxOb()->getStatus(Horde_Imap_Client::STATUS_UIDNOTSTICKY))
507            : true;
508    }
509
510    /**
511     * Returns a value from the internal params array.
512     *
513     * @param string $key  The param key.
514     *
515     * @return mixed  The param value, or null if not found.
516     */
517    public function getParam($key)
518    {
519        /* Passwords may be stored encrypted. */
520        switch ($key) {
521        case 'password':
522            if (isset($this->_params[$key]) &&
523                ($this->_params[$key] instanceof Horde_Imap_Client_Base_Password)) {
524                return $this->_params[$key]->getPassword();
525            }
526
527            // DEPRECATED
528            if (!empty($this->_params['_passencrypt'])) {
529                try {
530                    $secret = new Horde_Secret();
531                    return $secret->read($this->_getEncryptKey(), $this->_params['password']);
532                } catch (Exception $e) {
533                    return null;
534                }
535            }
536            break;
537        }
538
539        return isset($this->_params[$key])
540            ? $this->_params[$key]
541            : null;
542    }
543
544    /**
545     * Sets a configuration parameter value.
546     *
547     * @param string $key  The param key.
548     * @param mixed $val   The param value.
549     */
550    public function setParam($key, $val)
551    {
552        switch ($key) {
553        case 'password':
554            if ($val instanceof Horde_Imap_Client_Base_Password) {
555                break;
556            }
557
558            // DEPRECATED: Encrypt password.
559            try {
560                $encrypt_key = $this->_getEncryptKey();
561                if (strlen($encrypt_key)) {
562                    $secret = new Horde_Secret();
563                    $val = $secret->write($encrypt_key, $val);
564                    $this->_params['_passencrypt'] = true;
565                }
566            } catch (Exception $e) {}
567            break;
568        }
569
570        $this->_params[$key] = $val;
571        $this->changed = true;
572    }
573
574    /**
575     * Returns the Horde_Imap_Client_Cache object used, if available.
576     *
577     * @return mixed  Either the cache object or null.
578     */
579    public function getCache()
580    {
581        $this->_initCache();
582        return $this->_cache;
583    }
584
585    /**
586     * Returns the correct IDs object for use with this driver.
587     *
588     * @param mixed $ids  Either self::ALL, self::SEARCH_RES, self::LARGEST,
589     *                    Horde_Imap_Client_Ids object, array, or sequence
590     *                    string.
591     * @param boolean $sequence  Are $ids message sequence numbers?
592     *
593     * @return Horde_Imap_Client_Ids  The IDs object.
594     */
595    public function getIdsOb($ids = null, $sequence = false)
596    {
597        return new Horde_Imap_Client_Ids($ids, $sequence);
598    }
599
600    /**
601     * Returns whether the IMAP server supports the given capability
602     * (See RFC 3501 [6.1.1]).
603     *
604     * @deprecated  Use $capability property instead.
605     *
606     * @param string $capability  The capability string to query.
607     *
608     * @return mixed  True if the server supports the queried capability,
609     *                false if it doesn't, or an array if the capability can
610     *                contain multiple values.
611     */
612    public function queryCapability($capability)
613    {
614        try {
615            $c = $this->_capability();
616            return ($out = $c->getParams($capability))
617                ? $out
618                : $c->query($capability);
619        } catch (Horde_Imap_Client_Exception $e) {
620            return false;
621        }
622    }
623
624    /**
625     * Get CAPABILITY information from the IMAP server.
626     *
627     * @deprecated  Use $capability property instead.
628     *
629     * @return array  The capability array.
630     *
631     * @throws Horde_Imap_Client_Exception
632     */
633    public function capability()
634    {
635        return $this->_capability()->toArray();
636    }
637
638    /**
639     * Query server capability.
640     *
641     * Required because internal code can't call capability via magic method
642     * directly - it may not exist yet, the creation code may call capability
643     * recursively, and __get() doesn't allow recursive calls to the same
644     * property (chicken/egg issue).
645     *
646     * @return mixed  The capability object if no arguments provided. If
647     *                arguments are provided, they are passed to the query()
648     *                method and this value is returned.
649     * @throws Horde_Imap_Client_Exception
650     */
651    protected function _capability()
652    {
653        if (!isset($this->_init['capability'])) {
654            $this->_initCapability();
655        }
656
657        return ($args = func_num_args())
658            ? $this->_init['capability']->query(func_get_arg(0), ($args > 1) ? func_get_arg(1) : null)
659            : $this->_init['capability'];
660    }
661
662    /**
663     * Retrieve capability information from the IMAP server.
664     *
665     * @throws Horde_Imap_Client_Exception
666     */
667    abstract protected function _initCapability();
668
669    /**
670     * Send a NOOP command (RFC 3501 [6.1.2]).
671     *
672     * @throws Horde_Imap_Client_Exception
673     */
674    public function noop()
675    {
676        if (!$this->_connection) {
677            // NOOP can be called in the unauthenticated state.
678            $this->_connect();
679        }
680        $this->_noop();
681    }
682
683    /**
684     * Send a NOOP command.
685     *
686     * @throws Horde_Imap_Client_Exception
687     */
688    abstract protected function _noop();
689
690    /**
691     * Get the NAMESPACE information from the IMAP server (RFC 2342).
692     *
693     * @param array $additional  If the server supports namespaces, any
694     *                           additional namespaces to add to the
695     *                           namespace list that are not broadcast by
696     *                           the server. The namespaces must be UTF-8
697     *                           strings.
698     * @param array $opts        Additional options:
699     *   - ob_return: (boolean) If true, returns a
700     *                Horde_Imap_Client_Namespace_List object instead of an
701     *                array.
702     *
703     * @return mixed  A Horde_Imap_Client_Namespace_List object if
704     *                'ob_return', is true. Otherwise, an array of namespace
705     *                objects (@deprecated) with the name as the key (UTF-8)
706     *                and the following values:
707     * <pre>
708     *  - delimiter: (string) The namespace delimiter.
709     *  - hidden: (boolean) Is this a hidden namespace?
710     *  - name: (string) The namespace name (UTF-8).
711     *  - translation: (string) Returns the translated name of the namespace
712     *                 (UTF-8). Requires RFC 5255 and a previous call to
713     *                 setLanguage().
714     *  - type: (integer) The namespace type. Either:
715     *    - Horde_Imap_Client::NS_PERSONAL
716     *    - Horde_Imap_Client::NS_OTHER
717     *    - Horde_Imap_Client::NS_SHARED
718     * </pre>
719     *
720     * @throws Horde_Imap_Client_Exception
721     */
722    public function getNamespaces(
723        array $additional = array(), array $opts = array()
724    )
725    {
726        $additional = array_map('strval', $additional);
727        $sig = hash(
728            'md5',
729            json_encode($additional) . intval(empty($opts['ob_return']))
730        );
731
732        if (isset($this->_init['namespace'][$sig])) {
733            $ns = $this->_init['namespace'][$sig];
734        } else {
735            $this->login();
736
737            $ns = $this->_getNamespaces();
738
739            /* Skip namespaces if we have already auto-detected them. Also,
740             * hidden namespaces cannot be empty. */
741            $to_process = array_diff(array_filter($additional, 'strlen'), array_map('strlen', iterator_to_array($ns)));
742            if (!empty($to_process)) {
743                foreach ($this->listMailboxes($to_process, Horde_Imap_Client::MBOX_ALL, array('delimiter' => true)) as $key => $val) {
744                    $ob = new Horde_Imap_Client_Data_Namespace();
745                    $ob->delimiter = $val['delimiter'];
746                    $ob->hidden = true;
747                    $ob->name = $key;
748                    $ob->type = $ob::NS_SHARED;
749                    $ns[$val] = $ob;
750                }
751            }
752
753            if (!count($ns)) {
754                /* This accurately determines the namespace information of the
755                 * base namespace if the NAMESPACE command is not supported.
756                 * See: RFC 3501 [6.3.8] */
757                $mbox = $this->listMailboxes('', Horde_Imap_Client::MBOX_ALL, array('delimiter' => true));
758                $first = reset($mbox);
759
760                $ob = new Horde_Imap_Client_Data_Namespace();
761                $ob->delimiter = $first['delimiter'];
762                $ns[''] = $ob;
763            }
764
765            $this->_init['namespace'][$sig] = $ns;
766            $this->_setInit('namespace', $this->_init['namespace']);
767        }
768
769        if (!empty($opts['ob_return'])) {
770            return $ns;
771        }
772
773        /* @todo Remove for 3.0 */
774        $out = array();
775        foreach ($ns as $key => $val) {
776            $out[$key] = array(
777                'delimiter' => $val->delimiter,
778                'hidden' => $val->hidden,
779                'name' => $val->name,
780                'translation' => $val->translation,
781                'type' => $val->type
782            );
783        }
784
785        return $out;
786    }
787
788    /**
789     * Get the NAMESPACE information from the IMAP server.
790     *
791     * @return Horde_Imap_Client_Namespace_List  Namespace list object.
792     *
793     * @throws Horde_Imap_Client_Exception
794     */
795    abstract protected function _getNamespaces();
796
797    /**
798     * Display if connection to the server has been secured via TLS or SSL.
799     *
800     * @return boolean  True if the IMAP connection is secured.
801     */
802    public function isSecureConnection()
803    {
804        return ($this->_connection && $this->_connection->secure);
805    }
806
807    /**
808     * Connect to the remote server.
809     *
810     * @throws Horde_Imap_Client_Exception
811     */
812    abstract protected function _connect();
813
814    /**
815     * Return a list of alerts that MUST be presented to the user (RFC 3501
816     * [7.1]).
817     *
818     * @deprecated  Add an observer to the $alerts_ob property instead.
819     *
820     * @return array  An array of alert messages.
821     */
822    public function alerts()
823    {
824        $alerts = isset($this->_temp['alerts'])
825            ? $this->_temp['alerts']
826            : array();
827        unset($this->_temp['alerts']);
828        return $alerts;
829    }
830
831    /**
832     * Login to the IMAP server.
833     *
834     * @throws Horde_Imap_Client_Exception
835     */
836    public function login()
837    {
838        if (!$this->_isAuthenticated && $this->_login()) {
839            if ($this->getParam('id')) {
840                try {
841                    $this->sendID();
842                    /* ID is queued - force sending the queued command. */
843                    $this->_sendCmd($this->_pipeline());
844                } catch (Horde_Imap_Client_Exception_NoSupportExtension $e) {
845                    // Ignore if server doesn't support ID extension.
846                }
847            }
848
849            if ($this->getParam('comparator')) {
850                try {
851                    $this->setComparator();
852                } catch (Horde_Imap_Client_Exception_NoSupportExtension $e) {
853                    // Ignore if server doesn't support I18NLEVEL=2
854                }
855            }
856        }
857
858        $this->_isAuthenticated = true;
859    }
860
861    /**
862     * Login to the IMAP server.
863     *
864     * @return boolean  Return true if global login tasks should be run.
865     *
866     * @throws Horde_Imap_Client_Exception
867     */
868    abstract protected function _login();
869
870    /**
871     * Logout from the IMAP server (see RFC 3501 [6.1.3]).
872     */
873    public function logout()
874    {
875        if ($this->_isAuthenticated && $this->_connection->connected) {
876            $this->_logout();
877            $this->_connection->close();
878        }
879
880        $this->_connection = $this->_selected = null;
881        $this->_isAuthenticated = false;
882        $this->_mode = 0;
883    }
884
885    /**
886     * Logout from the IMAP server (see RFC 3501 [6.1.3]).
887     */
888    abstract protected function _logout();
889
890    /**
891     * Send ID information to the IMAP server (RFC 2971).
892     *
893     * @param array $info  Overrides the value of the 'id' param and sends
894     *                     this information instead.
895     *
896     * @throws Horde_Imap_Client_Exception
897     * @throws Horde_Imap_Client_Exception_NoSupportExtension
898     */
899    public function sendID($info = null)
900    {
901        if (!$this->_capability('ID')) {
902            throw new Horde_Imap_Client_Exception_NoSupportExtension('ID');
903        }
904
905        $this->_sendID(is_null($info) ? ($this->getParam('id') ?: array()) : $info);
906    }
907
908    /**
909     * Send ID information to the IMAP server (RFC 2971).
910     *
911     * @param array $info  The information to send to the server.
912     *
913     * @throws Horde_Imap_Client_Exception
914     */
915    abstract protected function _sendID($info);
916
917    /**
918     * Return ID information from the IMAP server (RFC 2971).
919     *
920     * @return array  An array of information returned, with the keys as the
921     *                'field' and the values as the 'value'.
922     *
923     * @throws Horde_Imap_Client_Exception
924     * @throws Horde_Imap_Client_Exception_NoSupportExtension
925     */
926    public function getID()
927    {
928        if (!$this->_capability('ID')) {
929            throw new Horde_Imap_Client_Exception_NoSupportExtension('ID');
930        }
931
932        return $this->_getID();
933    }
934
935    /**
936     * Return ID information from the IMAP server (RFC 2971).
937     *
938     * @return array  An array of information returned, with the keys as the
939     *                'field' and the values as the 'value'.
940     *
941     * @throws Horde_Imap_Client_Exception
942     */
943    abstract protected function _getID();
944
945    /**
946     * Sets the preferred language for server response messages (RFC 5255).
947     *
948     * @param array $langs  Overrides the value of the 'lang' param and sends
949     *                      this list of preferred languages instead. The
950     *                      special string 'i-default' can be used to restore
951     *                      the language to the server default.
952     *
953     * @return string  The language accepted by the server, or null if the
954     *                 default language is used.
955     *
956     * @throws Horde_Imap_Client_Exception
957     */
958    public function setLanguage($langs = null)
959    {
960        $lang = null;
961
962        if ($this->_capability('LANGUAGE')) {
963            $lang = is_null($langs)
964                ? $this->getParam('lang')
965                : $langs;
966        }
967
968        return is_null($lang)
969            ? null
970            : $this->_setLanguage($lang);
971    }
972
973    /**
974     * Sets the preferred language for server response messages (RFC 5255).
975     *
976     * @param array $langs  The preferred list of languages.
977     *
978     * @return string  The language accepted by the server, or null if the
979     *                 default language is used.
980     *
981     * @throws Horde_Imap_Client_Exception
982     */
983    abstract protected function _setLanguage($langs);
984
985    /**
986     * Gets the preferred language for server response messages (RFC 5255).
987     *
988     * @param array $list  If true, return the list of available languages.
989     *
990     * @return mixed  If $list is true, the list of languages available on the
991     *                server (may be empty). If false, the language used by
992     *                the server, or null if the default language is used.
993     *
994     * @throws Horde_Imap_Client_Exception
995     */
996    public function getLanguage($list = false)
997    {
998        if (!$this->_capability('LANGUAGE')) {
999            return $list ? array() : null;
1000        }
1001
1002        return $this->_getLanguage($list);
1003    }
1004
1005    /**
1006     * Gets the preferred language for server response messages (RFC 5255).
1007     *
1008     * @param array $list  If true, return the list of available languages.
1009     *
1010     * @return mixed  If $list is true, the list of languages available on the
1011     *                server (may be empty). If false, the language used by
1012     *                the server, or null if the default language is used.
1013     *
1014     * @throws Horde_Imap_Client_Exception
1015     */
1016    abstract protected function _getLanguage($list);
1017
1018    /**
1019     * Open a mailbox.
1020     *
1021     * @param mixed $mailbox  The mailbox to open. Either a
1022     *                        Horde_Imap_Client_Mailbox object or a string
1023     *                        (UTF-8).
1024     * @param integer $mode   The access mode. Either
1025     *   - Horde_Imap_Client::OPEN_READONLY
1026     *   - Horde_Imap_Client::OPEN_READWRITE
1027     *   - Horde_Imap_Client::OPEN_AUTO
1028     *
1029     * @throws Horde_Imap_Client_Exception
1030     */
1031    public function openMailbox($mailbox, $mode = Horde_Imap_Client::OPEN_AUTO)
1032    {
1033        $this->login();
1034
1035        $change = false;
1036        $mailbox = Horde_Imap_Client_Mailbox::get($mailbox);
1037
1038        if ($mode == Horde_Imap_Client::OPEN_AUTO) {
1039            if (is_null($this->_selected) ||
1040                !$mailbox->equals($this->_selected)) {
1041                $mode = Horde_Imap_Client::OPEN_READONLY;
1042                $change = true;
1043            }
1044        } else {
1045            $change = (is_null($this->_selected) ||
1046                       !$mailbox->equals($this->_selected) ||
1047                       ($mode != $this->_mode));
1048        }
1049
1050        if ($change) {
1051            $this->_openMailbox($mailbox, $mode);
1052            $this->_mailboxOb()->open = true;
1053            if ($this->_initCache(true)) {
1054                $this->_condstoreSync();
1055            }
1056        }
1057    }
1058
1059    /**
1060     * Open a mailbox.
1061     *
1062     * @param Horde_Imap_Client_Mailbox $mailbox  The mailbox to open.
1063     * @param integer $mode                       The access mode.
1064     *
1065     * @throws Horde_Imap_Client_Exception
1066     */
1067    abstract protected function _openMailbox(Horde_Imap_Client_Mailbox $mailbox,
1068                                             $mode);
1069
1070    /**
1071     * Called when the selected mailbox is changed.
1072     *
1073     * @param mixed $mailbox  The selected mailbox or null.
1074     * @param integer $mode   The access mode.
1075     */
1076    protected function _changeSelected($mailbox = null, $mode = null)
1077    {
1078        $this->_mode = $mode;
1079        if (is_null($mailbox)) {
1080            $this->_selected = null;
1081        } else {
1082            $this->_selected = clone $mailbox;
1083            $this->_mailboxOb()->reset();
1084        }
1085    }
1086
1087    /**
1088     * Return the Horde_Imap_Client_Base_Mailbox object.
1089     *
1090     * @param string $mailbox  The mailbox name. Defaults to currently
1091     *                         selected mailbox.
1092     *
1093     * @return Horde_Imap_Client_Base_Mailbox  Mailbox object.
1094     */
1095    protected function _mailboxOb($mailbox = null)
1096    {
1097        $name = is_null($mailbox)
1098            ? strval($this->_selected)
1099            : strval($mailbox);
1100
1101        if (!isset($this->_temp['mailbox_ob'][$name])) {
1102            $this->_temp['mailbox_ob'][$name] = new Horde_Imap_Client_Base_Mailbox();
1103        }
1104
1105        return $this->_temp['mailbox_ob'][$name];
1106    }
1107
1108    /**
1109     * Return the currently opened mailbox and access mode.
1110     *
1111     * @return mixed  Null if no mailbox selected, or an array with two
1112     *                elements:
1113     *   - mailbox: (Horde_Imap_Client_Mailbox) The mailbox object.
1114     *   - mode: (integer) Current mode.
1115     *
1116     * @throws Horde_Imap_Client_Exception
1117     */
1118    public function currentMailbox()
1119    {
1120        return is_null($this->_selected)
1121            ? null
1122            : array(
1123                'mailbox' => clone $this->_selected,
1124                'mode' => $this->_mode
1125            );
1126    }
1127
1128    /**
1129     * Create a mailbox.
1130     *
1131     * @param mixed $mailbox  The mailbox to create. Either a
1132     *                        Horde_Imap_Client_Mailbox object or a string
1133     *                        (UTF-8).
1134     * @param array $opts     Additional options:
1135     *   - special_use: (array) An array of special-use flags to mark the
1136     *                  mailbox with. The server MUST support RFC 6154.
1137     *
1138     * @throws Horde_Imap_Client_Exception
1139     */
1140    public function createMailbox($mailbox, array $opts = array())
1141    {
1142        $this->login();
1143
1144        if (!$this->_capability('CREATE-SPECIAL-USE')) {
1145            unset($opts['special_use']);
1146        }
1147
1148        $this->_createMailbox(Horde_Imap_Client_Mailbox::get($mailbox), $opts);
1149    }
1150
1151    /**
1152     * Create a mailbox.
1153     *
1154     * @param Horde_Imap_Client_Mailbox $mailbox  The mailbox to create.
1155     * @param array $opts                         Additional options. See
1156     *                                            createMailbox().
1157     *
1158     * @throws Horde_Imap_Client_Exception
1159     */
1160    abstract protected function _createMailbox(Horde_Imap_Client_Mailbox $mailbox,
1161                                               $opts);
1162
1163    /**
1164     * Delete a mailbox.
1165     *
1166     * @param mixed $mailbox  The mailbox to delete. Either a
1167     *                        Horde_Imap_Client_Mailbox object or a string
1168     *                        (UTF-8).
1169     *
1170     * @throws Horde_Imap_Client_Exception
1171     */
1172    public function deleteMailbox($mailbox)
1173    {
1174        $this->login();
1175
1176        $mailbox = Horde_Imap_Client_Mailbox::get($mailbox);
1177
1178        $this->_deleteMailbox($mailbox);
1179        $this->_deleteMailboxPost($mailbox);
1180    }
1181
1182    /**
1183     * Delete a mailbox.
1184     *
1185     * @param Horde_Imap_Client_Mailbox $mailbox  The mailbox to delete.
1186     *
1187     * @throws Horde_Imap_Client_Exception
1188     */
1189    abstract protected function _deleteMailbox(Horde_Imap_Client_Mailbox $mailbox);
1190
1191    /**
1192     * Actions to perform after a mailbox delete.
1193     *
1194     * @param Horde_Imap_Client_Mailbox $mailbox  The deleted mailbox.
1195     */
1196    protected function _deleteMailboxPost(Horde_Imap_Client_Mailbox $mailbox)
1197    {
1198        /* Delete mailbox caches. */
1199        if ($this->_initCache()) {
1200            $this->_cache->deleteMailbox($mailbox);
1201        }
1202        unset($this->_temp['mailbox_ob'][strval($mailbox)]);
1203
1204        /* Unsubscribe from mailbox. */
1205        try {
1206            $this->subscribeMailbox($mailbox, false);
1207        } catch (Horde_Imap_Client_Exception $e) {
1208            // Ignore failed unsubscribe request
1209        }
1210    }
1211
1212    /**
1213     * Rename a mailbox.
1214     *
1215     * @param mixed $old  The old mailbox name. Either a
1216     *                    Horde_Imap_Client_Mailbox object or a string (UTF-8).
1217     * @param mixed $new  The new mailbox name. Either a
1218     *                    Horde_Imap_Client_Mailbox object or a string (UTF-8).
1219     *
1220     * @throws Horde_Imap_Client_Exception
1221     */
1222    public function renameMailbox($old, $new)
1223    {
1224        // Login will be handled by first listMailboxes() call.
1225
1226        $old = Horde_Imap_Client_Mailbox::get($old);
1227        $new = Horde_Imap_Client_Mailbox::get($new);
1228
1229        /* Check if old mailbox(es) were subscribed to. */
1230        $base = $this->listMailboxes($old, Horde_Imap_Client::MBOX_SUBSCRIBED, array('delimiter' => true));
1231        if (empty($base)) {
1232            $base = $this->listMailboxes($old, Horde_Imap_Client::MBOX_ALL, array('delimiter' => true));
1233            $base = reset($base);
1234            $subscribed = array();
1235        } else {
1236            $base = reset($base);
1237            $subscribed = array($base['mailbox']);
1238        }
1239
1240        $all_mboxes = array($base['mailbox']);
1241        if (strlen($base['delimiter'])) {
1242            $search = $old->list_escape . $base['delimiter'] . '*';
1243            $all_mboxes = array_merge($all_mboxes, $this->listMailboxes($search, Horde_Imap_Client::MBOX_ALL, array('flat' => true)));
1244            $subscribed = array_merge($subscribed, $this->listMailboxes($search, Horde_Imap_Client::MBOX_SUBSCRIBED, array('flat' => true)));
1245        }
1246
1247        $this->_renameMailbox($old, $new);
1248
1249        /* Delete mailbox actions. */
1250        foreach ($all_mboxes as $val) {
1251            $this->_deleteMailboxPost($val);
1252        }
1253
1254        foreach ($subscribed as $val) {
1255            try {
1256                $this->subscribeMailbox(new Horde_Imap_Client_Mailbox(substr_replace($val, $new, 0, strlen($old))));
1257            } catch (Horde_Imap_Client_Exception $e) {
1258                // Ignore failed subscription requests
1259            }
1260        }
1261    }
1262
1263    /**
1264     * Rename a mailbox.
1265     *
1266     * @param Horde_Imap_Client_Mailbox $old  The old mailbox name.
1267     * @param Horde_Imap_Client_Mailbox $new  The new mailbox name.
1268     *
1269     * @throws Horde_Imap_Client_Exception
1270     */
1271    abstract protected function _renameMailbox(Horde_Imap_Client_Mailbox $old,
1272                                               Horde_Imap_Client_Mailbox $new);
1273
1274    /**
1275     * Manage subscription status for a mailbox.
1276     *
1277     * @param mixed $mailbox      The mailbox to [un]subscribe to. Either a
1278     *                            Horde_Imap_Client_Mailbox object or a string
1279     *                            (UTF-8).
1280     * @param boolean $subscribe  True to subscribe, false to unsubscribe.
1281     *
1282     * @throws Horde_Imap_Client_Exception
1283     */
1284    public function subscribeMailbox($mailbox, $subscribe = true)
1285    {
1286        $this->login();
1287        $this->_subscribeMailbox(Horde_Imap_Client_Mailbox::get($mailbox), (bool)$subscribe);
1288    }
1289
1290    /**
1291     * Manage subscription status for a mailbox.
1292     *
1293     * @param Horde_Imap_Client_Mailbox $mailbox  The mailbox to [un]subscribe
1294     *                                            to.
1295     * @param boolean $subscribe                  True to subscribe, false to
1296     *                                            unsubscribe.
1297     *
1298     * @throws Horde_Imap_Client_Exception
1299     */
1300    abstract protected function _subscribeMailbox(Horde_Imap_Client_Mailbox $mailbox,
1301                                                  $subscribe);
1302
1303    /**
1304     * Obtain a list of mailboxes matching a pattern.
1305     *
1306     * @param mixed $pattern   The mailbox search pattern(s) (see RFC 3501
1307     *                         [6.3.8] for the format). A UTF-8 string or an
1308     *                         array of strings. If a Horde_Imap_Client_Mailbox
1309     *                         object is given, it is escaped (i.e. wildcard
1310     *                         patterns are converted to return the miminal
1311     *                         number of matches possible).
1312     * @param integer $mode    Which mailboxes to return.  Either:
1313     *   - Horde_Imap_Client::MBOX_SUBSCRIBED
1314     *     Return subscribed mailboxes.
1315     *   - Horde_Imap_Client::MBOX_SUBSCRIBED_EXISTS
1316     *     Return subscribed mailboxes that exist on the server.
1317     *   - Horde_Imap_Client::MBOX_UNSUBSCRIBED
1318     *     Return unsubscribed mailboxes.
1319     *   - Horde_Imap_Client::MBOX_ALL
1320     *     Return all mailboxes regardless of subscription status.
1321     *   - Horde_Imap_Client::MBOX_ALL_SUBSCRIBED (@since 2.23.0)
1322     *     Return all mailboxes regardless of subscription status, and ensure
1323     *     the '\subscribed' attribute is set if mailbox is subscribed
1324     *     (implies 'attributes' option is true).
1325     * @param array $options   Additional options:
1326     * <pre>
1327     *   - attributes: (boolean) If true, return attribute information under
1328     *                 the 'attributes' key.
1329     *                 DEFAULT: Do not return this information.
1330     *   - children: (boolean) Tell server to return children attribute
1331     *               information (\HasChildren, \HasNoChildren). Requires the
1332     *               LIST-EXTENDED extension to guarantee this information is
1333     *               returned. Server MAY return this attribute without this
1334     *               option, or if the CHILDREN extension is available, but it
1335     *               is not guaranteed.
1336     *               DEFAULT: false
1337     *   - flat: (boolean) If true, return a flat list of mailbox names only.
1338     *           Overrides the 'attributes' option.
1339     *           DEFAULT: Do not return flat list.
1340     *   - recursivematch: (boolean) Force the server to return information
1341     *                     about parent mailboxes that don't match other
1342     *                     selection options, but have some sub-mailboxes that
1343     *                     do. Information about children is returned in the
1344     *                     CHILDINFO extended data item ('extended'). Requires
1345     *                     the LIST-EXTENDED extension.
1346     *                     DEFAULT: false
1347     *   - remote: (boolean) Tell server to return mailboxes that reside on
1348     *             another server. Requires the LIST-EXTENDED extension.
1349     *             DEFAULT: false
1350     *   - special_use: (boolean) Tell server to return special-use attribute
1351     *                  information (see Horde_Imap_Client SPECIALUSE_*
1352     *                  constants). Server must support the SPECIAL-USE return
1353     *                  option for this setting to have any effect.
1354     *                  DEFAULT: false
1355     *   - status: (integer) Tell server to return status information. The
1356     *             value is a bitmask that may contain any of:
1357     *     - Horde_Imap_Client::STATUS_MESSAGES
1358     *     - Horde_Imap_Client::STATUS_RECENT
1359     *     - Horde_Imap_Client::STATUS_UIDNEXT
1360     *     - Horde_Imap_Client::STATUS_UIDVALIDITY
1361     *     - Horde_Imap_Client::STATUS_UNSEEN
1362     *     - Horde_Imap_Client::STATUS_HIGHESTMODSEQ
1363     *     DEFAULT: 0
1364     *   - sort: (boolean) If true, return a sorted list of mailboxes?
1365     *           DEFAULT: Do not sort the list.
1366     *   - sort_delimiter: (string) If 'sort' is true, this is the delimiter
1367     *                     used to sort the mailboxes.
1368     *                     DEFAULT: '.'
1369     * </pre>
1370     *
1371     * @return array  If 'flat' option is true, the array values are a list
1372     *                of Horde_Imap_Client_Mailbox objects. Otherwise, the
1373     *                keys are UTF-8 mailbox names and the values are arrays
1374     *                with these keys:
1375     *   - attributes: (array) List of lower-cased attributes [only if
1376     *                 'attributes' option is true].
1377     *   - delimiter: (string) The delimiter for the mailbox.
1378     *   - extended: (TODO) TODO [only if 'recursivematch' option is true and
1379     *               LIST-EXTENDED extension is supported on the server].
1380     *   - mailbox: (Horde_Imap_Client_Mailbox) The mailbox object.
1381     *   - status: (array) See status() [only if 'status' option is true].
1382     *
1383     * @throws Horde_Imap_Client_Exception
1384     */
1385    public function listMailboxes($pattern,
1386                                  $mode = Horde_Imap_Client::MBOX_ALL,
1387                                  array $options = array())
1388    {
1389        $this->login();
1390
1391        $pattern = is_array($pattern)
1392            ? array_unique($pattern)
1393            : array($pattern);
1394
1395        /* Prepare patterns. */
1396        $plist = array();
1397        foreach ($pattern as $val) {
1398            if ($val instanceof Horde_Imap_Client_Mailbox) {
1399                $val = $val->list_escape;
1400            }
1401            $plist[] = Horde_Imap_Client_Mailbox::get(preg_replace(
1402                array("/\*{2,}/", "/\%{2,}/"),
1403                array('*', '%'),
1404                Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($val)
1405            ), true);
1406        }
1407
1408        if (isset($options['special_use']) &&
1409            !$this->_capability('SPECIAL-USE')) {
1410            unset($options['special_use']);
1411        }
1412
1413        $ret = $this->_listMailboxes($plist, $mode, $options);
1414
1415        if (!empty($options['status']) &&
1416            !$this->_capability('LIST-STATUS')) {
1417            foreach ($this->status(array_keys($ret), $options['status']) as $key => $val) {
1418                $ret[$key]['status'] = $val;
1419            }
1420        }
1421
1422        if (empty($options['sort'])) {
1423            return $ret;
1424        }
1425
1426        $list_ob = new Horde_Imap_Client_Mailbox_List(empty($options['flat']) ? array_keys($ret) : $ret);
1427        $sorted = $list_ob->sort(array(
1428            'delimiter' => empty($options['sort_delimiter']) ? '.' : $options['sort_delimiter']
1429        ));
1430
1431        if (!empty($options['flat'])) {
1432            return $sorted;
1433        }
1434
1435        $out = array();
1436        foreach ($sorted as $val) {
1437            $out[$val] = $ret[$val];
1438        }
1439
1440        return $out;
1441    }
1442
1443    /**
1444     * Obtain a list of mailboxes matching a pattern.
1445     *
1446     * @param array $pattern  The mailbox search patterns
1447     *                        (Horde_Imap_Client_Mailbox objects).
1448     * @param integer $mode   Which mailboxes to return.
1449     * @param array $options  Additional options.
1450     *
1451     * @return array  See listMailboxes().
1452     *
1453     * @throws Horde_Imap_Client_Exception
1454     */
1455    abstract protected function _listMailboxes($pattern, $mode, $options);
1456
1457    /**
1458     * Obtain status information for a mailbox.
1459     *
1460     * @param mixed $mailbox  The mailbox(es) to query. Either a
1461     *                        Horde_Imap_Client_Mailbox object, a string
1462     *                        (UTF-8), or an array of objects/strings (since
1463     *                        2.10.0).
1464     * @param integer $flags  A bitmask of information requested from the
1465     *                        server. Allowed flags:
1466     * <pre>
1467     *   - Horde_Imap_Client::STATUS_MESSAGES
1468     *     Return key: messages
1469     *     Return format: (integer) The number of messages in the mailbox.
1470     *
1471     *   - Horde_Imap_Client::STATUS_RECENT
1472     *     Return key: recent
1473     *     Return format: (integer) The number of messages with the \Recent
1474     *                    flag set as currently reported in the mailbox
1475     *
1476     *   - Horde_Imap_Client::STATUS_RECENT_TOTAL
1477     *     Return key: recent_total
1478     *     Return format: (integer) The number of messages with the \Recent
1479     *                    flag set. This returns the total number of messages
1480     *                    that have been marked as recent in this mailbox
1481     *                    since the PHP process began. (since 2.12.0)
1482     *
1483     *   - Horde_Imap_Client::STATUS_UIDNEXT
1484     *     Return key: uidnext
1485     *     Return format: (integer) The next UID to be assigned in the
1486     *                    mailbox. Only returned if the server automatically
1487     *                    provides the data.
1488     *
1489     *   - Horde_Imap_Client::STATUS_UIDNEXT_FORCE
1490     *     Return key: uidnext
1491     *     Return format: (integer) The next UID to be assigned in the
1492     *                    mailbox. This option will always determine this
1493     *                    value, even if the server does not automatically
1494     *                    provide this data.
1495     *
1496     *   - Horde_Imap_Client::STATUS_UIDVALIDITY
1497     *     Return key: uidvalidity
1498     *     Return format: (integer) The unique identifier validity of the
1499     *                    mailbox.
1500     *
1501     *   - Horde_Imap_Client::STATUS_UNSEEN
1502     *     Return key: unseen
1503     *     Return format: (integer) The number of messages which do not have
1504     *                    the \Seen flag set.
1505     *
1506     *   - Horde_Imap_Client::STATUS_FIRSTUNSEEN
1507     *     Return key: firstunseen
1508     *     Return format: (integer) The sequence number of the first unseen
1509     *                    message in the mailbox.
1510     *
1511     *   - Horde_Imap_Client::STATUS_FLAGS
1512     *     Return key: flags
1513     *     Return format: (array) The list of defined flags in the mailbox
1514     *                    (all flags are in lowercase).
1515     *
1516     *   - Horde_Imap_Client::STATUS_PERMFLAGS
1517     *     Return key: permflags
1518     *     Return format: (array) The list of flags that a client can change
1519     *                    permanently (all flags are in lowercase).
1520     *
1521     *   - Horde_Imap_Client::STATUS_HIGHESTMODSEQ
1522     *     Return key: highestmodseq
1523     *     Return format: (integer) If the server supports the CONDSTORE
1524     *                    IMAP extension, this will be the highest
1525     *                    mod-sequence value of all messages in the mailbox.
1526     *                    Else 0 if CONDSTORE not available or the mailbox
1527     *                    does not support mod-sequences.
1528     *
1529     *   - Horde_Imap_Client::STATUS_SYNCMODSEQ
1530     *     Return key: syncmodseq
1531     *     Return format: (integer) If caching, and the server supports the
1532     *                    CONDSTORE IMAP extension, this is the cached
1533     *                    mod-sequence value of the mailbox when it was opened
1534     *                    for the first time in this access. Will be null if
1535     *                    not caching, CONDSTORE not available, or the mailbox
1536     *                    does not support mod-sequences.
1537     *
1538     *   - Horde_Imap_Client::STATUS_SYNCFLAGUIDS
1539     *     Return key: syncflaguids
1540     *     Return format: (Horde_Imap_Client_Ids) If caching, the server
1541     *                    supports the CONDSTORE IMAP extension, and the
1542     *                    mailbox contained cached data when opened for the
1543     *                    first time in this access, this is the list of UIDs
1544     *                    in which flags have changed since STATUS_SYNCMODSEQ.
1545     *
1546     *   - Horde_Imap_Client::STATUS_SYNCVANISHED
1547     *     Return key: syncvanished
1548     *     Return format: (Horde_Imap_Client_Ids) If caching, the server
1549     *                    supports the CONDSTORE IMAP extension, and the
1550     *                    mailbox contained cached data when opened for the
1551     *                    first time in this access, this is the list of UIDs
1552     *                    which have been deleted since STATUS_SYNCMODSEQ.
1553     *
1554     *   - Horde_Imap_Client::STATUS_UIDNOTSTICKY
1555     *     Return key: uidnotsticky
1556     *     Return format: (boolean) If the server supports the UIDPLUS IMAP
1557     *                    extension, and the queried mailbox does not support
1558     *                    persistent UIDs, this value will be true. In all
1559     *                    other cases, this value will be false.
1560     *
1561     *   - Horde_Imap_Client::STATUS_FORCE_REFRESH
1562     *     Normally, the status information will be cached for a given
1563     *     mailbox. Since most PHP requests are generally less than a second,
1564     *     this is fine. However, if your script is long running, the status
1565     *     information may not be up-to-date. Specifying this flag will ensure
1566     *     that the server is always polled for the current mailbox status
1567     *     before results are returned. (since 2.14.0)
1568     *
1569     *   - Horde_Imap_Client::STATUS_ALL (DEFAULT)
1570     *     Shortcut to return 'messages', 'recent', 'uidnext', 'uidvalidity',
1571     *     and 'unseen' values.
1572     * </ul>
1573     * @param array $opts     Additional options:
1574     * <pre>
1575     *   - sort: (boolean) If true, sort the list of mailboxes? (since 2.10.0)
1576     *           DEFAULT: Do not sort the list.
1577     *   - sort_delimiter: (string) If 'sort' is true, this is the delimiter
1578     *                     used to sort the mailboxes. (since 2.10.0)
1579     *                     DEFAULT: '.'
1580     * </pre>
1581     *
1582     * @return array  If $mailbox contains multiple mailboxes, an array with
1583     *                keys being the UTF-8 mailbox name and values as arrays
1584     *                containing the requested keys (see above).
1585     *                Otherwise, an array with keys as the requested keys (see
1586     *                above) and values as the key data.
1587     *
1588     * @throws Horde_Imap_Client_Exception
1589     */
1590    public function status($mailbox, $flags = Horde_Imap_Client::STATUS_ALL,
1591                           array $opts = array())
1592    {
1593        $opts = array_merge(array(
1594            'sort' => false,
1595            'sort_delimiter' => '.'
1596        ), $opts);
1597
1598        $this->login();
1599
1600        if (is_array($mailbox)) {
1601            if (empty($mailbox)) {
1602                return array();
1603            }
1604            $ret_array = true;
1605        } else {
1606            $mailbox = array($mailbox);
1607            $ret_array = false;
1608        }
1609
1610        $mlist = array_map(array('Horde_Imap_Client_Mailbox', 'get'), $mailbox);
1611
1612        $unselected_flags = array(
1613            'messages' => Horde_Imap_Client::STATUS_MESSAGES,
1614            'recent' => Horde_Imap_Client::STATUS_RECENT,
1615            'uidnext' => Horde_Imap_Client::STATUS_UIDNEXT,
1616            'uidvalidity' => Horde_Imap_Client::STATUS_UIDVALIDITY,
1617            'unseen' => Horde_Imap_Client::STATUS_UNSEEN
1618        );
1619
1620        if (!$this->statuscache) {
1621            $flags |= Horde_Imap_Client::STATUS_FORCE_REFRESH;
1622        }
1623
1624        if ($flags & Horde_Imap_Client::STATUS_ALL) {
1625            foreach ($unselected_flags as $val) {
1626                $flags |= $val;
1627            }
1628        }
1629
1630        $master = $ret = array();
1631
1632        /* Catch flags that are not supported. */
1633        if (($flags & Horde_Imap_Client::STATUS_HIGHESTMODSEQ) &&
1634            !$this->_capability()->isEnabled('CONDSTORE')) {
1635            $master['highestmodseq'] = 0;
1636            $flags &= ~Horde_Imap_Client::STATUS_HIGHESTMODSEQ;
1637        }
1638
1639        if (($flags & Horde_Imap_Client::STATUS_UIDNOTSTICKY) &&
1640            !$this->_capability('UIDPLUS')) {
1641            $master['uidnotsticky'] = false;
1642            $flags &= ~Horde_Imap_Client::STATUS_UIDNOTSTICKY;
1643        }
1644
1645        /* UIDNEXT return options. */
1646        if ($flags & Horde_Imap_Client::STATUS_UIDNEXT_FORCE) {
1647            $flags |= Horde_Imap_Client::STATUS_UIDNEXT;
1648        }
1649
1650        foreach ($mlist as $val) {
1651            $name = strval($val);
1652            $tmp_flags = $flags;
1653
1654            if ($val->equals($this->_selected)) {
1655                /* Check if already in mailbox. */
1656                $opened = true;
1657
1658                if ($flags & Horde_Imap_Client::STATUS_FORCE_REFRESH) {
1659                    $this->noop();
1660                }
1661            } else {
1662                /* A list of STATUS options (other than those handled directly
1663                 * below) that require the mailbox to be explicitly opened. */
1664                $opened = ($flags & Horde_Imap_Client::STATUS_FIRSTUNSEEN) ||
1665                    ($flags & Horde_Imap_Client::STATUS_FLAGS) ||
1666                    ($flags & Horde_Imap_Client::STATUS_PERMFLAGS) ||
1667                    ($flags & Horde_Imap_Client::STATUS_UIDNOTSTICKY) ||
1668                    /* Force mailboxes containing wildcards to be accessed via
1669                     * STATUS so that wildcards do not return a bunch of
1670                     * mailboxes in the LIST-STATUS response. */
1671                    (strpbrk($name, '*%') !== false);
1672            }
1673
1674            $ret[$name] = $master;
1675            $ptr = &$ret[$name];
1676
1677            /* STATUS_PERMFLAGS requires a read/write mailbox. */
1678            if ($flags & Horde_Imap_Client::STATUS_PERMFLAGS) {
1679                $this->openMailbox($val, Horde_Imap_Client::OPEN_READWRITE);
1680                $opened = true;
1681            }
1682
1683            /* Handle SYNC related return options. These require the mailbox
1684             * to be opened at least once. */
1685            if ($flags & Horde_Imap_Client::STATUS_SYNCMODSEQ) {
1686                $this->openMailbox($val);
1687                $ptr['syncmodseq'] = $this->_mailboxOb($val)->getStatus(Horde_Imap_Client::STATUS_SYNCMODSEQ);
1688                $tmp_flags &= ~Horde_Imap_Client::STATUS_SYNCMODSEQ;
1689                $opened = true;
1690            }
1691
1692            if ($flags & Horde_Imap_Client::STATUS_SYNCFLAGUIDS) {
1693                $this->openMailbox($val);
1694                $ptr['syncflaguids'] = $this->getIdsOb($this->_mailboxOb($val)->getStatus(Horde_Imap_Client::STATUS_SYNCFLAGUIDS));
1695                $tmp_flags &= ~Horde_Imap_Client::STATUS_SYNCFLAGUIDS;
1696                $opened = true;
1697            }
1698
1699            if ($flags & Horde_Imap_Client::STATUS_SYNCVANISHED) {
1700                $this->openMailbox($val);
1701                $ptr['syncvanished'] = $this->getIdsOb($this->_mailboxOb($val)->getStatus(Horde_Imap_Client::STATUS_SYNCVANISHED));
1702                $tmp_flags &= ~Horde_Imap_Client::STATUS_SYNCVANISHED;
1703                $opened = true;
1704            }
1705
1706            /* Handle RECENT_TOTAL option. */
1707            if ($flags & Horde_Imap_Client::STATUS_RECENT_TOTAL) {
1708                $this->openMailbox($val);
1709                $ptr['recent_total'] = $this->_mailboxOb($val)->getStatus(Horde_Imap_Client::STATUS_RECENT_TOTAL);
1710                $tmp_flags &= ~Horde_Imap_Client::STATUS_RECENT_TOTAL;
1711                $opened = true;
1712            }
1713
1714            if ($opened) {
1715                if ($tmp_flags) {
1716                    $tmp = $this->_status(array($val), $tmp_flags);
1717                    $ptr += reset($tmp);
1718                }
1719            } else {
1720                $to_process[] = $val;
1721            }
1722        }
1723
1724        if ($flags && !empty($to_process)) {
1725            if ((count($to_process) > 1) &&
1726                $this->_capability('LIST-STATUS')) {
1727                foreach ($this->listMailboxes($to_process, Horde_Imap_Client::MBOX_ALL, array('status' => $flags)) as $key => $val) {
1728                    if (isset($val['status'])) {
1729                        $ret[$key] += $val['status'];
1730                    }
1731                }
1732            } else {
1733                foreach ($this->_status($to_process, $flags) as $key => $val) {
1734                    $ret[$key] += $val;
1735                }
1736            }
1737        }
1738
1739        if (!$opts['sort'] || (count($ret) === 1)) {
1740            return $ret_array
1741                ? $ret
1742                : reset($ret);
1743        }
1744
1745        $list_ob = new Horde_Imap_Client_Mailbox_List(array_keys($ret));
1746        $sorted = $list_ob->sort(array(
1747            'delimiter' => $opts['sort_delimiter']
1748        ));
1749
1750        $out = array();
1751        foreach ($sorted as $val) {
1752            $out[$val] = $ret[$val];
1753        }
1754
1755        return $out;
1756    }
1757
1758    /**
1759     * Obtain status information for mailboxes.
1760     *
1761     * @param array $mboxes   The list of mailbox objects to query.
1762     * @param integer $flags  A bitmask of information requested from the
1763     *                        server.
1764     *
1765     * @return array  See array return for status().
1766     *
1767     * @throws Horde_Imap_Client_Exception
1768     */
1769    abstract protected function _status($mboxes, $flags);
1770
1771    /**
1772     * Perform a STATUS call on multiple mailboxes at the same time.
1773     *
1774     * This method leverages the LIST-EXTENDED and LIST-STATUS extensions on
1775     * the IMAP server to improve the efficiency of this operation.
1776     *
1777     * @deprecated  Use status() instead.
1778     *
1779     * @param array $mailboxes  The mailboxes to query. Either
1780     *                          Horde_Imap_Client_Mailbox objects, strings
1781     *                          (UTF-8), or a combination of the two.
1782     * @param integer $flags    See status().
1783     * @param array $opts       Additional options:
1784     *   - sort: (boolean) If true, sort the list of mailboxes?
1785     *           DEFAULT: Do not sort the list.
1786     *   - sort_delimiter: (string) If 'sort' is true, this is the delimiter
1787     *                     used to sort the mailboxes.
1788     *                     DEFAULT: '.'
1789     *
1790     * @return array  An array with the keys as the mailbox names (UTF-8) and
1791     *                the values as arrays with the requested keys (from the
1792     *                mask given in $flags).
1793     */
1794    public function statusMultiple($mailboxes,
1795                                   $flags = Horde_Imap_Client::STATUS_ALL,
1796                                   array $opts = array())
1797    {
1798        return $this->status($mailboxes, $flags, $opts);
1799    }
1800
1801    /**
1802     * Append message(s) to a mailbox.
1803     *
1804     * @param mixed $mailbox  The mailbox to append the message(s) to. Either
1805     *                        a Horde_Imap_Client_Mailbox object or a string
1806     *                        (UTF-8).
1807     * @param array $data     The message data to append, along with
1808     *                        additional options. An array of arrays with
1809     *                        each embedded array having the following
1810     *                        entries:
1811     * <pre>
1812     *   - data: (mixed) The data to append. If a string or a stream resource,
1813     *           this will be used as the entire contents of a single message.
1814     *           If an array, will catenate all given parts into a single
1815     *           message. This array contains one or more arrays with
1816     *           two keys:
1817     *     - t: (string) Either 'url' or 'text'.
1818     *     - v: (mixed) If 't' is 'url', this is the IMAP URL to the message
1819     *          part to append. If 't' is 'text', this is either a string or
1820     *          resource representation of the message part data.
1821     *     DEFAULT: NONE (entry is MANDATORY)
1822     *   - flags: (array) An array of flags/keywords to set on the appended
1823     *            message.
1824     *            DEFAULT: Only the \Recent flag is set.
1825     *   - internaldate: (DateTime) The internaldate to set for the appended
1826     *                   message.
1827     *                   DEFAULT: internaldate will be the same date as when
1828     *                   the message was appended.
1829     * </pre>
1830     * @param array $options  Additonal options:
1831     * <pre>
1832     *   - create: (boolean) Try to create $mailbox if it does not exist?
1833     *             DEFAULT: No.
1834     * </pre>
1835     *
1836     * @return Horde_Imap_Client_Ids  The UIDs of the appended messages.
1837     *
1838     * @throws Horde_Imap_Client_Exception
1839     */
1840    public function append($mailbox, $data, array $options = array())
1841    {
1842        $this->login();
1843
1844        $mailbox = Horde_Imap_Client_Mailbox::get($mailbox);
1845
1846        $ret = $this->_append($mailbox, $data, $options);
1847
1848        if ($ret instanceof Horde_Imap_Client_Ids) {
1849            return $ret;
1850        }
1851
1852        $uids = $this->getIdsOb();
1853
1854        foreach ($data as $val) {
1855            if (is_resource($val['data'])) {
1856                rewind($val['data']);
1857            }
1858
1859            $uids->add($this->_getUidByMessageId(
1860                $mailbox,
1861                Horde_Mime_Headers::parseHeaders($val['data'])->getHeader('Message-ID')
1862            ));
1863        }
1864
1865        return $uids;
1866    }
1867
1868    /**
1869     * Append message(s) to a mailbox.
1870     *
1871     * @param Horde_Imap_Client_Mailbox $mailbox  The mailbox to append the
1872     *                                            message(s) to.
1873     * @param array $data                         The message data.
1874     * @param array $options                      Additional options.
1875     *
1876     * @return mixed  A Horde_Imap_Client_Ids object containing the UIDs of
1877     *                the appended messages (if server supports UIDPLUS
1878     *                extension) or true.
1879     *
1880     * @throws Horde_Imap_Client_Exception
1881     */
1882    abstract protected function _append(Horde_Imap_Client_Mailbox $mailbox,
1883                                        $data, $options);
1884
1885    /**
1886     * Request a checkpoint of the currently selected mailbox (RFC 3501
1887     * [6.4.1]).
1888     *
1889     * @throws Horde_Imap_Client_Exception
1890     */
1891    public function check()
1892    {
1893        // CHECK only useful if we are already authenticated.
1894        if ($this->_isAuthenticated) {
1895            $this->_check();
1896        }
1897    }
1898
1899    /**
1900     * Request a checkpoint of the currently selected mailbox.
1901     *
1902     * @throws Horde_Imap_Client_Exception
1903     */
1904    abstract protected function _check();
1905
1906    /**
1907     * Close the connection to the currently selected mailbox, optionally
1908     * expunging all deleted messages (RFC 3501 [6.4.2]).
1909     *
1910     * @param array $options  Additional options:
1911     *   - expunge: (boolean) Expunge all messages flagged as deleted?
1912     *              DEFAULT: No
1913     *
1914     * @throws Horde_Imap_Client_Exception
1915     */
1916    public function close(array $options = array())
1917    {
1918        // This check catches the non-logged in case.
1919        if (is_null($this->_selected)) {
1920            return;
1921        }
1922
1923        /* If we are caching, search for deleted messages. */
1924        if (!empty($options['expunge']) && $this->_initCache(true)) {
1925            /* Make sure mailbox is read-write to expunge. */
1926            $this->openMailbox($this->_selected, Horde_Imap_Client::OPEN_READWRITE);
1927            if ($this->_mode == Horde_Imap_Client::OPEN_READONLY) {
1928                throw new Horde_Imap_Client_Exception(
1929                    Horde_Imap_Client_Translation::r("Cannot expunge read-only mailbox."),
1930                    Horde_Imap_Client_Exception::MAILBOX_READONLY
1931                );
1932            }
1933
1934            $search_query = new Horde_Imap_Client_Search_Query();
1935            $search_query->flag(Horde_Imap_Client::FLAG_DELETED, true);
1936            $search_res = $this->search($this->_selected, $search_query);
1937            $mbox = $this->_selected;
1938        } else {
1939            $search_res = null;
1940        }
1941
1942        $this->_close($options);
1943        $this->_selected = null;
1944        $this->_mode = 0;
1945
1946        if (!is_null($search_res)) {
1947            $this->_deleteMsgs($mbox, $search_res['match']);
1948        }
1949    }
1950
1951    /**
1952     * Close the connection to the currently selected mailbox, optionally
1953     * expunging all deleted messages (RFC 3501 [6.4.2]).
1954     *
1955     * @param array $options  Additional options.
1956     *
1957     * @throws Horde_Imap_Client_Exception
1958     */
1959    abstract protected function _close($options);
1960
1961    /**
1962     * Expunge deleted messages from the given mailbox.
1963     *
1964     * @param mixed $mailbox  The mailbox to expunge. Either a
1965     *                        Horde_Imap_Client_Mailbox object or a string
1966     *                        (UTF-8).
1967     * @param array $options  Additional options:
1968     *   - delete: (boolean) If true, will flag all messages in 'ids' as
1969     *             deleted (since 2.10.0).
1970     *             DEFAULT: false
1971     *   - ids: (Horde_Imap_Client_Ids) A list of messages to expunge. These
1972     *          messages must already be flagged as deleted (unless 'delete'
1973     *          is true).
1974     *          DEFAULT: All messages marked as deleted will be expunged.
1975     *   - list: (boolean) If true, returns the list of expunged messages
1976     *           (UIDs only).
1977     *           DEFAULT: false
1978     *
1979     * @return Horde_Imap_Client_Ids  If 'list' option is true, returns the
1980     *                                UID list of expunged messages.
1981     *
1982     * @throws Horde_Imap_Client_Exception
1983     */
1984    public function expunge($mailbox, array $options = array())
1985    {
1986        // Open mailbox call will handle the login.
1987        $this->openMailbox($mailbox, Horde_Imap_Client::OPEN_READWRITE);
1988
1989        /* Don't expunge if the mailbox is readonly. */
1990        if ($this->_mode == Horde_Imap_Client::OPEN_READONLY) {
1991            throw new Horde_Imap_Client_Exception(
1992                Horde_Imap_Client_Translation::r("Cannot expunge read-only mailbox."),
1993                Horde_Imap_Client_Exception::MAILBOX_READONLY
1994            );
1995        }
1996
1997        if (empty($options['ids'])) {
1998            $options['ids'] = $this->getIdsOb(Horde_Imap_Client_Ids::ALL);
1999        } elseif ($options['ids']->isEmpty()) {
2000            return $this->getIdsOb();
2001        }
2002
2003        return $this->_expunge($options);
2004    }
2005
2006    /**
2007     * Expunge all deleted messages from the given mailbox.
2008     *
2009     * @param array $options  Additional options.
2010     *
2011     * @return Horde_Imap_Client_Ids  If 'list' option is true, returns the
2012     *                                list of expunged messages.
2013     *
2014     * @throws Horde_Imap_Client_Exception
2015     */
2016    abstract protected function _expunge($options);
2017
2018    /**
2019     * Search a mailbox.
2020     *
2021     * @param mixed $mailbox                         The mailbox to search.
2022     *                                               Either a
2023     *                                               Horde_Imap_Client_Mailbox
2024     *                                               object or a string
2025     *                                               (UTF-8).
2026     * @param Horde_Imap_Client_Search_Query $query  The search query.
2027     *                                               Defaults to an ALL
2028     *                                               search.
2029     * @param array $options                         Additional options:
2030     * <pre>
2031     *   - nocache: (boolean) Don't cache the results.
2032     *              DEFAULT: false (results cached, if possible)
2033     *   - partial: (mixed) The range of results to return (message sequence
2034     *              numbers) Only a single range is supported (represented by
2035     *              the minimum and maximum values contained in the range
2036     *              given).
2037     *              DEFAULT: All messages are returned.
2038     *   - results: (array) The data to return. Consists of zero or more of
2039     *              the following flags:
2040     *     - Horde_Imap_Client::SEARCH_RESULTS_COUNT
2041     *     - Horde_Imap_Client::SEARCH_RESULTS_MATCH (DEFAULT)
2042     *     - Horde_Imap_Client::SEARCH_RESULTS_MAX
2043     *     - Horde_Imap_Client::SEARCH_RESULTS_MIN
2044     *     - Horde_Imap_Client::SEARCH_RESULTS_SAVE
2045     *     - Horde_Imap_Client::SEARCH_RESULTS_RELEVANCY
2046     *   - sequence: (boolean) If true, returns an array of sequence numbers.
2047     *               DEFAULT: Returns an array of UIDs
2048     *   - sort: (array) Sort the returned list of messages. Multiple sort
2049     *           criteria can be specified. Any sort criteria can be sorted in
2050     *           reverse order (instead of the default ascending order) by
2051     *           adding a Horde_Imap_Client::SORT_REVERSE element to the array
2052     *           directly before adding the sort element. The following sort
2053     *           criteria are available:
2054     *     - Horde_Imap_Client::SORT_ARRIVAL
2055     *     - Horde_Imap_Client::SORT_CC
2056     *     - Horde_Imap_Client::SORT_DATE
2057     *     - Horde_Imap_Client::SORT_DISPLAYFROM
2058     *       On servers that don't support SORT=DISPLAY, this criteria will
2059     *       fallback to doing client-side sorting.
2060     *     - Horde_Imap_Client::SORT_DISPLAYFROM_FALLBACK
2061     *       On servers that don't support SORT=DISPLAY, this criteria will
2062     *       fallback to Horde_Imap_Client::SORT_FROM [since 2.4.0].
2063     *     - Horde_Imap_Client::SORT_DISPLAYTO
2064     *       On servers that don't support SORT=DISPLAY, this criteria will
2065     *       fallback to doing client-side sorting.
2066     *     - Horde_Imap_Client::SORT_DISPLAYTO_FALLBACK
2067     *       On servers that don't support SORT=DISPLAY, this criteria will
2068     *       fallback to Horde_Imap_Client::SORT_TO [since 2.4.0].
2069     *     - Horde_Imap_Client::SORT_FROM
2070     *     - Horde_Imap_Client::SORT_SEQUENCE
2071     *     - Horde_Imap_Client::SORT_SIZE
2072     *     - Horde_Imap_Client::SORT_SUBJECT
2073     *     - Horde_Imap_Client::SORT_TO
2074     *
2075     *     [On servers that support SEARCH=FUZZY, this criteria is also
2076     *     available:]
2077     *     - Horde_Imap_Client::SORT_RELEVANCY
2078     * </pre>
2079     *
2080     * @return array  An array with the following keys:
2081     * <pre>
2082     *   - count: (integer) The number of messages that match the search
2083     *            criteria. Always returned.
2084     *   - match: (Horde_Imap_Client_Ids) The IDs that match $criteria, sorted
2085     *            if the 'sort' modifier was set. Returned if
2086     *            Horde_Imap_Client::SEARCH_RESULTS_MATCH is set.
2087     *   - max: (integer) The UID (default) or message sequence number (if
2088     *          'sequence' is true) of the highest message that satisifies
2089     *          $criteria. Returns null if no matches found. Returned if
2090     *          Horde_Imap_Client::SEARCH_RESULTS_MAX is set.
2091     *   - min: (integer) The UID (default) or message sequence number (if
2092     *          'sequence' is true) of the lowest message that satisifies
2093     *          $criteria. Returns null if no matches found. Returned if
2094     *          Horde_Imap_Client::SEARCH_RESULTS_MIN is set.
2095     *   - modseq: (integer) The highest mod-sequence for all messages being
2096     *            returned. Returned if 'sort' is false, the search query
2097     *            includes a MODSEQ command, and the server supports the
2098     *            CONDSTORE IMAP extension.
2099     *   - relevancy: (array) The list of relevancy scores. Returned if
2100     *                Horde_Imap_Client::SEARCH_RESULTS_RELEVANCY is set and
2101     *                the server supports FUZZY search matching.
2102     *   - save: (boolean) Whether the search results were saved. Returned if
2103     *           Horde_Imap_Client::SEARCH_RESULTS_SAVE is set.
2104     * </pre>
2105     *
2106     * @throws Horde_Imap_Client_Exception
2107     */
2108    public function search($mailbox, $query = null, array $options = array())
2109    {
2110        $this->login();
2111
2112        if (empty($options['results'])) {
2113            $options['results'] = array(
2114                Horde_Imap_Client::SEARCH_RESULTS_MATCH,
2115                Horde_Imap_Client::SEARCH_RESULTS_COUNT
2116            );
2117        } elseif (!in_array(Horde_Imap_Client::SEARCH_RESULTS_COUNT, $options['results'])) {
2118            $options['results'][] = Horde_Imap_Client::SEARCH_RESULTS_COUNT;
2119        }
2120
2121        // Default to an ALL search.
2122        if (is_null($query)) {
2123            $query = new Horde_Imap_Client_Search_Query();
2124        }
2125
2126        // Check for SEARCHRES support.
2127        if ((($pos = array_search(Horde_Imap_Client::SEARCH_RESULTS_SAVE, $options['results'])) !== false) &&
2128            !$this->_capability('SEARCHRES')) {
2129            unset($options['results'][$pos]);
2130        }
2131
2132        // Check for SORT-related options.
2133        if (!empty($options['sort'])) {
2134            foreach ($options['sort'] as $key => $val) {
2135                switch ($val) {
2136                case Horde_Imap_Client::SORT_DISPLAYFROM_FALLBACK:
2137                    $options['sort'][$key] = $this->_capability('SORT', 'DISPLAY')
2138                        ? Horde_Imap_Client::SORT_DISPLAYFROM
2139                        : Horde_Imap_Client::SORT_FROM;
2140                    break;
2141
2142                case Horde_Imap_Client::SORT_DISPLAYTO_FALLBACK:
2143                    $options['sort'][$key] = $this->_capability('SORT', 'DISPLAY')
2144                        ? Horde_Imap_Client::SORT_DISPLAYTO
2145                        : Horde_Imap_Client::SORT_TO;
2146                    break;
2147                }
2148            }
2149        }
2150
2151        /* Default search results. */
2152        $default_ret = array(
2153            'count' => 0,
2154            'match' => $this->getIdsOb(),
2155            'max' => null,
2156            'min' => null,
2157            'relevancy' => array()
2158        );
2159
2160        /* Build search query. */
2161        $squery = $query->build($this);
2162
2163        /* Check for query contents. If empty, this means that the query
2164         * object has identified that this query can NEVER return any results.
2165         * Immediately return now. */
2166        if (!count($squery['query'])) {
2167            return $default_ret;
2168        }
2169
2170        // Check for supported charset.
2171        if (!is_null($squery['charset']) &&
2172            ($this->search_charset->query($squery['charset'], true) === false)) {
2173            foreach ($this->search_charset->charsets as $val) {
2174                try {
2175                    $new_query = clone $query;
2176                    $new_query->charset($val);
2177                    break;
2178                } catch (Horde_Imap_Client_Exception_SearchCharset $e) {
2179                    unset($new_query);
2180                }
2181            }
2182
2183            if (!isset($new_query)) {
2184                throw $e;
2185            }
2186
2187            $query = $new_query;
2188            $squery = $query->build($this);
2189        }
2190
2191        // Store query in $options array to pass to child method.
2192        $options['_query'] = $squery;
2193
2194        /* RFC 6203: MUST NOT request relevancy results if we are not using
2195         * FUZZY searching. */
2196        if (in_array(Horde_Imap_Client::SEARCH_RESULTS_RELEVANCY, $options['results']) &&
2197            !in_array('SEARCH=FUZZY', $squery['exts_used'])) {
2198            throw new InvalidArgumentException('Cannot specify RELEVANCY results if not doing a FUZZY search.');
2199        }
2200
2201        /* Check for partial matching. */
2202        if (!empty($options['partial'])) {
2203            $pids = $this->getIdsOb($options['partial'], true)->range_string;
2204            if (!strlen($pids)) {
2205                throw new InvalidArgumentException('Cannot specify empty sequence range for a PARTIAL search.');
2206            }
2207
2208            if (strpos($pids, ':') === false) {
2209                $pids .= ':' . $pids;
2210            }
2211
2212            $options['partial'] = $pids;
2213        }
2214
2215        /* Optimization - if query is just for a count of either RECENT or
2216         * ALL messages, we can send status information instead. Can't
2217         * optimize with unseen queries because we may cause an infinite loop
2218         * between here and the status() call. */
2219        if ((count($options['results']) === 1) &&
2220            (reset($options['results']) == Horde_Imap_Client::SEARCH_RESULTS_COUNT)) {
2221            switch ($squery['query']) {
2222            case 'ALL':
2223                $ret = $this->status($mailbox, Horde_Imap_Client::STATUS_MESSAGES);
2224                return array('count' => $ret['messages']);
2225
2226            case 'RECENT':
2227                $ret = $this->status($mailbox, Horde_Imap_Client::STATUS_RECENT);
2228                return array('count' => $ret['recent']);
2229            }
2230        }
2231
2232        $this->openMailbox($mailbox, Horde_Imap_Client::OPEN_AUTO);
2233
2234        /* Take advantage of search result caching.  If CONDSTORE available,
2235         * we can cache all queries and invalidate the cache when the MODSEQ
2236         * changes. If CONDSTORE not available, we can only store queries
2237         * that don't involve flags. We store results by hashing the options
2238         * array. */
2239        $cache = null;
2240        if (empty($options['nocache']) &&
2241            $this->_initCache(true) &&
2242            ($this->_capability()->isEnabled('CONDSTORE') ||
2243             !$query->flagSearch())) {
2244            $cache = $this->_getSearchCache('search', $options);
2245            if (isset($cache['data'])) {
2246                if (isset($cache['data']['match'])) {
2247                    $cache['data']['match'] = $this->getIdsOb($cache['data']['match']);
2248                }
2249                return $cache['data'];
2250            }
2251        }
2252
2253        /* Optimization: Catch when there are no messages in a mailbox. */
2254        $status_res = $this->status($this->_selected, Horde_Imap_Client::STATUS_MESSAGES | Horde_Imap_Client::STATUS_HIGHESTMODSEQ);
2255        if ($status_res['messages'] ||
2256            in_array(Horde_Imap_Client::SEARCH_RESULTS_SAVE, $options['results'])) {
2257            /* RFC 7162 [3.1.2.2] - trying to do a MODSEQ SEARCH on a mailbox
2258             * that doesn't support it will return BAD. */
2259            if (in_array('CONDSTORE', $squery['exts']) &&
2260                !$this->_mailboxOb()->getStatus(Horde_Imap_Client::STATUS_HIGHESTMODSEQ)) {
2261                throw new Horde_Imap_Client_Exception(
2262                    Horde_Imap_Client_Translation::r("Mailbox does not support mod-sequences."),
2263                    Horde_Imap_Client_Exception::MBOXNOMODSEQ
2264                );
2265            }
2266
2267            $ret = $this->_search($query, $options);
2268        } else {
2269            $ret = $default_ret;
2270            if (isset($status_res['highestmodseq'])) {
2271                $ret['modseq'] = $status_res['highestmodseq'];
2272            }
2273        }
2274
2275        if ($cache) {
2276            $save = $ret;
2277            if (isset($save['match'])) {
2278                $save['match'] = strval($ret['match']);
2279            }
2280            $this->_setSearchCache($save, $cache);
2281        }
2282
2283        return $ret;
2284    }
2285
2286    /**
2287     * Search a mailbox.
2288     *
2289     * @param object $query   The search query.
2290     * @param array $options  Additional options. The '_query' key contains
2291     *                        the value of $query->build().
2292     *
2293     * @return Horde_Imap_Client_Ids  An array of IDs.
2294     *
2295     * @throws Horde_Imap_Client_Exception
2296     */
2297    abstract protected function _search($query, $options);
2298
2299    /**
2300     * Set the comparator to use for searching/sorting (RFC 5255).
2301     *
2302     * @param string $comparator  The comparator string (see RFC 4790 [3.1] -
2303     *                            "collation-id" - for format). The reserved
2304     *                            string 'default' can be used to select
2305     *                            the default comparator.
2306     *
2307     * @throws Horde_Imap_Client_Exception
2308     * @throws Horde_Imap_Client_Exception_NoSupportExtension
2309     */
2310    public function setComparator($comparator = null)
2311    {
2312        $comp = is_null($comparator)
2313            ? $this->getParam('comparator')
2314            : $comparator;
2315        if (is_null($comp)) {
2316            return;
2317        }
2318
2319        $this->login();
2320
2321        if (!$this->_capability('I18NLEVEL', '2')) {
2322            throw new Horde_Imap_Client_Exception_NoSupportExtension(
2323                'I18NLEVEL',
2324                'The IMAP server does not support changing SEARCH/SORT comparators.'
2325            );
2326        }
2327
2328        $this->_setComparator($comp);
2329    }
2330
2331    /**
2332     * Set the comparator to use for searching/sorting (RFC 5255).
2333     *
2334     * @param string $comparator  The comparator string (see RFC 4790 [3.1] -
2335     *                            "collation-id" - for format). The reserved
2336     *                            string 'default' can be used to select
2337     *                            the default comparator.
2338     *
2339     * @throws Horde_Imap_Client_Exception
2340     */
2341    abstract protected function _setComparator($comparator);
2342
2343    /**
2344     * Get the comparator used for searching/sorting (RFC 5255).
2345     *
2346     * @return mixed  Null if the default comparator is being used, or an
2347     *                array of comparator information (see RFC 5255 [4.8]).
2348     *
2349     * @throws Horde_Imap_Client_Exception
2350     */
2351    public function getComparator()
2352    {
2353        $this->login();
2354
2355        return $this->_capability('I18NLEVEL', '2')
2356            ? $this->_getComparator()
2357            : null;
2358    }
2359
2360    /**
2361     * Get the comparator used for searching/sorting (RFC 5255).
2362     *
2363     * @return mixed  Null if the default comparator is being used, or an
2364     *                array of comparator information (see RFC 5255 [4.8]).
2365     *
2366     * @throws Horde_Imap_Client_Exception
2367     */
2368    abstract protected function _getComparator();
2369
2370    /**
2371     * Thread sort a given list of messages (RFC 5256).
2372     *
2373     * @param mixed $mailbox  The mailbox to query. Either a
2374     *                        Horde_Imap_Client_Mailbox object or a string
2375     *                        (UTF-8).
2376     * @param array $options  Additional options:
2377     * <pre>
2378     *   - criteria: (mixed) The following thread criteria are available:
2379     *     - Horde_Imap_Client::THREAD_ORDEREDSUBJECT
2380     *     - Horde_Imap_Client::THREAD_REFERENCES
2381     *     - Horde_Imap_Client::THREAD_REFS
2382     *       Other algorithms can be explicitly specified by passing the IMAP
2383     *       thread algorithm in as a string value.
2384     *     DEFAULT: Horde_Imap_Client::THREAD_ORDEREDSUBJECT
2385     *   - search: (Horde_Imap_Client_Search_Query) The search query.
2386     *             DEFAULT: All messages in mailbox included in thread sort.
2387     *   - sequence: (boolean) If true, each message is stored and referred to
2388     *               by its message sequence number.
2389     *               DEFAULT: Stored/referred to by UID.
2390     * </pre>
2391     *
2392     * @return Horde_Imap_Client_Data_Thread  A thread data object.
2393     *
2394     * @throws Horde_Imap_Client_Exception
2395     */
2396    public function thread($mailbox, array $options = array())
2397    {
2398        // Open mailbox call will handle the login.
2399        $this->openMailbox($mailbox, Horde_Imap_Client::OPEN_AUTO);
2400
2401        /* Take advantage of search result caching.  If CONDSTORE available,
2402         * we can cache all queries and invalidate the cache when the MODSEQ
2403         * changes. If CONDSTORE not available, we can only store queries
2404         * that don't involve flags. See search() for similar caching. */
2405        $cache = null;
2406        if ($this->_initCache(true) &&
2407            ($this->_capability()->isEnabled('CONDSTORE') ||
2408             empty($options['search']) ||
2409             !$options['search']->flagSearch())) {
2410            $cache = $this->_getSearchCache('thread', $options);
2411            if (isset($cache['data']) &&
2412                ($cache['data'] instanceof Horde_Imap_Client_Data_Thread)) {
2413                return $cache['data'];
2414            }
2415        }
2416
2417        $status_res = $this->status($this->_selected, Horde_Imap_Client::STATUS_MESSAGES);
2418
2419        $ob = $status_res['messages']
2420            ? $this->_thread($options)
2421            : new Horde_Imap_Client_Data_Thread(array(), empty($options['sequence']) ? 'uid' : 'sequence');
2422
2423        if ($cache) {
2424            $this->_setSearchCache($ob, $cache);
2425        }
2426
2427        return $ob;
2428    }
2429
2430    /**
2431     * Thread sort a given list of messages (RFC 5256).
2432     *
2433     * @param array $options  Additional options. See thread().
2434     *
2435     * @return Horde_Imap_Client_Data_Thread  A thread data object.
2436     *
2437     * @throws Horde_Imap_Client_Exception
2438     */
2439    abstract protected function _thread($options);
2440
2441    /**
2442     * Fetch message data (see RFC 3501 [6.4.5]).
2443     *
2444     * @param mixed $mailbox                        The mailbox to search.
2445     *                                              Either a
2446     *                                              Horde_Imap_Client_Mailbox
2447     *                                              object or a string (UTF-8).
2448     * @param Horde_Imap_Client_Fetch_Query $query  Fetch query object.
2449     * @param array $options                        Additional options:
2450     *   - changedsince: (integer) Only return messages that have a
2451     *                   mod-sequence larger than this value. This option
2452     *                   requires the CONDSTORE IMAP extension (if not present,
2453     *                   this value is ignored). Additionally, the mailbox
2454     *                   must support mod-sequences or an exception will be
2455     *                   thrown. If valid, this option implicity adds the
2456     *                   mod-sequence fetch criteria to the fetch command.
2457     *                   DEFAULT: Mod-sequence values are ignored.
2458     *   - exists: (boolean) Ensure that all ids returned exist on the server.
2459     *             If false, the list of ids returned in the results object
2460     *             is not guaranteed to reflect the current state of the
2461     *             remote mailbox.
2462     *             DEFAULT: false
2463     *   - ids: (Horde_Imap_Client_Ids) A list of messages to fetch data from.
2464     *          DEFAULT: All messages in $mailbox will be fetched.
2465     *   - nocache: (boolean) If true, will not cache the results (previously
2466     *              cached data will still be used to generate results) [since
2467     *              2.8.0].
2468     *              DEFAULT: false
2469     *
2470     * @return Horde_Imap_Client_Fetch_Results  A results object.
2471     *
2472     * @throws Horde_Imap_Client_Exception
2473     * @throws Horde_Imap_Client_Exception_NoSupportExtension
2474     */
2475    public function fetch($mailbox, $query, array $options = array())
2476    {
2477        try {
2478            $ret = $this->_fetchWrapper($mailbox, $query, $options);
2479            unset($this->_temp['fetch_nocache']);
2480            return $ret;
2481        } catch (Exception $e) {
2482            unset($this->_temp['fetch_nocache']);
2483            throw $e;
2484        }
2485    }
2486
2487    /**
2488     * Wrapper for fetch() to allow internal state to be reset on exception.
2489     *
2490     * @internal
2491     * @see fetch()
2492     */
2493    private function _fetchWrapper($mailbox, $query, $options)
2494    {
2495        $this->login();
2496
2497        $query = clone $query;
2498
2499        $cache_array = $header_cache = $new_query = array();
2500
2501        if (empty($options['ids'])) {
2502            $options['ids'] = $this->getIdsOb(Horde_Imap_Client_Ids::ALL);
2503        } elseif ($options['ids']->isEmpty()) {
2504            return new Horde_Imap_Client_Fetch_Results($this->_fetchDataClass);
2505        } elseif ($options['ids']->search_res &&
2506                  !$this->_capability('SEARCHRES')) {
2507            /* SEARCHRES requires server support. */
2508            throw new Horde_Imap_Client_Exception_NoSupportExtension('SEARCHRES');
2509        }
2510
2511        $this->openMailbox($mailbox, Horde_Imap_Client::OPEN_AUTO);
2512        $mbox_ob = $this->_mailboxOb();
2513
2514        if (!empty($options['nocache'])) {
2515            $this->_temp['fetch_nocache'] = true;
2516        }
2517
2518        $cf = $this->_initCache(true)
2519            ? $this->_cacheFields()
2520            : array();
2521
2522        if (!empty($cf)) {
2523            /* If using cache, we store by UID so we need to return UIDs. */
2524            $query->uid();
2525        }
2526
2527        $modseq_check = !empty($options['changedsince']);
2528        if ($query->contains(Horde_Imap_Client::FETCH_MODSEQ)) {
2529            if (!$this->_capability()->isEnabled('CONDSTORE')) {
2530                unset($query[Horde_Imap_Client::FETCH_MODSEQ]);
2531            } elseif (empty($options['changedsince'])) {
2532                $modseq_check = true;
2533            }
2534        }
2535
2536        if ($modseq_check &&
2537            !$mbox_ob->getStatus(Horde_Imap_Client::STATUS_HIGHESTMODSEQ)) {
2538            /* RFC 7162 [3.1.2.2] - trying to do a MODSEQ FETCH on a mailbox
2539             * that doesn't support it will return BAD. */
2540            throw new Horde_Imap_Client_Exception(
2541                Horde_Imap_Client_Translation::r("Mailbox does not support mod-sequences."),
2542                Horde_Imap_Client_Exception::MBOXNOMODSEQ
2543            );
2544        }
2545
2546        /* Determine if caching is available and if anything in $query is
2547         * cacheable. */
2548        foreach ($cf as $k => $v) {
2549            if (isset($query[$k])) {
2550                switch ($k) {
2551                case Horde_Imap_Client::FETCH_ENVELOPE:
2552                case Horde_Imap_Client::FETCH_FLAGS:
2553                case Horde_Imap_Client::FETCH_IMAPDATE:
2554                case Horde_Imap_Client::FETCH_SIZE:
2555                case Horde_Imap_Client::FETCH_STRUCTURE:
2556                    $cache_array[$k] = $v;
2557                    break;
2558
2559                case Horde_Imap_Client::FETCH_HEADERS:
2560                    $this->_temp['headers_caching'] = array();
2561
2562                    foreach ($query[$k] as $key => $val) {
2563                        /* Only cache if directly requested.  Iterate through
2564                         * requests to ensure at least one can be cached. */
2565                        if (!empty($val['cache']) && !empty($val['peek'])) {
2566                            $cache_array[$k] = $v;
2567                            ksort($val);
2568                            $header_cache[$key] = hash('md5', serialize($val));
2569                        }
2570                    }
2571                    break;
2572                }
2573            }
2574        }
2575
2576        $ret = new Horde_Imap_Client_Fetch_Results(
2577            $this->_fetchDataClass,
2578            $options['ids']->sequence ? Horde_Imap_Client_Fetch_Results::SEQUENCE : Horde_Imap_Client_Fetch_Results::UID
2579        );
2580
2581        /* If nothing is cacheable, we can do a straight search. */
2582        if (empty($cache_array)) {
2583            $options['_query'] = $query;
2584            $this->_fetch($ret, array($options));
2585            return $ret;
2586        }
2587
2588        $cs_ret = empty($options['changedsince'])
2589            ? null
2590            : clone $ret;
2591
2592        /* Convert special searches to UID lists and create mapping. */
2593        $ids = $this->resolveIds(
2594            $this->_selected,
2595            $options['ids'],
2596            empty($options['exists']) ? 1 : 2
2597        );
2598
2599        /* Add non-user settable cache fields. */
2600        $cache_array[Horde_Imap_Client::FETCH_DOWNGRADED] = self::CACHE_DOWNGRADED;
2601
2602        /* Get the cached values. */
2603        $data = $this->_cache->get(
2604            $this->_selected,
2605            $ids->ids,
2606            array_values($cache_array),
2607            $mbox_ob->getStatus(Horde_Imap_Client::STATUS_UIDVALIDITY)
2608        );
2609
2610        /* Build a list of what we still need. */
2611        $map = array_flip($mbox_ob->map->map);
2612        $sequence = $options['ids']->sequence;
2613        foreach ($ids as $uid) {
2614            $crit = clone $query;
2615
2616            if ($sequence) {
2617                if (!isset($map[$uid])) {
2618                    continue;
2619                }
2620                $entry_idx = $map[$uid];
2621            } else {
2622                $entry_idx = $uid;
2623                unset($crit[Horde_Imap_Client::FETCH_UID]);
2624            }
2625
2626            $entry = $ret->get($entry_idx);
2627
2628            if (isset($map[$uid])) {
2629                $entry->setSeq($map[$uid]);
2630                unset($crit[Horde_Imap_Client::FETCH_SEQ]);
2631            }
2632
2633            $entry->setUid($uid);
2634
2635            foreach ($cache_array as $key => $cid) {
2636                switch ($key) {
2637                case Horde_Imap_Client::FETCH_DOWNGRADED:
2638                    if (!empty($data[$uid][$cid])) {
2639                        $entry->setDowngraded(true);
2640                    }
2641                    break;
2642
2643                case Horde_Imap_Client::FETCH_ENVELOPE:
2644                    if (isset($data[$uid][$cid]) &&
2645                        ($data[$uid][$cid] instanceof Horde_Imap_Client_Data_Envelope)) {
2646                        $entry->setEnvelope($data[$uid][$cid]);
2647                        unset($crit[$key]);
2648                    }
2649                    break;
2650
2651                case Horde_Imap_Client::FETCH_FLAGS:
2652                    if (isset($data[$uid][$cid]) &&
2653                        is_array($data[$uid][$cid])) {
2654                        $entry->setFlags($data[$uid][$cid]);
2655                        unset($crit[$key]);
2656                    }
2657                    break;
2658
2659                case Horde_Imap_Client::FETCH_HEADERS:
2660                    foreach ($header_cache as $hkey => $hval) {
2661                        if (isset($data[$uid][$cid][$hval])) {
2662                            /* We have found a cached entry with the same
2663                             * MD5 sum. */
2664                            $entry->setHeaders($hkey, $data[$uid][$cid][$hval]);
2665                            $crit->remove($key, $hkey);
2666                        } else {
2667                            $this->_temp['headers_caching'][$hkey] = $hval;
2668                        }
2669                    }
2670                    break;
2671
2672                case Horde_Imap_Client::FETCH_IMAPDATE:
2673                    if (isset($data[$uid][$cid]) &&
2674                        ($data[$uid][$cid] instanceof Horde_Imap_Client_DateTime)) {
2675                        $entry->setImapDate($data[$uid][$cid]);
2676                        unset($crit[$key]);
2677                    }
2678                    break;
2679
2680                case Horde_Imap_Client::FETCH_SIZE:
2681                    if (isset($data[$uid][$cid])) {
2682                        $entry->setSize($data[$uid][$cid]);
2683                        unset($crit[$key]);
2684                    }
2685                    break;
2686
2687                case Horde_Imap_Client::FETCH_STRUCTURE:
2688                    if (isset($data[$uid][$cid]) &&
2689                        ($data[$uid][$cid] instanceof Horde_Mime_Part)) {
2690                        $entry->setStructure($data[$uid][$cid]);
2691                        unset($crit[$key]);
2692                    }
2693                    break;
2694                }
2695            }
2696
2697            if (count($crit)) {
2698                $sig = $crit->hash();
2699                if (isset($new_query[$sig])) {
2700                    $new_query[$sig]['i'][] = $entry_idx;
2701                } else {
2702                    $new_query[$sig] = array(
2703                        'c' => $crit,
2704                        'i' => array($entry_idx)
2705                    );
2706                }
2707            }
2708        }
2709
2710        $to_fetch = array();
2711        foreach ($new_query as $val) {
2712            $ids_ob = $this->getIdsOb(null, $sequence);
2713            $ids_ob->duplicates = true;
2714            $ids_ob->add($val['i']);
2715            $to_fetch[] = array_merge($options, array(
2716                '_query' => $val['c'],
2717                'ids' => $ids_ob
2718            ));
2719        }
2720
2721        if (!empty($to_fetch)) {
2722            $this->_fetch(is_null($cs_ret) ? $ret : $cs_ret, $to_fetch);
2723        }
2724
2725        if (is_null($cs_ret)) {
2726            return $ret;
2727        }
2728
2729        /* If doing changedsince query, and all other data is cached, we still
2730         * need to hit IMAP server to determine proper results set. */
2731        if (empty($new_query)) {
2732            $squery = new Horde_Imap_Client_Search_Query();
2733            $squery->modseq($options['changedsince'] + 1);
2734            $squery->ids($options['ids']);
2735
2736            $cs = $this->search($this->_selected, $squery, array(
2737                'sequence' => $sequence
2738            ));
2739
2740            foreach ($cs['match'] as $val) {
2741                $entry = $ret->get($val);
2742                if ($sequence) {
2743                    $entry->setSeq($val);
2744                } else {
2745                    $entry->setUid($val);
2746                }
2747                $cs_ret[$val] = $entry;
2748            }
2749        } else {
2750            foreach ($cs_ret as $key => $val) {
2751                $val->merge($ret->get($key));
2752            }
2753        }
2754
2755        return $cs_ret;
2756    }
2757
2758    /**
2759     * Fetch message data.
2760     *
2761     * Fetch queries should be grouped in the $queries argument. Each value
2762     * is an array of fetch options, with the fetch query stored in the
2763     * '_query' parameter. IMPORTANT: All queries must have the same ID
2764     * type (either sequence or UID).
2765     *
2766     * @param Horde_Imap_Client_Fetch_Results $results  Fetch results.
2767     * @param array $queries                            The list of queries.
2768     *
2769     * @throws Horde_Imap_Client_Exception
2770     */
2771    abstract protected function _fetch(Horde_Imap_Client_Fetch_Results $results,
2772                                       $queries);
2773
2774    /**
2775     * Get the list of vanished messages (UIDs that have been expunged since a
2776     * given mod-sequence value).
2777     *
2778     * @param mixed $mailbox   The mailbox to query. Either a
2779     *                         Horde_Imap_Client_Mailbox object or a string
2780     *                         (UTF-8).
2781     * @param integer $modseq  Search for expunged messages after this
2782     *                         mod-sequence value.
2783     * @param array $opts      Additional options:
2784     *   - ids: (Horde_Imap_Client_Ids)  Restrict to these UIDs.
2785     *          DEFAULT: Returns full list of UIDs vanished (QRESYNC only).
2786     *                   This option is REQUIRED for non-QRESYNC servers or
2787     *                   else an empty list will be returned.
2788     *
2789     * @return Horde_Imap_Client_Ids  List of UIDs that have vanished.
2790     *
2791     * @throws Horde_Imap_Client_NoSupportExtension
2792     */
2793    public function vanished($mailbox, $modseq, array $opts = array())
2794    {
2795        $this->login();
2796
2797        if (empty($opts['ids'])) {
2798            if (!$this->_capability()->isEnabled('QRESYNC')) {
2799                return $this->getIdsOb();
2800            }
2801            $opts['ids'] = $this->getIdsOb(Horde_Imap_Client_Ids::ALL);
2802        } elseif ($opts['ids']->isEmpty()) {
2803            return $this->getIdsOb();
2804        } elseif ($opts['ids']->sequence) {
2805            throw new InvalidArgumentException('Vanished requires UIDs.');
2806        }
2807
2808        $this->openMailbox($mailbox, Horde_Imap_Client::OPEN_AUTO);
2809
2810        if ($this->_capability()->isEnabled('QRESYNC')) {
2811            if (!$this->_mailboxOb()->getStatus(Horde_Imap_Client::STATUS_HIGHESTMODSEQ)) {
2812                throw new Horde_Imap_Client_Exception(
2813                    Horde_Imap_Client_Translation::r("Mailbox does not support mod-sequences."),
2814                    Horde_Imap_Client_Exception::MBOXNOMODSEQ
2815                );
2816            }
2817
2818            return $this->_vanished(max(1, $modseq), $opts['ids']);
2819        }
2820
2821        $ids = $this->resolveIds($mailbox, $opts['ids']);
2822
2823        $squery = new Horde_Imap_Client_Search_Query();
2824        $squery->ids($ids);
2825        $search = $this->search($mailbox, $squery, array(
2826            'nocache' => true
2827        ));
2828
2829        return $this->getIdsOb(array_diff($ids->ids, $search['match']->ids));
2830    }
2831
2832    /**
2833     * Get the list of vanished messages.
2834     *
2835     * @param integer $modseq             Mod-sequence value.
2836     * @param Horde_Imap_Client_Ids $ids  UIDs.
2837     *
2838     * @return Horde_Imap_Client_Ids  List of UIDs that have vanished.
2839     */
2840    abstract protected function _vanished($modseq, Horde_Imap_Client_Ids $ids);
2841
2842    /**
2843     * Store message flag data (see RFC 3501 [6.4.6]).
2844     *
2845     * @param mixed $mailbox  The mailbox containing the messages to modify.
2846     *                        Either a Horde_Imap_Client_Mailbox object or a
2847     *                        string (UTF-8).
2848     * @param array $options  Additional options:
2849     *   - add: (array) An array of flags to add.
2850     *          DEFAULT: No flags added.
2851     *   - ids: (Horde_Imap_Client_Ids) The list of messages to modify.
2852     *          DEFAULT: All messages in $mailbox will be modified.
2853     *   - remove: (array) An array of flags to remove.
2854     *             DEFAULT: No flags removed.
2855     *   - replace: (array) Replace the current flags with this set
2856     *              of flags. Overrides both the 'add' and 'remove' options.
2857     *              DEFAULT: No replace is performed.
2858     *   - unchangedsince: (integer) Only changes flags if the mod-sequence ID
2859     *                     of the message is equal or less than this value.
2860     *                     Requires the CONDSTORE IMAP extension on the server.
2861     *                     Also requires the mailbox to support mod-sequences.
2862     *                     Will throw an exception if either condition is not
2863     *                     met.
2864     *                     DEFAULT: mod-sequence is ignored when applying
2865     *                              changes
2866     *
2867     * @return Horde_Imap_Client_Ids  A Horde_Imap_Client_Ids object
2868     *                                containing the list of IDs that failed
2869     *                                the 'unchangedsince' test.
2870     *
2871     * @throws Horde_Imap_Client_Exception
2872     * @throws Horde_Imap_Client_Exception_NoSupportExtension
2873     */
2874    public function store($mailbox, array $options = array())
2875    {
2876        // Open mailbox call will handle the login.
2877        $this->openMailbox($mailbox, Horde_Imap_Client::OPEN_READWRITE);
2878
2879        /* SEARCHRES requires server support. */
2880        if (empty($options['ids'])) {
2881            $options['ids'] = $this->getIdsOb(Horde_Imap_Client_Ids::ALL);
2882        } elseif ($options['ids']->isEmpty()) {
2883            return $this->getIdsOb();
2884        } elseif ($options['ids']->search_res &&
2885                  !$this->_capability('SEARCHRES')) {
2886            throw new Horde_Imap_Client_Exception_NoSupportExtension('SEARCHRES');
2887        }
2888
2889        if (!empty($options['unchangedsince'])) {
2890            if (!$this->_capability()->isEnabled('CONDSTORE')) {
2891                throw new Horde_Imap_Client_Exception_NoSupportExtension('CONDSTORE');
2892            }
2893
2894            /* RFC 7162 [3.1.2.2] - trying to do a UNCHANGEDSINCE STORE on a
2895             * mailbox that doesn't support it will return BAD. */
2896            if (!$this->_mailboxOb()->getStatus(Horde_Imap_Client::STATUS_HIGHESTMODSEQ)) {
2897                throw new Horde_Imap_Client_Exception(
2898                    Horde_Imap_Client_Translation::r("Mailbox does not support mod-sequences."),
2899                    Horde_Imap_Client_Exception::MBOXNOMODSEQ
2900                );
2901            }
2902        }
2903
2904        return $this->_store($options);
2905    }
2906
2907    /**
2908     * Store message flag data.
2909     *
2910     * @param array $options  Additional options.
2911     *
2912     * @return Horde_Imap_Client_Ids  A Horde_Imap_Client_Ids object
2913     *                                containing the list of IDs that failed
2914     *                                the 'unchangedsince' test.
2915     *
2916     * @throws Horde_Imap_Client_Exception
2917     */
2918    abstract protected function _store($options);
2919
2920    /**
2921     * Copy messages to another mailbox.
2922     *
2923     * @param mixed $source   The source mailbox. Either a
2924     *                        Horde_Imap_Client_Mailbox object or a string
2925     *                        (UTF-8).
2926     * @param mixed $dest     The destination mailbox. Either a
2927     *                        Horde_Imap_Client_Mailbox object or a string
2928     *                        (UTF-8).
2929     * @param array $options  Additional options:
2930     *   - create: (boolean) Try to create $dest if it does not exist?
2931     *             DEFAULT: No.
2932     *   - force_map: (boolean) Forces the array mapping to always be
2933     *                returned. [@since 2.19.0]
2934     *   - ids: (Horde_Imap_Client_Ids) The list of messages to copy.
2935     *          DEFAULT: All messages in $mailbox will be copied.
2936     *   - move: (boolean) If true, delete the original messages.
2937     *           DEFAULT: Original messages are not deleted.
2938     *
2939     * @return mixed  An array mapping old UIDs (keys) to new UIDs (values) on
2940     *                success (only guaranteed if 'force_map' is true) or
2941     *                true.
2942     *
2943     * @throws Horde_Imap_Client_Exception
2944     * @throws Horde_Imap_Client_Exception_NoSupportExtension
2945     */
2946    public function copy($source, $dest, array $options = array())
2947    {
2948        // Open mailbox call will handle the login.
2949        $this->openMailbox($source, empty($options['move']) ? Horde_Imap_Client::OPEN_AUTO : Horde_Imap_Client::OPEN_READWRITE);
2950
2951        /* SEARCHRES requires server support. */
2952        if (empty($options['ids'])) {
2953            $options['ids'] = $this->getIdsOb(Horde_Imap_Client_Ids::ALL);
2954        } elseif ($options['ids']->isEmpty()) {
2955            return array();
2956        } elseif ($options['ids']->search_res &&
2957                  !$this->_capability('SEARCHRES')) {
2958            throw new Horde_Imap_Client_Exception_NoSupportExtension('SEARCHRES');
2959        }
2960
2961        $dest = Horde_Imap_Client_Mailbox::get($dest);
2962        $res = $this->_copy($dest, $options);
2963
2964        if (($res === true) && !empty($options['force_map'])) {
2965            /* Need to manually create mapping from Message-ID data. */
2966            $query = new Horde_Imap_Client_Fetch_Query();
2967            $query->envelope();
2968            $fetch = $this->fetch($source, $query, array(
2969                'ids' => $options['ids']
2970            ));
2971
2972            $res = array();
2973            foreach ($fetch as $val) {
2974                if ($uid = $this->_getUidByMessageId($dest, $val->getEnvelope()->message_id)) {
2975                    $res[$val->getUid()] = $uid;
2976                }
2977            }
2978        }
2979
2980        return $res;
2981    }
2982
2983    /**
2984     * Copy messages to another mailbox.
2985     *
2986     * @param Horde_Imap_Client_Mailbox $dest  The destination mailbox.
2987     * @param array $options                   Additional options.
2988     *
2989     * @return mixed  An array mapping old UIDs (keys) to new UIDs (values) on
2990     *                success (if the IMAP server and/or driver support the
2991     *                UIDPLUS extension) or true.
2992     *
2993     * @throws Horde_Imap_Client_Exception
2994     */
2995    abstract protected function _copy(Horde_Imap_Client_Mailbox $dest,
2996                                      $options);
2997
2998    /**
2999     * Set quota limits. The server must support the IMAP QUOTA extension
3000     * (RFC 2087).
3001     *
3002     * @param mixed $root       The quota root. Either a
3003     *                          Horde_Imap_Client_Mailbox object or a string
3004     *                          (UTF-8).
3005     * @param array $resources  The resource values to set. Keys are the
3006     *                          resource atom name; value is the resource
3007     *                          value.
3008     *
3009     * @throws Horde_Imap_Client_Exception
3010     * @throws Horde_Imap_Client_Exception_NoSupportExtension
3011     */
3012    public function setQuota($root, array $resources = array())
3013    {
3014        $this->login();
3015
3016        if (!$this->_capability('QUOTA')) {
3017            throw new Horde_Imap_Client_Exception_NoSupportExtension('QUOTA');
3018        }
3019
3020        if (!empty($resources)) {
3021            $this->_setQuota(Horde_Imap_Client_Mailbox::get($root), $resources);
3022        }
3023    }
3024
3025    /**
3026     * Set quota limits.
3027     *
3028     * @param Horde_Imap_Client_Mailbox $root  The quota root.
3029     * @param array $resources                 The resource values to set.
3030     *
3031     * @return boolean  True on success.
3032     *
3033     * @throws Horde_Imap_Client_Exception
3034     */
3035    abstract protected function _setQuota(Horde_Imap_Client_Mailbox $root,
3036                                          $resources);
3037
3038    /**
3039     * Get quota limits. The server must support the IMAP QUOTA extension
3040     * (RFC 2087).
3041     *
3042     * @param mixed $root  The quota root. Either a Horde_Imap_Client_Mailbox
3043     *                     object or a string (UTF-8).
3044     *
3045     * @return mixed  An array with resource keys. Each key holds an array
3046     *                with 2 values: 'limit' and 'usage'.
3047     *
3048     * @throws Horde_Imap_Client_Exception
3049     * @throws Horde_Imap_Client_Exception_NoSupportExtension
3050     */
3051    public function getQuota($root)
3052    {
3053        $this->login();
3054
3055        if (!$this->_capability('QUOTA')) {
3056            throw new Horde_Imap_Client_Exception_NoSupportExtension('QUOTA');
3057        }
3058
3059        return $this->_getQuota(Horde_Imap_Client_Mailbox::get($root));
3060    }
3061
3062    /**
3063     * Get quota limits.
3064     *
3065     * @param Horde_Imap_Client_Mailbox $root  The quota root.
3066     *
3067     * @return mixed  An array with resource keys. Each key holds an array
3068     *                with 2 values: 'limit' and 'usage'.
3069     *
3070     * @throws Horde_Imap_Client_Exception
3071     */
3072    abstract protected function _getQuota(Horde_Imap_Client_Mailbox $root);
3073
3074    /**
3075     * Get quota limits for a mailbox. The server must support the IMAP QUOTA
3076     * extension (RFC 2087).
3077     *
3078     * @param mixed $mailbox  A mailbox. Either a Horde_Imap_Client_Mailbox
3079     *                        object or a string (UTF-8).
3080     *
3081     * @return mixed  An array with the keys being the quota roots. Each key
3082     *                holds an array with resource keys: each of these keys
3083     *                holds an array with 2 values: 'limit' and 'usage'.
3084     *
3085     * @throws Horde_Imap_Client_Exception
3086     * @throws Horde_Imap_Client_Exception_NoSupportExtension
3087     */
3088    public function getQuotaRoot($mailbox)
3089    {
3090        $this->login();
3091
3092        if (!$this->_capability('QUOTA')) {
3093            throw new Horde_Imap_Client_Exception_NoSupportExtension('QUOTA');
3094        }
3095
3096        return $this->_getQuotaRoot(Horde_Imap_Client_Mailbox::get($mailbox));
3097    }
3098
3099    /**
3100     * Get quota limits for a mailbox.
3101     *
3102     * @param Horde_Imap_Client_Mailbox $mailbox  A mailbox.
3103     *
3104     * @return mixed  An array with the keys being the quota roots. Each key
3105     *                holds an array with resource keys: each of these keys
3106     *                holds an array with 2 values: 'limit' and 'usage'.
3107     *
3108     * @throws Horde_Imap_Client_Exception
3109     */
3110    abstract protected function _getQuotaRoot(Horde_Imap_Client_Mailbox $mailbox);
3111
3112    /**
3113     * Get the ACL rights for a given mailbox. The server must support the
3114     * IMAP ACL extension (RFC 2086/4314).
3115     *
3116     * @param mixed $mailbox  A mailbox. Either a Horde_Imap_Client_Mailbox
3117     *                        object or a string (UTF-8).
3118     *
3119     * @return array  An array with identifiers as the keys and
3120     *                Horde_Imap_Client_Data_Acl objects as the values.
3121     *
3122     * @throws Horde_Imap_Client_Exception
3123     */
3124    public function getACL($mailbox)
3125    {
3126        $this->login();
3127        return $this->_getACL(Horde_Imap_Client_Mailbox::get($mailbox));
3128    }
3129
3130    /**
3131     * Get ACL rights for a given mailbox.
3132     *
3133     * @param Horde_Imap_Client_Mailbox $mailbox  A mailbox.
3134     *
3135     * @return array  An array with identifiers as the keys and
3136     *                Horde_Imap_Client_Data_Acl objects as the values.
3137     *
3138     * @throws Horde_Imap_Client_Exception
3139     */
3140    abstract protected function _getACL(Horde_Imap_Client_Mailbox $mailbox);
3141
3142    /**
3143     * Set ACL rights for a given mailbox/identifier.
3144     *
3145     * @param mixed $mailbox      A mailbox. Either a Horde_Imap_Client_Mailbox
3146     *                            object or a string (UTF-8).
3147     * @param string $identifier  The identifier to alter (UTF-8).
3148     * @param array $options      Additional options:
3149     *   - rights: (string) The rights to alter or set.
3150     *   - action: (string, optional) If 'add' or 'remove', adds or removes the
3151     *             specified rights. Sets the rights otherwise.
3152     *
3153     * @throws Horde_Imap_Client_Exception
3154     * @throws Horde_Imap_Client_Exception_NoSupportExtension
3155     */
3156    public function setACL($mailbox, $identifier, $options)
3157    {
3158        $this->login();
3159
3160        if (!$this->_capability('ACL')) {
3161            throw new Horde_Imap_Client_Exception_NoSupportExtension('ACL');
3162        }
3163
3164        if (empty($options['rights'])) {
3165            if (!isset($options['action']) ||
3166                (($options['action'] != 'add') &&
3167                 $options['action'] != 'remove')) {
3168                $this->_deleteACL(
3169                    Horde_Imap_Client_Mailbox::get($mailbox),
3170                    Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($identifier)
3171                );
3172            }
3173            return;
3174        }
3175
3176        $acl = ($options['rights'] instanceof Horde_Imap_Client_Data_Acl)
3177            ? $options['rights']
3178            : new Horde_Imap_Client_Data_Acl(strval($options['rights']));
3179
3180        $options['rights'] = $acl->getString(
3181            $this->_capability('RIGHTS')
3182                ? Horde_Imap_Client_Data_AclCommon::RFC_4314
3183                : Horde_Imap_Client_Data_AclCommon::RFC_2086
3184        );
3185        if (isset($options['action'])) {
3186            switch ($options['action']) {
3187            case 'add':
3188                $options['rights'] = '+' . $options['rights'];
3189                break;
3190            case 'remove':
3191                $options['rights'] = '-' . $options['rights'];
3192                break;
3193            }
3194        }
3195
3196        $this->_setACL(
3197            Horde_Imap_Client_Mailbox::get($mailbox),
3198            Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($identifier),
3199            $options
3200        );
3201    }
3202
3203    /**
3204     * Set ACL rights for a given mailbox/identifier.
3205     *
3206     * @param Horde_Imap_Client_Mailbox $mailbox  A mailbox.
3207     * @param string $identifier                  The identifier to alter
3208     *                                            (UTF7-IMAP).
3209     * @param array $options                      Additional options. 'rights'
3210     *                                            contains the string of
3211     *                                            rights to set on the server.
3212     *
3213     * @throws Horde_Imap_Client_Exception
3214     */
3215    abstract protected function _setACL(Horde_Imap_Client_Mailbox $mailbox,
3216                                        $identifier, $options);
3217
3218    /**
3219     * Deletes ACL rights for a given mailbox/identifier.
3220     *
3221     * @param mixed $mailbox      A mailbox. Either a Horde_Imap_Client_Mailbox
3222     *                            object or a string (UTF-8).
3223     * @param string $identifier  The identifier to delete (UTF-8).
3224     *
3225     * @throws Horde_Imap_Client_Exception
3226     * @throws Horde_Imap_Client_Exception_NoSupportExtension
3227     */
3228    public function deleteACL($mailbox, $identifier)
3229    {
3230        $this->login();
3231
3232        if (!$this->_capability('ACL')) {
3233            throw new Horde_Imap_Client_Exception_NoSupportExtension('ACL');
3234        }
3235
3236        $this->_deleteACL(
3237            Horde_Imap_Client_Mailbox::get($mailbox),
3238            Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($identifier)
3239        );
3240    }
3241
3242    /**
3243     * Deletes ACL rights for a given mailbox/identifier.
3244     *
3245     * @param Horde_Imap_Client_Mailbox $mailbox  A mailbox.
3246     * @param string $identifier                  The identifier to delete
3247     *                                            (UTF7-IMAP).
3248     *
3249     * @throws Horde_Imap_Client_Exception
3250     */
3251    abstract protected function _deleteACL(Horde_Imap_Client_Mailbox $mailbox,
3252                                           $identifier);
3253
3254    /**
3255     * List the ACL rights for a given mailbox/identifier. The server must
3256     * support the IMAP ACL extension (RFC 2086/4314).
3257     *
3258     * @param mixed $mailbox      A mailbox. Either a Horde_Imap_Client_Mailbox
3259     *                            object or a string (UTF-8).
3260     * @param string $identifier  The identifier to query (UTF-8).
3261     *
3262     * @return Horde_Imap_Client_Data_AclRights  An ACL data rights object.
3263     *
3264     * @throws Horde_Imap_Client_Exception
3265     * @throws Horde_Imap_Client_Exception_NoSupportExtension
3266     */
3267    public function listACLRights($mailbox, $identifier)
3268    {
3269        $this->login();
3270
3271        if (!$this->_capability('ACL')) {
3272            throw new Horde_Imap_Client_Exception_NoSupportExtension('ACL');
3273        }
3274
3275        return $this->_listACLRights(
3276            Horde_Imap_Client_Mailbox::get($mailbox),
3277            Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($identifier)
3278        );
3279    }
3280
3281    /**
3282     * Get ACL rights for a given mailbox/identifier.
3283     *
3284     * @param Horde_Imap_Client_Mailbox $mailbox  A mailbox.
3285     * @param string $identifier                  The identifier to query
3286     *                                            (UTF7-IMAP).
3287     *
3288     * @return Horde_Imap_Client_Data_AclRights  An ACL data rights object.
3289     *
3290     * @throws Horde_Imap_Client_Exception
3291     */
3292    abstract protected function _listACLRights(Horde_Imap_Client_Mailbox $mailbox,
3293                                               $identifier);
3294
3295    /**
3296     * Get the ACL rights for the current user for a given mailbox. The
3297     * server must support the IMAP ACL extension (RFC 2086/4314).
3298     *
3299     * @param mixed $mailbox  A mailbox. Either a Horde_Imap_Client_Mailbox
3300     *                        object or a string (UTF-8).
3301     *
3302     * @return Horde_Imap_Client_Data_Acl  An ACL data object.
3303     *
3304     * @throws Horde_Imap_Client_Exception
3305     * @throws Horde_Imap_Client_Exception_NoSupportExtension
3306     */
3307    public function getMyACLRights($mailbox)
3308    {
3309        $this->login();
3310
3311        if (!$this->_capability('ACL')) {
3312            throw new Horde_Imap_Client_Exception_NoSupportExtension('ACL');
3313        }
3314
3315        return $this->_getMyACLRights(Horde_Imap_Client_Mailbox::get($mailbox));
3316    }
3317
3318    /**
3319     * Get the ACL rights for the current user for a given mailbox.
3320     *
3321     * @param Horde_Imap_Client_Mailbox $mailbox  A mailbox.
3322     *
3323     * @return Horde_Imap_Client_Data_Acl  An ACL data object.
3324     *
3325     * @throws Horde_Imap_Client_Exception
3326     */
3327    abstract protected function _getMyACLRights(Horde_Imap_Client_Mailbox $mailbox);
3328
3329    /**
3330     * Return master list of ACL rights available on the server.
3331     *
3332     * @return array  A list of ACL rights.
3333     */
3334    public function allAclRights()
3335    {
3336        $this->login();
3337
3338        $rights = array(
3339            Horde_Imap_Client::ACL_LOOKUP,
3340            Horde_Imap_Client::ACL_READ,
3341            Horde_Imap_Client::ACL_SEEN,
3342            Horde_Imap_Client::ACL_WRITE,
3343            Horde_Imap_Client::ACL_INSERT,
3344            Horde_Imap_Client::ACL_POST,
3345            Horde_Imap_Client::ACL_ADMINISTER
3346        );
3347
3348        if ($capability = $this->_capability()->getParams('RIGHTS')) {
3349            // Add rights defined in CAPABILITY string (RFC 4314).
3350            return array_merge($rights, str_split(reset($capability)));
3351        }
3352
3353        // Add RFC 2086 rights (deprecated by RFC 4314, but need to keep for
3354        // compatibility with old servers).
3355        return array_merge($rights, array(
3356            Horde_Imap_Client::ACL_CREATE,
3357            Horde_Imap_Client::ACL_DELETE
3358        ));
3359    }
3360
3361    /**
3362     * Get metadata for a given mailbox. The server must support either the
3363     * IMAP METADATA extension (RFC 5464) or the ANNOTATEMORE extension
3364     * (http://ietfreport.isoc.org/idref/draft-daboo-imap-annotatemore/).
3365     *
3366     * @param mixed $mailbox  A mailbox. Either a Horde_Imap_Client_Mailbox
3367     *                        object or a string (UTF-8).
3368     * @param array $entries  The entries to fetch (UTF-8 strings).
3369     * @param array $options  Additional options:
3370     *   - depth: (string) Either "0", "1" or "infinity". Returns only the
3371     *            given value (0), only values one level below the specified
3372     *            value (1) or all entries below the specified value
3373     *            (infinity).
3374     *   - maxsize: (integer) The maximal size the returned values may have.
3375     *              DEFAULT: No maximal size.
3376     *
3377     * @return array  An array with metadata names as the keys and metadata
3378     *                values as the values. If 'maxsize' is set, and entries
3379     *                exist on the server larger than this size, the size will
3380     *                be returned in the key '*longentries'.
3381     *
3382     * @throws Horde_Imap_Client_Exception
3383     */
3384    public function getMetadata($mailbox, $entries, array $options = array())
3385    {
3386        $this->login();
3387
3388        if (!is_array($entries)) {
3389            $entries = array($entries);
3390        }
3391
3392        return $this->_getMetadata(Horde_Imap_Client_Mailbox::get($mailbox), array_map(array('Horde_Imap_Client_Utf7imap', 'Utf8ToUtf7Imap'), $entries), $options);
3393    }
3394
3395    /**
3396     * Get metadata for a given mailbox.
3397     *
3398     * @param Horde_Imap_Client_Mailbox $mailbox  A mailbox.
3399     * @param array $entries                      The entries to fetch
3400     *                                            (UTF7-IMAP strings).
3401     * @param array $options                      Additional options.
3402     *
3403     * @return array  An array with metadata names as the keys and metadata
3404     *                values as the values.
3405     *
3406     * @throws Horde_Imap_Client_Exception
3407     */
3408    abstract protected function _getMetadata(Horde_Imap_Client_Mailbox $mailbox,
3409                                             $entries, $options);
3410
3411    /**
3412     * Set metadata for a given mailbox/identifier.
3413     *
3414     * @param mixed $mailbox  A mailbox. Either a Horde_Imap_Client_Mailbox
3415     *                        object or a string (UTF-8). If empty, sets a
3416     *                        server annotation.
3417     * @param array $data     A set of data values. The metadata values
3418     *                        corresponding to the keys of the array will
3419     *                        be set to the values in the array.
3420     *
3421     * @throws Horde_Imap_Client_Exception
3422     */
3423    public function setMetadata($mailbox, $data)
3424    {
3425        $this->login();
3426        $this->_setMetadata(Horde_Imap_Client_Mailbox::get($mailbox), $data);
3427    }
3428
3429    /**
3430     * Set metadata for a given mailbox/identifier.
3431     *
3432     * @param Horde_Imap_Client_Mailbox $mailbox  A mailbox.
3433     * @param array $data                         A set of data values. See
3434     *                                            setMetadata() for format.
3435     *
3436     * @throws Horde_Imap_Client_Exception
3437     */
3438    abstract protected function _setMetadata(Horde_Imap_Client_Mailbox $mailbox,
3439                                             $data);
3440
3441    /* Public utility functions. */
3442
3443    /**
3444     * Returns a unique identifier for the current mailbox status.
3445     *
3446     * @deprecated
3447     *
3448     * @param mixed $mailbox  A mailbox. Either a Horde_Imap_Client_Mailbox
3449     *                        object or a string (UTF-8).
3450     * @param array $addl     Additional cache info to add to the cache ID
3451     *                        string.
3452     *
3453     * @return string  The cache ID string, which will change when the
3454     *                 composition of the mailbox changes. The uidvalidity
3455     *                 will always be the first element, and will be delimited
3456     *                 by the '|' character.
3457     *
3458     * @throws Horde_Imap_Client_Exception
3459     */
3460    public function getCacheId($mailbox, array $addl = array())
3461    {
3462        return Horde_Imap_Client_Base_Deprecated::getCacheId($this, $mailbox, $this->_capability()->isEnabled('CONDSTORE'), $addl);
3463    }
3464
3465    /**
3466     * Parses a cacheID created by getCacheId().
3467     *
3468     * @deprecated
3469     *
3470     * @param string $id  The cache ID.
3471     *
3472     * @return array  An array with the following information:
3473     *   - highestmodseq: (integer)
3474     *   - messages: (integer)
3475     *   - uidnext: (integer)
3476     *   - uidvalidity: (integer) Always present
3477     */
3478    public function parseCacheId($id)
3479    {
3480        return Horde_Imap_Client_Base_Deprecated::parseCacheId($id);
3481    }
3482
3483    /**
3484     * Resolves an IDs object into a list of IDs.
3485     *
3486     * @param Horde_Imap_Client_Mailbox $mailbox  The mailbox.
3487     * @param Horde_Imap_Client_Ids $ids          The Ids object.
3488     * @param integer $convert                    Convert to UIDs?
3489     *   - 0: No
3490     *   - 1: Only if $ids is not already a UIDs object
3491     *   - 2: Always
3492     *
3493     * @return Horde_Imap_Client_Ids  The list of IDs.
3494     */
3495    public function resolveIds(Horde_Imap_Client_Mailbox $mailbox,
3496                               Horde_Imap_Client_Ids $ids, $convert = 0)
3497    {
3498        $map = $this->_mailboxOb($mailbox)->map;
3499
3500        if ($ids->special) {
3501            /* Optimization for ALL sequence searches. */
3502            if (!$convert && $ids->all && $ids->sequence) {
3503                $res = $this->status($mailbox, Horde_Imap_Client::STATUS_MESSAGES);
3504                return $this->getIdsOb($res['messages'] ? ('1:' . $res['messages']) : array(), true);
3505            }
3506
3507            $convert = 2;
3508        } elseif (!$convert ||
3509                  (!$ids->sequence && ($convert == 1)) ||
3510                  $ids->isEmpty()) {
3511            return clone $ids;
3512        } else {
3513            /* Do an all or nothing: either we have all the numbers/UIDs in
3514             * memory and can return, or just send the whole ID query to the
3515             * server. Any advantage we would get by a partial search are
3516             * outweighed by the complexities needed to make the search and
3517             * then merge back into the original results. */
3518            $lookup = $map->lookup($ids);
3519            if (count($lookup) === count($ids)) {
3520                return $this->getIdsOb(array_values($lookup));
3521            }
3522        }
3523
3524        $query = new Horde_Imap_Client_Search_Query();
3525        $query->ids($ids);
3526
3527        $res = $this->search($mailbox, $query, array(
3528            'results' => array(
3529                Horde_Imap_Client::SEARCH_RESULTS_MATCH,
3530                Horde_Imap_Client::SEARCH_RESULTS_SAVE
3531            ),
3532            'sequence' => (!$convert && $ids->sequence),
3533            'sort' => array(Horde_Imap_Client::SORT_SEQUENCE)
3534        ));
3535
3536        /* Update mapping. */
3537        if ($convert) {
3538            if ($ids->all) {
3539                $ids = $this->getIdsOb('1:' . count($res['match']));
3540            } elseif ($ids->special) {
3541                return $res['match'];
3542            }
3543
3544            /* Sanity checking (Bug #12911). */
3545            $list1 = array_slice($ids->ids, 0, count($res['match']));
3546            $list2 = $res['match']->ids;
3547            if (!empty($list1) &&
3548                !empty($list2) &&
3549                (count($list1) === count($list2))) {
3550                $map->update(array_combine($list1, $list2));
3551            }
3552        }
3553
3554        return $res['match'];
3555    }
3556
3557    /**
3558     * Determines if the given charset is valid for search-related queries.
3559     * This check pertains just to the basic IMAP SEARCH command.
3560     *
3561     * @deprecated Use $search_charset property instead.
3562     *
3563     * @param string $charset  The query charset.
3564     *
3565     * @return boolean  True if server supports this charset.
3566     */
3567    public function validSearchCharset($charset)
3568    {
3569        return $this->search_charset->query($charset);
3570    }
3571
3572    /* Mailbox syncing functions. */
3573
3574    /**
3575     * Returns a unique token for the current mailbox synchronization status.
3576     *
3577     * @since 2.2.0
3578     *
3579     * @param mixed $mailbox  A mailbox. Either a Horde_Imap_Client_Mailbox
3580     *                        object or a string (UTF-8).
3581     *
3582     * @return string  The sync token.
3583     *
3584     * @throws Horde_Imap_Client_Exception
3585     */
3586    public function getSyncToken($mailbox)
3587    {
3588        $out = array();
3589
3590        foreach ($this->_syncStatus($mailbox) as $key => $val) {
3591            $out[] = $key . $val;
3592        }
3593
3594        return base64_encode(implode(',', $out));
3595    }
3596
3597    /**
3598     * Synchronize a mailbox from a sync token.
3599     *
3600     * @since 2.2.0
3601     *
3602     * @param mixed $mailbox  A mailbox. Either a Horde_Imap_Client_Mailbox
3603     *                        object or a string (UTF-8).
3604     * @param string $token   A sync token generated by getSyncToken().
3605     * @param array $opts     Additional options:
3606     *   - criteria: (integer) Mask of Horde_Imap_Client::SYNC_* criteria to
3607     *               return. Defaults to SYNC_ALL.
3608     *   - ids: (Horde_Imap_Client_Ids) A cached list of UIDs. Unless QRESYNC
3609     *          is available on the server, failure to specify this option
3610     *          means SYNC_VANISHEDUIDS information cannot be returned.
3611     *
3612     * @return Horde_Imap_Client_Data_Sync  A sync object.
3613     *
3614     * @throws Horde_Imap_Client_Exception
3615     * @throws Horde_Imap_Client_Exception_Sync
3616     */
3617    public function sync($mailbox, $token, array $opts = array())
3618    {
3619        if (($token = base64_decode($token, true)) === false) {
3620            throw new Horde_Imap_Client_Exception_Sync('Bad token.', Horde_Imap_Client_Exception_Sync::BAD_TOKEN);
3621        }
3622
3623        $sync = array();
3624        foreach (explode(',', $token) as $val) {
3625            $sync[substr($val, 0, 1)] = substr($val, 1);
3626        }
3627
3628        return new Horde_Imap_Client_Data_Sync(
3629            $this,
3630            $mailbox,
3631            $sync,
3632            $this->_syncStatus($mailbox),
3633            (isset($opts['criteria']) ? $opts['criteria'] : Horde_Imap_Client::SYNC_ALL),
3634            (isset($opts['ids']) ? $opts['ids'] : null)
3635        );
3636    }
3637
3638    /* Private utility functions. */
3639
3640    /**
3641     * Store FETCH data in cache.
3642     *
3643     * @param Horde_Imap_Client_Fetch_Results $data  The fetch results.
3644     *
3645     * @throws Horde_Imap_Client_Exception
3646     */
3647    protected function _updateCache(Horde_Imap_Client_Fetch_Results $data)
3648    {
3649        if (!empty($this->_temp['fetch_nocache']) ||
3650            empty($this->_selected) ||
3651            !count($data) ||
3652            !$this->_initCache(true)) {
3653            return;
3654        }
3655
3656        $c = $this->getParam('cache');
3657        if (in_array(strval($this->_selected), $c['fetch_ignore'])) {
3658            $this->_debug->info(sprintf(
3659                'CACHE: Ignoring FETCH data [%s]',
3660                $this->_selected
3661            ));
3662            return;
3663        }
3664
3665        /* Optimization: we can directly use getStatus() here since we know
3666         * these values are initialized. */
3667        $mbox_ob = $this->_mailboxOb();
3668        $highestmodseq = $mbox_ob->getStatus(Horde_Imap_Client::STATUS_HIGHESTMODSEQ);
3669        $uidvalidity = $mbox_ob->getStatus(Horde_Imap_Client::STATUS_UIDVALIDITY);
3670
3671        $mapping = $modseq = $tocache = array();
3672        if (count($data)) {
3673            $cf = $this->_cacheFields();
3674        }
3675
3676        foreach ($data as $v) {
3677            /* It is possible that we received FETCH information that doesn't
3678             * contain UID data. This is uncacheable so don't process. */
3679            if (!($uid = $v->getUid())) {
3680                return;
3681            }
3682
3683            $tmp = array();
3684
3685            if ($v->isDowngraded()) {
3686                $tmp[self::CACHE_DOWNGRADED] = true;
3687            }
3688
3689            foreach ($cf as $key => $val) {
3690                if ($v->exists($key)) {
3691                    switch ($key) {
3692                    case Horde_Imap_Client::FETCH_ENVELOPE:
3693                        $tmp[$val] = $v->getEnvelope();
3694                        break;
3695
3696                    case Horde_Imap_Client::FETCH_FLAGS:
3697                        if ($highestmodseq) {
3698                            $modseq[$uid] = $v->getModSeq();
3699                            $tmp[$val] = $v->getFlags();
3700                        }
3701                        break;
3702
3703                    case Horde_Imap_Client::FETCH_HEADERS:
3704                        foreach ($this->_temp['headers_caching'] as $label => $hash) {
3705                            if ($hdr = $v->getHeaders($label)) {
3706                                $tmp[$val][$hash] = $hdr;
3707                            }
3708                        }
3709                        break;
3710
3711                    case Horde_Imap_Client::FETCH_IMAPDATE:
3712                        $tmp[$val] = $v->getImapDate();
3713                        break;
3714
3715                    case Horde_Imap_Client::FETCH_SIZE:
3716                        $tmp[$val] = $v->getSize();
3717                        break;
3718
3719                    case Horde_Imap_Client::FETCH_STRUCTURE:
3720                        $tmp[$val] = clone $v->getStructure();
3721                        break;
3722                    }
3723                }
3724            }
3725
3726            if (!empty($tmp)) {
3727                $tocache[$uid] = $tmp;
3728            }
3729
3730            $mapping[$v->getSeq()] = $uid;
3731        }
3732
3733        if (!empty($mapping)) {
3734            if (!empty($tocache)) {
3735                $this->_cache->set($this->_selected, $tocache, $uidvalidity);
3736            }
3737
3738            $this->_mailboxOb()->map->update($mapping);
3739        }
3740
3741        if (!empty($modseq)) {
3742            $this->_updateModSeq(max(array_merge($modseq, array($highestmodseq))));
3743            $mbox_ob->setStatus(Horde_Imap_Client::STATUS_SYNCFLAGUIDS, array_keys($modseq));
3744        }
3745    }
3746
3747    /**
3748     * Moves cache entries from the current mailbox to another mailbox.
3749     *
3750     * @param Horde_Imap_Client_Mailbox $to  The destination mailbox.
3751     * @param array $map                     Mapping of source UIDs (keys) to
3752     *                                       destination UIDs (values).
3753     * @param string $uidvalid               UIDVALIDITY of destination
3754     *                                       mailbox.
3755     *
3756     * @throws Horde_Imap_Client_Exception
3757     */
3758    protected function _moveCache(Horde_Imap_Client_Mailbox $to, $map,
3759                                  $uidvalid)
3760    {
3761        if (!$this->_initCache()) {
3762            return;
3763        }
3764
3765        $c = $this->getParam('cache');
3766        if (in_array(strval($to), $c['fetch_ignore'])) {
3767            $this->_debug->info(sprintf(
3768                'CACHE: Ignoring moving FETCH data (%s => %s)',
3769                $this->_selected,
3770                $to
3771            ));
3772            return;
3773        }
3774
3775        $old = $this->_cache->get($this->_selected, array_keys($map), null);
3776        $new = array();
3777
3778        foreach ($map as $key => $val) {
3779            if (!empty($old[$key])) {
3780                $new[$val] = $old[$key];
3781            }
3782        }
3783
3784        if (!empty($new)) {
3785            $this->_cache->set($to, $new, $uidvalid);
3786        }
3787    }
3788
3789    /**
3790     * Delete messages in the cache.
3791     *
3792     * @param Horde_Imap_Client_Mailbox $mailbox  The mailbox.
3793     * @param Horde_Imap_Client_Ids $ids          The list of IDs to delete in
3794     *                                            $mailbox.
3795     * @param array $opts                         Additional options (not used
3796     *                                            in base class).
3797     *
3798     * @return Horde_Imap_Client_Ids  UIDs that were deleted.
3799     * @throws Horde_Imap_Client_Exception
3800     */
3801    protected function _deleteMsgs(Horde_Imap_Client_Mailbox $mailbox,
3802                                   Horde_Imap_Client_Ids $ids,
3803                                   array $opts = array())
3804    {
3805        if (!$this->_initCache()) {
3806            return $ids;
3807        }
3808
3809        $mbox_ob = $this->_mailboxOb();
3810        $ids_ob = $ids->sequence
3811            ? $this->getIdsOb($mbox_ob->map->lookup($ids))
3812            : $ids;
3813
3814        $this->_cache->deleteMsgs($mailbox, $ids_ob->ids);
3815        $mbox_ob->setStatus(Horde_Imap_Client::STATUS_SYNCVANISHED, $ids_ob->ids);
3816        $mbox_ob->map->remove($ids);
3817
3818        return $ids_ob;
3819    }
3820
3821    /**
3822     * Retrieve data from the search cache.
3823     *
3824     * @param string $type    The cache type ('search' or 'thread').
3825     * @param array $options  The options array of the calling function.
3826     *
3827     * @return mixed  Returns search cache metadata. If search was retrieved,
3828     *                data is in key 'data'.
3829     *                Returns null if caching is not available.
3830     */
3831    protected function _getSearchCache($type, $options)
3832    {
3833        $status = $this->status($this->_selected, Horde_Imap_Client::STATUS_HIGHESTMODSEQ | Horde_Imap_Client::STATUS_UIDVALIDITY);
3834
3835        /* Search caching requires MODSEQ, which may not be active for a
3836         * mailbox. */
3837        if (empty($status['highestmodseq'])) {
3838            return null;
3839        }
3840
3841        ksort($options);
3842        $cache = hash('md5', $type . serialize($options));
3843        $cacheid = $this->getSyncToken($this->_selected);
3844        $ret = array();
3845
3846        $md = $this->_cache->getMetaData(
3847            $this->_selected,
3848            $status['uidvalidity'],
3849            array(self::CACHE_SEARCH, self::CACHE_SEARCHID)
3850        );
3851
3852        if (!isset($md[self::CACHE_SEARCHID]) ||
3853            ($md[self::CACHE_SEARCHID] != $cacheid)) {
3854            $md[self::CACHE_SEARCH] = array();
3855            $md[self::CACHE_SEARCHID] = $cacheid;
3856            if ($this->_debug->debug &&
3857                !isset($this->_temp['searchcacheexpire'][strval($this->_selected)])) {
3858                $this->_debug->info(sprintf(
3859                    'SEARCH: Expired from cache [%s]',
3860                    $this->_selected
3861                ));
3862                $this->_temp['searchcacheexpire'][strval($this->_selected)] = true;
3863            }
3864        } elseif (isset($md[self::CACHE_SEARCH][$cache])) {
3865            $this->_debug->info(sprintf(
3866                'SEARCH: Retrieved %s from cache (%s [%s])',
3867                $type,
3868                $cache,
3869                $this->_selected
3870            ));
3871            $ret['data'] = $md[self::CACHE_SEARCH][$cache];
3872            unset($md[self::CACHE_SEARCHID]);
3873        }
3874
3875        return array_merge($ret, array(
3876            'id' => $cache,
3877            'metadata' => $md,
3878            'type' => $type
3879        ));
3880    }
3881
3882    /**
3883     * Set data in the search cache.
3884     *
3885     * @param mixed $data    The cache data to store.
3886     * @param string $sdata  The search data returned from _getSearchCache().
3887     */
3888    protected function _setSearchCache($data, $sdata)
3889    {
3890        $sdata['metadata'][self::CACHE_SEARCH][$sdata['id']] = $data;
3891
3892        $this->_cache->setMetaData($this->_selected, null, $sdata['metadata']);
3893
3894        if ($this->_debug->debug) {
3895            $this->_debug->info(sprintf(
3896                'SEARCH: Saved %s to cache (%s [%s])',
3897                $sdata['type'],
3898                $sdata['id'],
3899                $this->_selected
3900            ));
3901            unset($this->_temp['searchcacheexpire'][strval($this->_selected)]);
3902        }
3903    }
3904
3905    /**
3906     * Updates the cached MODSEQ value.
3907     *
3908     * @param integer $modseq  MODSEQ value to store.
3909     *
3910     * @return mixed  The MODSEQ of the old value if it was replaced (or false
3911     *                if it didn't exist or is the same).
3912     */
3913    protected function _updateModSeq($modseq)
3914    {
3915        if (!$this->_initCache(true)) {
3916            return false;
3917        }
3918
3919        $mbox_ob = $this->_mailboxOb();
3920        $uidvalid = $mbox_ob->getStatus(Horde_Imap_Client::STATUS_UIDVALIDITY);
3921        $md = $this->_cache->getMetaData($this->_selected, $uidvalid, array(self::CACHE_MODSEQ));
3922
3923        if (isset($md[self::CACHE_MODSEQ])) {
3924            if ($md[self::CACHE_MODSEQ] < $modseq) {
3925                $set = true;
3926                $sync = $md[self::CACHE_MODSEQ];
3927            } else {
3928                $set = false;
3929                $sync = 0;
3930            }
3931            $mbox_ob->setStatus(Horde_Imap_Client::STATUS_SYNCMODSEQ, $md[self::CACHE_MODSEQ]);
3932        } else {
3933            $set = true;
3934            $sync = 0;
3935        }
3936
3937        /* $modseq can be 0 - NOMODSEQ - so don't store in that case. */
3938        if ($set && $modseq) {
3939            $this->_cache->setMetaData($this->_selected, $uidvalid, array(
3940                self::CACHE_MODSEQ => $modseq
3941            ));
3942        }
3943
3944        return $sync;
3945    }
3946
3947    /**
3948     * Synchronizes the current mailbox cache with the server (using CONDSTORE
3949     * or QRESYNC).
3950     */
3951    protected function _condstoreSync()
3952    {
3953        $mbox_ob = $this->_mailboxOb();
3954
3955        /* Check that modseqs are available in mailbox. */
3956        if (!($highestmodseq = $mbox_ob->getStatus(Horde_Imap_Client::STATUS_HIGHESTMODSEQ)) ||
3957            !($modseq = $this->_updateModSeq($highestmodseq))) {
3958            $mbox_ob->sync = true;
3959        }
3960
3961        if ($mbox_ob->sync) {
3962            return;
3963        }
3964
3965        $uids_ob = $this->getIdsOb($this->_cache->get(
3966            $this->_selected,
3967            array(),
3968            array(),
3969            $mbox_ob->getStatus(Horde_Imap_Client::STATUS_UIDVALIDITY)
3970        ));
3971
3972        if (!count($uids_ob)) {
3973            $mbox_ob->sync = true;
3974            return;
3975        }
3976
3977        /* Are we caching flags? */
3978        if (array_key_exists(Horde_Imap_Client::FETCH_FLAGS, $this->_cacheFields())) {
3979            $fquery = new Horde_Imap_Client_Fetch_Query();
3980            $fquery->flags();
3981
3982            /* Update flags in cache. Cache will be updated in _fetch(). */
3983            $this->_fetch(new Horde_Imap_Client_Fetch_Results(), array(
3984                array(
3985                    '_query' => $fquery,
3986                    'changedsince' => $modseq,
3987                    'ids' => $uids_ob
3988                )
3989            ));
3990        }
3991
3992        /* Search for deleted messages, and remove from cache. */
3993        $vanished = $this->vanished($this->_selected, $modseq, array(
3994            'ids' => $uids_ob
3995        ));
3996        $disappear = array_diff($uids_ob->ids, $vanished->ids);
3997        if (!empty($disappear)) {
3998            $this->_deleteMsgs($this->_selected, $this->getIdsOb($disappear));
3999        }
4000
4001        $mbox_ob->sync = true;
4002    }
4003
4004    /**
4005     * Provide the list of available caching fields.
4006     *
4007     * @return array  The list of available caching fields (fields are in the
4008     *                key).
4009     */
4010    protected function _cacheFields()
4011    {
4012        $c = $this->getParam('cache');
4013        $out = $c['fields'];
4014
4015        if (!$this->_capability()->isEnabled('CONDSTORE')) {
4016            unset($out[Horde_Imap_Client::FETCH_FLAGS]);
4017        }
4018
4019        return $out;
4020    }
4021
4022    /**
4023     * Return the current mailbox synchronization status.
4024     *
4025     * @param mixed $mailbox  A mailbox. Either a Horde_Imap_Client_Mailbox
4026     *                        object or a string (UTF-8).
4027     *
4028     * @return array  An array with status data. (This data is not guaranteed
4029     *                to have any specific format).
4030     */
4031    protected function _syncStatus($mailbox)
4032    {
4033        $status = $this->status(
4034            $mailbox,
4035            Horde_Imap_Client::STATUS_HIGHESTMODSEQ |
4036            Horde_Imap_Client::STATUS_MESSAGES |
4037            Horde_Imap_Client::STATUS_UIDNEXT_FORCE |
4038            Horde_Imap_Client::STATUS_UIDVALIDITY
4039        );
4040
4041        $fields = array('uidnext', 'uidvalidity');
4042        if (empty($status['highestmodseq'])) {
4043            $fields[] = 'messages';
4044        } else {
4045            $fields[] = 'highestmodseq';
4046        }
4047
4048        $out = array();
4049        $sync_map = array_flip(Horde_Imap_Client_Data_Sync::$map);
4050
4051        foreach ($fields as $val) {
4052            $out[$sync_map[$val]] = $status[$val];
4053        }
4054
4055        return array_filter($out);
4056    }
4057
4058    /**
4059     * Get a message UID by the Message-ID. Returns the last message in a
4060     * mailbox that matches.
4061     *
4062     * @param Horde_Imap_Client_Mailbox $mailbox  The mailbox to search
4063     * @param string $msgid                       Message-ID.
4064     *
4065     * @return string  UID (null if not found).
4066     */
4067    protected function _getUidByMessageId($mailbox, $msgid)
4068    {
4069        if (!$msgid) {
4070            return null;
4071        }
4072
4073        $query = new Horde_Imap_Client_Search_Query();
4074        $query->headerText('Message-ID', $msgid);
4075        $res = $this->search($mailbox, $query, array(
4076            'results' => array(Horde_Imap_Client::SEARCH_RESULTS_MAX)
4077        ));
4078
4079        return $res['max'];
4080    }
4081
4082}
4083