[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

/lib/horde/framework/Horde/Imap/Client/ -> Base.php (source)

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


Generated: Thu Aug 11 10:00:09 2016 Cross-referenced by PHPXref 0.7.1