Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403]

   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   */
  33  abstract class Horde_Imap_Client_Base
  34  implements 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  }