Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401]

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