[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

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

   1  <?php
   2  /**
   3   * Originally based on code from:
   4   *   - auth.php (1.49)
   5   *   - imap_general.php (1.212)
   6   *   - imap_messages.php (revision 13038)
   7   *   - strings.php (1.184.2.35)
   8   * from the Squirrelmail project.
   9   * Copyright (c) 1999-2007 The SquirrelMail Project Team
  10   *
  11   * Copyright 2005-2014 Horde LLC (http://www.horde.org/)
  12   *
  13   * See the enclosed file COPYING for license information (LGPL). If you
  14   * did not receive this file, see http://www.horde.org/licenses/lgpl21.
  15   *
  16   * @category  Horde
  17   * @copyright 1999-2007 The SquirrelMail Project Team
  18   * @copyright 2005-2014 Horde LLC
  19   * @license   http://www.horde.org/licenses/lgpl21 LGPL 2.1
  20   * @package   Imap_Client
  21   */
  22  
  23  /**
  24   * An interface to an IMAP4rev1 server (RFC 3501) using built-in PHP features.
  25   *
  26   * Implements the following IMAP-related RFCs (see
  27   * http://www.iana.org/assignments/imap4-capabilities):
  28   * <pre>
  29   *   - RFC 2086/4314: ACL
  30   *   - RFC 2087: QUOTA
  31   *   - RFC 2088: LITERAL+
  32   *   - RFC 2195: AUTH=CRAM-MD5
  33   *   - RFC 2221: LOGIN-REFERRALS
  34   *   - RFC 2342: NAMESPACE
  35   *   - RFC 2595/4616: TLS & AUTH=PLAIN
  36   *   - RFC 2831: DIGEST-MD5 authentication mechanism (obsoleted by RFC 6331)
  37   *   - RFC 2971: ID
  38   *   - RFC 3348: CHILDREN
  39   *   - RFC 3501: IMAP4rev1 specification
  40   *   - RFC 3502: MULTIAPPEND
  41   *   - RFC 3516: BINARY
  42   *   - RFC 3691: UNSELECT
  43   *   - RFC 4315: UIDPLUS
  44   *   - RFC 4422: SASL Authentication (for DIGEST-MD5)
  45   *   - RFC 4466: Collected extensions (updates RFCs 2088, 3501, 3502, 3516)
  46   *   - RFC 4469/5550: CATENATE
  47   *   - RFC 4731: ESEARCH
  48   *   - RFC 4959: SASL-IR
  49   *   - RFC 5032: WITHIN
  50   *   - RFC 5161: ENABLE
  51   *   - RFC 5182: SEARCHRES
  52   *   - RFC 5255: LANGUAGE/I18NLEVEL
  53   *   - RFC 5256: THREAD/SORT
  54   *   - RFC 5258: LIST-EXTENDED
  55   *   - RFC 5267: ESORT; PARTIAL search return option
  56   *   - RFC 5464: METADATA
  57   *   - RFC 5530: IMAP Response Codes
  58   *   - RFC 5819: LIST-STATUS
  59   *   - RFC 5957: SORT=DISPLAY
  60   *   - RFC 6154: SPECIAL-USE/CREATE-SPECIAL-USE
  61   *   - RFC 6203: SEARCH=FUZZY
  62   *   - RFC 6851: MOVE
  63   *   - RFC 6858: DOWNGRADED response code
  64   *   - RFC 7162: CONDSTORE/QRESYNC
  65   * </pre>
  66   *
  67   * Implements the following non-RFC extensions:
  68   * <pre>
  69   *   - draft-ietf-morg-inthread-01: THREAD=REFS
  70   *   - draft-daboo-imap-annotatemore-07: ANNOTATEMORE
  71   *   - draft-daboo-imap-annotatemore-08: ANNOTATEMORE2
  72   *   - XIMAPPROXY
  73   *     Requires imapproxy v1.2.7-rc1 or later
  74   *     See https://squirrelmail.svn.sourceforge.net/svnroot/squirrelmail/trunk/imap_proxy/README
  75   *   - AUTH=XOAUTH2
  76   *     https://developers.google.com/gmail/xoauth2_protocol
  77   * </pre>
  78   *
  79   * TODO (or not necessary?):
  80   * <pre>
  81   *   - RFC 2177: IDLE
  82   *     Probably not necessary due to the limited connection time of each
  83   *     HTTP/PHP request
  84   *   - RFC 2193: MAILBOX-REFERRALS
  85   *   - RFC 4467/5092/5524/5550/5593: URLAUTH, URLAUTH=BINARY, URL-PARTIAL
  86   *   - RFC 4978: COMPRESS=DEFLATE
  87   *     See: http://bugs.php.net/bug.php?id=48725
  88   *   - RFC 5257: ANNOTATE (Experimental)
  89   *   - RFC 5259: CONVERT
  90   *   - RFC 5267: CONTEXT=SEARCH; CONTEXT=SORT
  91   *   - RFC 5465: NOTIFY
  92   *   - RFC 5466: FILTERS
  93   *   - RFC 6237: MULTISEARCH (Experimental)
  94   *   - RFC 6855: UTF8
  95   * </pre>
  96   *
  97   * @author    Michael Slusarz <slusarz@horde.org>
  98   * @category  Horde
  99   * @copyright 1999-2007 The SquirrelMail Project Team
 100   * @copyright 2005-2014 Horde LLC
 101   * @license   http://www.horde.org/licenses/lgpl21 LGPL 2.1
 102   * @package   Imap_Client
 103   */
 104  class Horde_Imap_Client_Socket extends Horde_Imap_Client_Base
 105  {
 106      /**
 107       * Cache names used exclusively within this class.
 108       */
 109      const CACHE_FLAGS = 'HICflags';
 110  
 111      /**
 112       * Queued commands to send to the server.
 113       *
 114       * @var array
 115       */
 116      protected $_cmdQueue = array();
 117  
 118      /**
 119       * The default ports to use for a connection.
 120       *
 121       * @var array
 122       */
 123      protected $_defaultPorts = array(143, 993);
 124  
 125      /**
 126       * Mapping of status fields to IMAP names.
 127       *
 128       * @var array
 129       */
 130      protected $_statusFields = array(
 131          'messages' => Horde_Imap_Client::STATUS_MESSAGES,
 132          'recent' => Horde_Imap_Client::STATUS_RECENT,
 133          'uidnext' => Horde_Imap_Client::STATUS_UIDNEXT,
 134          'uidvalidity' => Horde_Imap_Client::STATUS_UIDVALIDITY,
 135          'unseen' => Horde_Imap_Client::STATUS_UNSEEN,
 136          'firstunseen' => Horde_Imap_Client::STATUS_FIRSTUNSEEN,
 137          'flags' => Horde_Imap_Client::STATUS_FLAGS,
 138          'permflags' => Horde_Imap_Client::STATUS_PERMFLAGS,
 139          'uidnotsticky' => Horde_Imap_Client::STATUS_UIDNOTSTICKY,
 140          'highestmodseq' => Horde_Imap_Client::STATUS_HIGHESTMODSEQ
 141      );
 142  
 143      /**
 144       * The unique tag to use when making an IMAP query.
 145       *
 146       * @var integer
 147       */
 148      protected $_tag = 0;
 149  
 150      /**
 151       * @param array $params  A hash containing configuration parameters.
 152       *                       Additional parameters to base driver:
 153       *   - debug_literal: (boolean) If true, will output the raw text of
 154       *                    literal responses to the debug stream. Otherwise,
 155       *                    outputs a summary of the literal response.
 156       *   - envelope_addrs: (integer) The maximum number of address entries to
 157       *                     read for FETCH ENVELOPE address fields.
 158       *                     DEFAULT: 1000
 159       *   - envelope_string: (integer) The maximum length of string fields
 160       *                      returned by the FETCH ENVELOPE command.
 161       *                      DEFAULT: 2048
 162       *   - xoauth2_token: (mixed) If set, will authenticate via the XOAUTH2
 163       *                    mechanism (if available) with this token. Either a
 164       *                    string (since 2.13.0) or a
 165       *                    Horde_Imap_Client_Base_Password object (since
 166       *                    2.14.0).
 167       */
 168      public function __construct(array $params = array())
 169      {
 170          parent::__construct(array_merge(array(
 171              'debug_literal' => false,
 172              'envelope_addrs' => 1000,
 173              'envelope_string' => 2048
 174          ), $params));
 175      }
 176  
 177      /**
 178       */
 179      public function getParam($key)
 180      {
 181          switch ($key) {
 182          case 'xoauth2_token':
 183              if (isset($this->_params[$key]) &&
 184                  ($this->_params[$key] instanceof Horde_Imap_Client_Base_Password)) {
 185                  return $this->_params[$key]->getPassword();
 186              }
 187              break;
 188          }
 189  
 190          return parent::getParam($key);
 191      }
 192  
 193      /**
 194       */
 195      protected function _capability()
 196      {
 197          // Need to use connect call here or else we run into loop issues
 198          // because _connect() can call capability() internally.
 199          $this->_connect();
 200  
 201          // It is possible the server provided capability information on
 202          // connect, so check for it now.
 203          if (!isset($this->_init['capability'])) {
 204              $this->_sendCmd($this->_command('CAPABILITY'));
 205          }
 206      }
 207  
 208      /**
 209       * Parse a CAPABILITY Response (RFC 3501 [7.2.1]).
 210       *
 211       * @param Horde_Imap_Client_Interaction_Pipeline $pipeline  Pipeline
 212       *                                                          object.
 213       * @param array $data  An array of CAPABILITY strings.
 214       */
 215      protected function _parseCapability(
 216          Horde_Imap_Client_Interaction_Pipeline $pipeline,
 217          $data
 218      )
 219      {
 220          if (!empty($this->_temp['no_cap'])) {
 221              return;
 222          }
 223  
 224          $pipeline->data['capability_set'] = true;
 225  
 226          $c = array();
 227  
 228          foreach ($data as $val) {
 229              $cap_list = explode('=', $val);
 230              $cap_list[0] = strtoupper($cap_list[0]);
 231              if (isset($cap_list[1])) {
 232                  if (!isset($c[$cap_list[0]]) || !is_array($c[$cap_list[0]])) {
 233                      $c[$cap_list[0]] = array();
 234                  }
 235                  $c[$cap_list[0]][] = $cap_list[1];
 236              } elseif (!isset($c[$cap_list[0]])) {
 237                  $c[$cap_list[0]] = true;
 238              }
 239          }
 240  
 241          $this->_setInit('capability', $c);
 242      }
 243  
 244      /**
 245       * Unsets a capability.
 246       *
 247       * @param string $cap  Capability to unset.
 248       */
 249      protected function _unsetCapability($cap)
 250      {
 251          $cap_list = $this->capability();
 252          unset($cap_list[$cap]);
 253          $this->_setInit('capability', $cap_list);
 254      }
 255  
 256      /**
 257       */
 258      protected function _noop()
 259      {
 260          // NOOP doesn't return any specific response
 261          $this->_sendCmd($this->_command('NOOP'));
 262      }
 263  
 264      /**
 265       */
 266      protected function _getNamespaces()
 267      {
 268          $data = $this->queryCapability('NAMESPACE')
 269              ? $this->_sendCmd($this->_command('NAMESPACE'))->data
 270              : array();
 271  
 272          return isset($data['namespace'])
 273              ? $data['namespace']
 274              : array();
 275      }
 276  
 277      /**
 278       * Parse a NAMESPACE response (RFC 2342 [5] & RFC 5255 [3.4]).
 279       *
 280       * @param Horde_Imap_Client_Interaction_Pipeline $pipeline  Pipeline
 281       *                                                          object.
 282       * @param Horde_Imap_Client_Tokenize $data  The NAMESPACE data.
 283       */
 284      protected function _parseNamespace(
 285          Horde_Imap_Client_Interaction_Pipeline $pipeline,
 286          Horde_Imap_Client_Tokenize $data
 287      )
 288      {
 289          $namespace_array = array(
 290              Horde_Imap_Client_Data_Namespace::NS_PERSONAL,
 291              Horde_Imap_Client_Data_Namespace::NS_OTHER,
 292              Horde_Imap_Client_Data_Namespace::NS_SHARED
 293          );
 294  
 295          $c = array();
 296  
 297          // Per RFC 2342, response from NAMESPACE command is:
 298          // (PERSONAL NAMESPACES) (OTHER_USERS NAMESPACE) (SHARED NAMESPACES)
 299          foreach ($namespace_array as $val) {
 300              $entry = $data->next();
 301  
 302              if (is_null($entry)) {
 303                  continue;
 304              }
 305  
 306              while ($data->next() !== false) {
 307                  $ob = Horde_Imap_Client_Mailbox::get($data->next(), true);
 308  
 309                  $ns = new Horde_Imap_Client_Data_Namespace();
 310                  $ns->delimiter = $data->next();
 311                  $ns->name = strval($ob);
 312                  $ns->type = $val;
 313                  $c[strval($ob)] = $ns;
 314  
 315                  // RFC 4466: NAMESPACE extensions
 316                  while (($ext = $data->next()) !== false) {
 317                      switch (strtoupper($ext)) {
 318                      case 'TRANSLATION':
 319                          // RFC 5255 [3.4] - TRANSLATION extension
 320                          $data->next();
 321                          $ns->translation = $data->next();
 322                          $data->next();
 323                          break;
 324                      }
 325                  }
 326              }
 327          }
 328  
 329          $pipeline->data['namespace'] = new Horde_Imap_Client_Namespace_List($c);
 330      }
 331  
 332      /**
 333       */
 334      public function alerts()
 335      {
 336          $alerts = empty($this->_temp['alerts'])
 337              ? array()
 338              : $this->_temp['alerts'];
 339          $this->_temp['alerts'] = array();
 340          return $alerts;
 341      }
 342  
 343      /**
 344       */
 345      protected function _login()
 346      {
 347          $secure = $this->getParam('secure');
 348  
 349          if (!empty($this->_temp['preauth'])) {
 350              unset($this->_temp['preauth']);
 351  
 352              /* Don't allow PREAUTH if we are requring secure access, since
 353               * PREAUTH cannot provide secure access. */
 354              if (!$this->isSecureConnection() && ($secure !== false)) {
 355                  $this->logout();
 356                  throw new Horde_Imap_Client_Exception(
 357                      Horde_Imap_Client_Translation::r("Could not open secure TLS connection to the IMAP server."),
 358                      Horde_Imap_Client_Exception::LOGIN_TLSFAILURE
 359                  );
 360              }
 361  
 362              return $this->_loginTasks();
 363          }
 364  
 365          /* Blank passwords are not allowed, so no need to even try
 366           * authentication to determine this. */
 367          if (is_null($this->getParam('password'))) {
 368              throw new Horde_Imap_Client_Exception(
 369                  Horde_Imap_Client_Translation::r("No password provided."),
 370                  Horde_Imap_Client_Exception::LOGIN_AUTHENTICATIONFAILED
 371              );
 372          }
 373  
 374          $this->_connect();
 375  
 376          $first_login = empty($this->_init['authmethod']);
 377  
 378          // Switch to secure channel if using TLS.
 379          if (!$this->isSecureConnection() &&
 380              (($secure === 'tls') ||
 381               (($secure === true) && $this->queryCapability('LOGINDISABLED')))) {
 382              if ($first_login && !$this->queryCapability('STARTTLS')) {
 383                  /* We should never hit this - STARTTLS is required pursuant to
 384                   * RFC 3501 [6.2.1]. */
 385                  throw new Horde_Imap_Client_Exception(
 386                      Horde_Imap_Client_Translation::r("Server does not support TLS connections."),
 387                      Horde_Imap_Client_Exception::LOGIN_TLSFAILURE
 388                  );
 389              }
 390  
 391              // Switch over to a TLS connection.
 392              // STARTTLS returns no untagged response.
 393              $this->_sendCmd($this->_command('STARTTLS'));
 394  
 395              if (!$this->_connection->startTls()) {
 396                  $this->logout();
 397                  throw new Horde_Imap_Client_Exception(
 398                      Horde_Imap_Client_Translation::r("Could not open secure TLS connection to the IMAP server."),
 399                      Horde_Imap_Client_Exception::LOGIN_TLSFAILURE
 400                  );
 401              }
 402  
 403              $this->_debug->info('Successfully completed TLS negotiation.');
 404  
 405              $this->setParam('secure', 'tls');
 406              $secure = 'tls';
 407  
 408              if ($first_login) {
 409                  // Expire cached CAPABILITY information (RFC 3501 [6.2.1])
 410                  $this->_setInit('capability');
 411  
 412                  // Reset language (RFC 5255 [3.1])
 413                  $this->_setInit('lang');
 414              }
 415  
 416              // Set language if using imapproxy
 417              if (!empty($this->_init['imapproxy'])) {
 418                  $this->setLanguage();
 419              }
 420          }
 421  
 422          /* If we reached this point and don't have a secure connection, then
 423           * a secure connections is not available. */
 424          if (($secure === true) && !$this->isSecureConnection()) {
 425              $this->setParam('secure', false);
 426              $secure = false;
 427          }
 428  
 429          if ($first_login) {
 430              // Add authentication methods.
 431              $auth_mech = array();
 432  
 433              $auth = ($auth = $this->queryCapability('AUTH'))
 434                  ? array_flip($auth)
 435                  : array();
 436  
 437              // XOAUTH2
 438              if (isset($auth['XOAUTH2']) && $this->getParam('xoauth2_token')) {
 439                  $auth_mech[] = 'XOAUTH2';
 440              }
 441              unset($auth['XOAUTH2']);
 442  
 443              /* 'PLAIN' authentication always exists if under TLS (RFC
 444               *  3501 [7.2.1]; RFC 2595). Use it over all other authentication
 445               *  methods, although we need to do sanity checking since broken
 446               *  IMAP servers may not support as required - fallback to
 447               *  LOGIN instead. */
 448              if ($secure) {
 449                  if (isset($auth['PLAIN'])) {
 450                      $auth_mech[] = 'PLAIN';
 451                      unset($auth['PLAIN']);
 452                  } else {
 453                      $auth_mech[] = 'LOGIN';
 454                  }
 455              }
 456  
 457              // Prefer CRAM-MD5 over DIGEST-MD5, as the latter has been
 458              // obsoleted (RFC 6331).
 459              if (isset($auth['CRAM-MD5'])) {
 460                  $auth_mech[] = 'CRAM-MD5';
 461              } elseif (isset($auth['DIGEST-MD5'])) {
 462                  $auth_mech[] = 'DIGEST-MD5';
 463              }
 464              unset($auth['CRAM-MD5'], $auth['DIGEST-MD5']);
 465  
 466              // Fall back to 'LOGIN' if available.
 467              $auth_mech = array_merge($auth_mech, array_keys($auth));
 468              if (!$secure && !$this->queryCapability('LOGINDISABLED')) {
 469                  $auth_mech[] = 'LOGIN';
 470              }
 471  
 472              if (empty($auth_mech)) {
 473                  throw new Horde_Imap_Client_Exception(
 474                      Horde_Imap_Client_Translation::r("No supported IMAP authentication method could be found."),
 475                      Horde_Imap_Client_Exception::LOGIN_NOAUTHMETHOD
 476                  );
 477              }
 478          } else {
 479              $auth_mech = array($this->_init['authmethod']);
 480          }
 481  
 482          $login_err = null;
 483  
 484          foreach ($auth_mech as $method) {
 485              try {
 486                  $resp = $this->_tryLogin($method);
 487                  $data = $resp->data;
 488                  $this->_setInit('authmethod', $method);
 489                  unset($this->_temp['referralcount']);
 490              } catch (Horde_Imap_Client_Exception_ServerResponse $e) {
 491                  $data = $e->resp_data;
 492                  if (isset($data['loginerr'])) {
 493                      $login_err = $data['loginerr'];
 494                  }
 495                  $resp = false;
 496              } catch (Horde_Imap_Client_Exception $e) {
 497                  $resp = false;
 498              }
 499  
 500              // Check for login referral (RFC 2221) response - can happen for
 501              // an OK, NO, or BYE response.
 502              if (isset($data['referral'])) {
 503                  foreach (array('hostspec', 'port', 'username') as $val) {
 504                      if (!is_null($data['referral']->$val)) {
 505                          $this->setParam($val, $data['referral']->$val);
 506                      }
 507                  }
 508  
 509                  if (!is_null($data['referral']->auth)) {
 510                      $this->_setInit('authmethod', $data['referral']->auth);
 511                  }
 512  
 513                  if (!isset($this->_temp['referralcount'])) {
 514                      $this->_temp['referralcount'] = 0;
 515                  }
 516  
 517                  // RFC 2221 [3] - Don't follow more than 10 levels of referral
 518                  // without consulting the user.
 519                  if (++$this->_temp['referralcount'] < 10) {
 520                      $this->logout();
 521                      $this->_setInit('capability');
 522                      $this->_setInit('namespace', array());
 523                      return $this->login();
 524                  }
 525  
 526                  unset($this->_temp['referralcount']);
 527              }
 528  
 529              if ($resp) {
 530                  return $this->_loginTasks($first_login, $resp->data);
 531              }
 532          }
 533  
 534          /* Try again from scratch if authentication failed in an established,
 535           * previously-authenticated object. */
 536          if (!empty($this->_init['authmethod'])) {
 537              $this->_setInit();
 538              unset($this->_temp['no_cap']);
 539              try {
 540                  return $this->_login();
 541              } catch (Horde_Imap_Client_Exception $e) {}
 542          }
 543  
 544          /* Default to AUTHENTICATIONFAILED error (see RFC 5530[3]). */
 545          if (is_null($login_err)) {
 546              throw new Horde_Imap_Client_Exception(
 547                  Horde_Imap_Client_Translation::r("Mail server denied authentication."),
 548                  Horde_Imap_Client_Exception::LOGIN_AUTHENTICATIONFAILED
 549              );
 550          }
 551  
 552          throw $login_err;
 553      }
 554  
 555      /**
 556       * Connects to the IMAP server.
 557       *
 558       * @throws Horde_Imap_Client_Exception
 559       */
 560      protected function _connect()
 561      {
 562          if (!is_null($this->_connection)) {
 563              return;
 564          }
 565  
 566          try {
 567              $this->_connection = new Horde_Imap_Client_Socket_Connection_Socket(
 568                  $this->getParam('hostspec'),
 569                  $this->getParam('port'),
 570                  $this->getParam('timeout'),
 571                  $this->getParam('secure'),
 572                  array(
 573                      'debug' => $this->_debug,
 574                      'debugliteral' => $this->getParam('debug_literal')
 575                  )
 576              );
 577          } catch (Horde\Socket\Client\Exception $e) {
 578              $e2 = new Horde_Imap_Client_Exception(
 579                  Horde_Imap_Client_Translation::r("Error connecting to mail server."),
 580                  Horde_Imap_Client_Exception::SERVER_CONNECT
 581              );
 582              $e2->details = $e->details;
 583              throw $e2;
 584          }
 585  
 586          // If we already have capability information, don't re-set with
 587          // (possibly) limited information sent in the initial banner.
 588          if (isset($this->_init['capability'])) {
 589              $this->_temp['no_cap'] = true;
 590          }
 591  
 592          /* Get greeting information (untagged response). */
 593          try {
 594              $this->_getLine($this->_pipeline());
 595          } catch (Horde_Imap_Client_Exception_ServerResponse $e) {
 596              if ($e->status === Horde_Imap_Client_Interaction_Server::BYE) {
 597                  /* Server is explicitly rejecting our connection (RFC 3501
 598                   * [7.1.5]). */
 599                  $e->setMessage(Horde_Imap_Client_Translation::r("Server rejected connection."));
 600                  $e->setCode(Horde_Imap_Client_Exception::SERVER_CONNECT);
 601              }
 602              throw $e;
 603          }
 604  
 605          // Check for IMAP4rev1 support
 606          if (!$this->queryCapability('IMAP4REV1')) {
 607              throw new Horde_Imap_Client_Exception(
 608                  Horde_Imap_Client_Translation::r("The mail server does not support IMAP4rev1 (RFC 3501)."),
 609                  Horde_Imap_Client_Exception::SERVER_CONNECT
 610              );
 611          }
 612  
 613          // Set language if NOT using imapproxy
 614          if (empty($this->_init['imapproxy'])) {
 615              if ($this->queryCapability('XIMAPPROXY')) {
 616                  $this->_setInit('imapproxy', true);
 617              } else {
 618                  $this->setLanguage();
 619              }
 620          }
 621  
 622          // If pre-authenticated, we need to do all login tasks now.
 623          if (!empty($this->_temp['preauth'])) {
 624              $this->login();
 625          }
 626      }
 627  
 628      /**
 629       * Authenticate to the IMAP server.
 630       *
 631       * @param string $method  IMAP login method.
 632       *
 633       * @return Horde_Imap_Client_Interaction_Pipeline  Pipeline object.
 634       *
 635       * @throws Horde_Imap_Client_Exception
 636       */
 637      protected function _tryLogin($method)
 638      {
 639          $username = $this->getParam('username');
 640          $password = $this->getParam('password');
 641  
 642          $authenticate_cmd = false;
 643  
 644          switch ($method) {
 645          case 'CRAM-MD5':
 646          case 'CRAM-SHA1':
 647          case 'CRAM-SHA256':
 648              // RFC 2195: CRAM-MD5
 649              // CRAM-SHA1 & CRAM-SHA256 supported by Courier SASL library
 650  
 651              // Need $args because PHP 5.3 doesn't allow access to $this in
 652              // anonymous functions.
 653              $args = array(
 654                  $username,
 655                  strtolower(substr($method, 5)),
 656                  $password
 657              );
 658  
 659              $cmd = $this->_command('AUTHENTICATE')->add(array(
 660                  $method,
 661                  new Horde_Imap_Client_Interaction_Command_Continuation(function($ob) use ($args) {
 662                      return new Horde_Imap_Client_Data_Format_List(
 663                          base64_encode($args[0] . ' ' . hash_hmac($args[1], base64_decode($ob->token->current()), $args[2], false))
 664                      );
 665                  })
 666              ));
 667              $cmd->debug = sprintf('[AUTHENTICATE %s Command - username: %s]', $method, $username);
 668              break;
 669  
 670          case 'DIGEST-MD5':
 671              // RFC 2831/4422; obsoleted by RFC 6331
 672  
 673              // Need $args because PHP 5.3 doesn't allow access to $this in
 674              // anonymous functions.
 675              $args = array(
 676                  $username,
 677                  $password,
 678                  $this->getParam('hostspec')
 679              );
 680  
 681              $cmd = $this->_command('AUTHENTICATE')->add(array(
 682                  $method,
 683                  new Horde_Imap_Client_Interaction_Command_Continuation(function($ob) use ($args) {
 684                      return new Horde_Imap_Client_Data_Format_List(
 685                          base64_encode(new Horde_Imap_Client_Auth_DigestMD5(
 686                              $args[0],
 687                              $args[1],
 688                              base64_decode($ob->token->current()),
 689                              $args[2],
 690                              'imap'
 691                          ))
 692                      );
 693                  }),
 694                  new Horde_Imap_Client_Interaction_Command_Continuation(function($ob) {
 695                      if (strpos(base64_decode($ob->token->current()), 'rspauth=') === false) {
 696                          throw new Horde_Imap_Client_Exception(
 697                              Horde_Imap_Client_Translation::r("Unexpected response from server when authenticating."),
 698                              Horde_Imap_Client_Exception::SERVER_CONNECT
 699                          );
 700                      }
 701  
 702                      return new Horde_Imap_Client_Data_Format_List();
 703                  })
 704              ));
 705              $cmd->debug = sprintf('[AUTHENTICATE DIGEST-MD5 Command - username: %s]', $username);
 706              break;
 707  
 708          case 'LOGIN':
 709              $cmd = $this->_command('LOGIN')->add(array(
 710                  new Horde_Imap_Client_Data_Format_Astring($username),
 711                  new Horde_Imap_Client_Data_Format_Astring($password)
 712              ));
 713              $cmd->debug = sprintf('[LOGIN Command - username: %s]', $username);
 714              break;
 715  
 716          case 'PLAIN':
 717              // RFC 2595/4616 - PLAIN SASL mechanism
 718              $auth = base64_encode(implode("\0", array(
 719                  $username,
 720                  $username,
 721                  $password
 722              )));
 723              $authenticate_cmd = true;
 724              break;
 725  
 726          case 'XOAUTH2':
 727              // Google XOAUTH2
 728              $auth = $this->getParam('xoauth2_token');
 729              $authenticate_cmd = true;
 730              break;
 731  
 732          default:
 733              throw new Horde_Imap_Client_Exception(
 734                  sprintf(Horde_Imap_Client_Translation::r("Unknown authentication method: %s"), $method),
 735                  Horde_Imap_Client_Exception::SERVER_CONNECT
 736              );
 737          }
 738  
 739          if ($authenticate_cmd) {
 740              $cmd = $this->_command('AUTHENTICATE')->add($method);
 741  
 742              if ($this->queryCapability('SASL-IR')) {
 743                  // IMAP Extension for SASL Initial Client Response (RFC 4959)
 744                  $cmd->add($auth);
 745                  $cmd->debug = sprintf('[SASL-IR AUTHENTICATE Command - method: %s, username: %s]', $method, $username);
 746              } else {
 747                  $cmd->add(new Horde_Imap_Client_Interaction_Command_Continuation(function($ob) use ($auth) {
 748                      return new Horde_Imap_Client_Data_Format_List($auth);
 749                  }));
 750                  $cmd->debug = sprintf('[AUTHENTICATE Command - method: %s, username: %s]', $method, $username);
 751              }
 752  
 753              /* This is an optional command continuation. E.g. XOAUTH2 will
 754               * return error information in continuation response. */
 755              $error_continuation = new Horde_Imap_Client_Interaction_Command_Continuation(function($ob) {
 756                  return new Horde_Imap_Client_Data_Format_List();
 757              });
 758              $error_continuation->optional = true;
 759              $cmd->add($error_continuation);
 760          }
 761  
 762          return $this->_sendCmd($this->_pipeline($cmd));
 763      }
 764  
 765      /**
 766       * Perform login tasks.
 767       *
 768       * @param boolean $firstlogin  Is this the first login?
 769       * @param array $resp          The data response from the login command.
 770       *                             May include:
 771       *   - capability_set: (boolean) True if CAPABILITY was set after login.
 772       *   - proxyreuse: (boolean) True if re-used connection via imapproxy.
 773       *
 774       * @return boolean  True if global login tasks should be performed.
 775       */
 776      protected function _loginTasks($firstlogin = true, array $resp = array())
 777      {
 778          /* If reusing an imapproxy connection, no need to do any of these
 779           * login tasks again. */
 780          if (!$firstlogin && !empty($resp['proxyreuse'])) {
 781              if (isset($this->_init['enabled'])) {
 782                  $this->_temp['enabled'] = $this->_init['enabled'];
 783              }
 784  
 785              // If we have not yet set the language, set it now.
 786              if (!isset($this->_init['lang'])) {
 787                  $this->_temp['lang_queue'] = true;
 788                  $this->setLanguage();
 789                  unset($this->_temp['lang_queue']);
 790              }
 791              return false;
 792          }
 793  
 794          /* If we logged in for first time, and server did not return
 795           * capability information, we need to mark for retrieval. */
 796          if ($firstlogin && empty($resp['capability_set'])) {
 797              $this->_setInit('capability');
 798          }
 799  
 800          $this->_temp['lang_queue'] = true;
 801          $this->setLanguage();
 802          unset($this->_temp['lang_queue']);
 803  
 804          /* Only active QRESYNC/CONDSTORE if caching is enabled. */
 805          if ($this->_initCache()) {
 806              if ($this->queryCapability('QRESYNC')) {
 807                  $this->_enable(array('QRESYNC'));
 808              } elseif ($this->queryCapability('CONDSTORE')) {
 809                  $this->_enable(array('CONDSTORE'));
 810              }
 811          }
 812  
 813          return true;
 814      }
 815  
 816      /**
 817       */
 818      protected function _logout()
 819      {
 820          if (empty($this->_temp['logout'])) {
 821              /* If using imapproxy, force sending these commands, since they
 822               * may not be sent again if they are (likely) initialization
 823               * commands. */
 824              if (!empty($this->_cmdQueue) &&
 825                  !empty($this->_init['imapproxy'])) {
 826                  $this->_sendCmd($this->_pipeline());
 827              }
 828  
 829              $this->_temp['logout'] = true;
 830              try {
 831                  $this->_sendCmd($this->_command('LOGOUT'));
 832              } catch (Horde_Imap_Client_Exception_ServerResponse $e) {
 833                  // Ignore server errors
 834              }
 835              unset($this->_temp['logout']);
 836          }
 837      }
 838  
 839      /**
 840       */
 841      protected function _sendID($info)
 842      {
 843          $cmd = $this->_command('ID');
 844  
 845          if (empty($info)) {
 846              $cmd->add(new Horde_Imap_Client_Data_Format_Nil());
 847          } else {
 848              $tmp = new Horde_Imap_Client_Data_Format_List();
 849              foreach ($info as $key => $val) {
 850                  $tmp->add(array(
 851                      new Horde_Imap_Client_Data_Format_String(strtolower($key)),
 852                      new Horde_Imap_Client_Data_Format_Nstring($val)
 853                  ));
 854              }
 855              $cmd->add($tmp);
 856          }
 857  
 858          $this->_temp['id'] = $this->_sendCmd($cmd)->data['id'];
 859      }
 860  
 861      /**
 862       * Parse an ID response (RFC 2971 [3.2]).
 863       *
 864       * @param Horde_Imap_Client_Interaction_Pipeline $pipeline  Pipeline
 865       *                                                          object.
 866       * @param Horde_Imap_Client_Tokenize $data  The server response.
 867       */
 868      protected function _parseID(
 869          Horde_Imap_Client_Interaction_Pipeline $pipeline,
 870          Horde_Imap_Client_Tokenize $data
 871      )
 872      {
 873          $ids = array();
 874  
 875          if (!is_null($data->next())) {
 876              while (($curr = $data->next()) !== false) {
 877                  if (!is_null($id = $data->next())) {
 878                      $ids[$curr] = $id;
 879                  }
 880              }
 881          }
 882  
 883          $pipeline->data['id'] = $ids;
 884      }
 885  
 886      /**
 887       */
 888      protected function _getID()
 889      {
 890          if (!isset($this->_temp['id'])) {
 891              $this->sendID();
 892          }
 893  
 894          return $this->_temp['id'];
 895      }
 896  
 897      /**
 898       */
 899      protected function _setLanguage($langs)
 900      {
 901          $cmd = $this->_command('LANGUAGE');
 902          foreach ($langs as $lang) {
 903              $cmd->add(new Horde_Imap_Client_Data_Format_Astring($lang));
 904          }
 905  
 906          if (!empty($this->_temp['lang_queue'])) {
 907              $this->_cmdQueue[] = $cmd;
 908              return array();
 909          }
 910  
 911          try {
 912              $this->_sendCmd($cmd);
 913          } catch (Horde_Imap_Client_Exception $e) {
 914              $this->_setInit('lang', false);
 915              return null;
 916          }
 917  
 918          return $this->_init['lang'];
 919      }
 920  
 921      /**
 922       */
 923      protected function _getLanguage($list)
 924      {
 925          if (!$list) {
 926              return empty($this->_init['lang'])
 927                  ? null
 928                  : $this->_init['lang'];
 929          }
 930  
 931          if (!isset($this->_init['langavail'])) {
 932              try {
 933                  $this->_sendCmd($this->_command('LANGUAGE'));
 934              } catch (Horde_Imap_Client_Exception $e) {
 935                  $this->_setInit('langavail', array());
 936              }
 937          }
 938  
 939          return $this->_init['langavail'];
 940      }
 941  
 942      /**
 943       * Parse a LANGUAGE response (RFC 5255 [3.3]).
 944       *
 945       * @param Horde_Imap_Client_Tokenize $data  The server response.
 946       */
 947      protected function _parseLanguage(Horde_Imap_Client_Tokenize $data)
 948      {
 949          $lang_list = $data->flushIterator();
 950  
 951          if (count($lang_list) === 1) {
 952              // This is the language that was set.
 953              $this->_setInit('lang', reset($lang_list));
 954          } else {
 955              // These are the languages that are available.
 956              $this->_setInit('langavail', $lang_list);
 957          }
 958      }
 959  
 960      /**
 961       * Enable an IMAP extension (see RFC 5161).
 962       *
 963       * @param array $exts  The extensions to enable.
 964       *
 965       * @throws Horde_Imap_Client_Exception
 966       */
 967      protected function _enable($exts)
 968      {
 969          if ($this->queryCapability('ENABLE')) {
 970              // Only enable non-enabled extensions.
 971              $exts = array_diff($exts, array_keys($this->_temp['enabled']));
 972              if (!empty($exts)) {
 973                  $this->_cmdQueue[] = $this->_command('ENABLE')->add($exts);
 974                  $this->_enabled($exts, 1);
 975              }
 976          }
 977      }
 978  
 979      /**
 980       * Parse an ENABLED response (RFC 5161 [3.2]).
 981       *
 982       * @param Horde_Imap_Client_Tokenize $data  The server response.
 983       */
 984      protected function _parseEnabled(Horde_Imap_Client_Tokenize $data)
 985      {
 986          $this->_enabled($data->flushIterator(), 2);
 987      }
 988  
 989      /**
 990       */
 991      protected function _enabled($exts, $status)
 992      {
 993          parent::_enabled($exts, $status);
 994  
 995          if (($status == 2) && !empty($this->_init['imapproxy'])) {
 996              $this->_setInit('enabled', $this->_temp['enabled']);
 997          }
 998      }
 999  
1000      /**
1001       */
1002      protected function _openMailbox(Horde_Imap_Client_Mailbox $mailbox, $mode)
1003      {
1004          $qresync = isset($this->_temp['enabled']['QRESYNC']);
1005  
1006          $cmd = $this->_command(
1007              ($mode == Horde_Imap_Client::OPEN_READONLY) ? 'EXAMINE' : 'SELECT'
1008          )->add(
1009              new Horde_Imap_Client_Data_Format_Mailbox($mailbox)
1010          );
1011          $pipeline = $this->_pipeline($cmd);
1012  
1013          /* If QRESYNC is available, synchronize the mailbox. */
1014          if ($qresync) {
1015              $this->_initCache();
1016              $md = $this->_cache->getMetaData($mailbox, null, array(self::CACHE_MODSEQ, 'uidvalid'));
1017  
1018              if (isset($md[self::CACHE_MODSEQ])) {
1019                  if ($uids = $this->_cache->get($mailbox)) {
1020                      $uids = $this->getIdsOb($uids);
1021  
1022                      /* Check for extra long UID string. Assume that any
1023                       * server that can handle QRESYNC can also handle long
1024                       * input strings (at least 8 KB), so 7 KB is as good as
1025                       * any guess as to an upper limit. If this occurs, provide
1026                       * a range string (min -> max) instead. */
1027                      if (strlen($uid_str = $uids->tostring_sort) > 7000) {
1028                          $uid_str = $uids->range_string;
1029                      }
1030                  } else {
1031                      $uid_str = null;
1032                  }
1033  
1034                  /* Several things can happen with a QRESYNC:
1035                   * 1. UIDVALIDITY may have changed.  If so, we need to expire
1036                   * the cache immediately (done below).
1037                   * 2. NOMODSEQ may have been returned. We can keep current
1038                   * message cache data but won't be able to do flag caching.
1039                   * 3. VANISHED/FETCH information was returned. These responses
1040                   * will have already been handled by those response handlers.
1041                   * 4. We are already synced with the local server in which
1042                   * case it acts like a normal EXAMINE/SELECT. */
1043                  $cmd->add(new Horde_Imap_Client_Data_Format_List(array(
1044                      'QRESYNC',
1045                      new Horde_Imap_Client_Data_Format_List(array_filter(array(
1046                          $md['uidvalid'],
1047                          $md[self::CACHE_MODSEQ],
1048                          $uid_str
1049                      )))
1050                  )));
1051              }
1052  
1053              /* Let the 'CLOSED' response code handle mailbox switching if
1054               * QRESYNC is active. */
1055              if ($this->_selected) {
1056                  $pipeline->data['qresyncmbox'] = array($mailbox, $mode);
1057              } else {
1058                  $this->_changeSelected($mailbox, $mode);
1059              }
1060          } else {
1061              if (!isset($this->_temp['enabled']['CONDSTORE']) &&
1062                  $this->_initCache() &&
1063                  $this->queryCapability('CONDSTORE')) {
1064                  /* Activate CONDSTORE now if ENABLE is not available. */
1065                  $cmd->add(new Horde_Imap_Client_Data_Format_List('CONDSTORE'));
1066                  $this->_enabled(array('CONDSTORE'), 2);
1067              }
1068  
1069              $this->_changeSelected($mailbox, $mode);
1070          }
1071  
1072          try {
1073              $this->_sendCmd($pipeline);
1074          } catch (Horde_Imap_Client_Exception_ServerResponse $e) {
1075              // An EXAMINE/SELECT failure with a return of 'NO' will cause the
1076              // current mailbox to be unselected.
1077              if ($e->status === Horde_Imap_Client_Interaction_Server::NO) {
1078                  $this->_changeSelected(null);
1079                  $this->_mode = 0;
1080                  if (!$e->getCode()) {
1081                      throw new Horde_Imap_Client_Exception(
1082                          sprintf(Horde_Imap_Client_Translation::r("Could not open mailbox \"%s\"."), $mailbox),
1083                          Horde_Imap_Client_Exception::MAILBOX_NOOPEN
1084                      );
1085                  }
1086              }
1087              throw $e;
1088          }
1089  
1090          if ($qresync) {
1091              /* Mailbox is fully sync'd. */
1092              $this->_mailboxOb()->sync = true;
1093          }
1094      }
1095  
1096      /**
1097       */
1098      protected function _createMailbox(Horde_Imap_Client_Mailbox $mailbox, $opts)
1099      {
1100          $cmd = $this->_command('CREATE')->add(
1101              new Horde_Imap_Client_Data_Format_Mailbox($mailbox)
1102          );
1103  
1104          if (!empty($opts['special_use'])) {
1105              $cmd->add(array(
1106                  'USE',
1107                  new Horde_Imap_Client_Data_Format_List($opts['special_use'])
1108              ));
1109          }
1110  
1111          // CREATE returns no untagged information (RFC 3501 [6.3.3])
1112          $this->_sendCmd($cmd);
1113      }
1114  
1115      /**
1116       */
1117      protected function _deleteMailbox(Horde_Imap_Client_Mailbox $mailbox)
1118      {
1119          // Some IMAP servers will not allow a delete of a currently open
1120          // mailbox.
1121          if ($mailbox->equals($this->_selected)) {
1122              $this->close();
1123          }
1124  
1125          $cmd = $this->_command('DELETE')->add(
1126              new Horde_Imap_Client_Data_Format_Mailbox($mailbox)
1127          );
1128  
1129          try {
1130              // DELETE returns no untagged information (RFC 3501 [6.3.4])
1131              $this->_sendCmd($cmd);
1132          } catch (Horde_Imap_Client_Exception $e) {
1133              // Some IMAP servers won't allow a mailbox delete unless all
1134              // messages in that mailbox are deleted.
1135              $this->expunge($mailbox, array(
1136                  'delete' => true
1137              ));
1138              $this->_sendCmd($cmd);
1139          }
1140      }
1141  
1142      /**
1143       */
1144      protected function _renameMailbox(Horde_Imap_Client_Mailbox $old,
1145                                        Horde_Imap_Client_Mailbox $new)
1146      {
1147          // Some IMAP servers will not allow a rename of a currently open
1148          // mailbox.
1149          if ($old->equals($this->_selected)) {
1150              $this->close();
1151          }
1152  
1153          // RENAME returns no untagged information (RFC 3501 [6.3.5])
1154          $this->_sendCmd(
1155              $this->_command('RENAME')->add(array(
1156                  new Horde_Imap_Client_Data_Format_Mailbox($old),
1157                  new Horde_Imap_Client_Data_Format_Mailbox($new)
1158              ))
1159          );
1160      }
1161  
1162      /**
1163       */
1164      protected function _subscribeMailbox(Horde_Imap_Client_Mailbox $mailbox,
1165                                           $subscribe)
1166      {
1167          // SUBSCRIBE/UNSUBSCRIBE returns no untagged information (RFC 3501
1168          // [6.3.6 & 6.3.7])
1169          $this->_sendCmd(
1170              $this->_command(
1171                  $subscribe ? 'SUBSCRIBE' : 'UNSUBSCRIBE'
1172              )->add(
1173                  new Horde_Imap_Client_Data_Format_Mailbox($mailbox)
1174              )
1175          );
1176      }
1177  
1178      /**
1179       */
1180      protected function _listMailboxes($pattern, $mode, $options)
1181      {
1182          // RFC 5258 [3.1]: Use LSUB for MBOX_SUBSCRIBED if no other server
1183          // return options are specified.
1184          if (($mode == Horde_Imap_Client::MBOX_SUBSCRIBED) &&
1185              empty($options['attributes']) &&
1186              empty($options['children']) &&
1187              empty($options['recursivematch']) &&
1188              empty($options['remote']) &&
1189              empty($options['special_use']) &&
1190              empty($options['status'])) {
1191              return $this->_getMailboxList(
1192                  $pattern,
1193                  Horde_Imap_Client::MBOX_SUBSCRIBED,
1194                  array(
1195                      'delimiter' => !empty($options['delimiter']),
1196                      'flat' => !empty($options['flat']),
1197                      'no_listext' => true
1198                  )
1199              );
1200          }
1201  
1202          // Get the list of subscribed/unsubscribed mailboxes. Since LSUB is
1203          // not guaranteed to have correct attributes, we must use LIST to
1204          // ensure we receive the correct information.
1205          if (($mode != Horde_Imap_Client::MBOX_ALL) &&
1206              !$this->queryCapability('LIST-EXTENDED')) {
1207              $subscribed = $this->_getMailboxList($pattern, Horde_Imap_Client::MBOX_SUBSCRIBED, array('flat' => true));
1208  
1209              // If mode is subscribed, and 'flat' option is true, we can
1210              // return now.
1211              if (($mode == Horde_Imap_Client::MBOX_SUBSCRIBED) &&
1212                  !empty($options['flat'])) {
1213                  return $subscribed;
1214              }
1215          } else {
1216              $subscribed = null;
1217          }
1218  
1219          return $this->_getMailboxList($pattern, $mode, $options, $subscribed);
1220      }
1221  
1222      /**
1223       * Obtain a list of mailboxes.
1224       *
1225       * @param array $pattern     The mailbox search pattern(s).
1226       * @param integer $mode      Which mailboxes to return.
1227       * @param array $options     Additional options. 'no_listext' will skip
1228       *                           using the LIST-EXTENDED capability.
1229       * @param array $subscribed  A list of subscribed mailboxes.
1230       *
1231       * @return array  See listMailboxes(().
1232       *
1233       * @throws Horde_Imap_Client_Exception
1234       */
1235      protected function _getMailboxList($pattern, $mode, $options,
1236                                         $subscribed = null)
1237      {
1238          $check = (($mode != Horde_Imap_Client::MBOX_ALL) && !is_null($subscribed));
1239  
1240          // Setup entry for use in _parseList().
1241          $pipeline = $this->_pipeline();
1242          $pipeline->data['mailboxlist'] = array(
1243              'check' => $check,
1244              'ext' => false,
1245              'options' => $options,
1246              'subexist' => ($mode == Horde_Imap_Client::MBOX_SUBSCRIBED_EXISTS),
1247              /* Can't use array_merge here because it will destroy any mailbox
1248               * name (key) that is "numeric". */
1249              'subscribed' => ($check ? (array_flip(array_map('strval', $subscribed)) + array('INBOX' => true)) : null)
1250          );
1251          $pipeline->data['listresponse'] = array();
1252  
1253          $cmds = array();
1254          $return_opts = new Horde_Imap_Client_Data_Format_List();
1255  
1256          if ($this->queryCapability('LIST-EXTENDED') &&
1257              empty($options['no_listext'])) {
1258              $cmd = $this->_command('LIST');
1259              $pipeline->data['mailboxlist']['ext'] = true;
1260  
1261              $select_opts = new Horde_Imap_Client_Data_Format_List();
1262              $subscribed = false;
1263  
1264              if (($mode == Horde_Imap_Client::MBOX_SUBSCRIBED) ||
1265                  ($mode == Horde_Imap_Client::MBOX_SUBSCRIBED_EXISTS)) {
1266                  $select_opts->add('SUBSCRIBED');
1267                  $return_opts->add('SUBSCRIBED');
1268                  $subscribed = true;
1269              }
1270  
1271              if (!empty($options['remote'])) {
1272                  $select_opts->add('REMOTE');
1273              }
1274  
1275              if (!empty($options['recursivematch'])) {
1276                  $select_opts->add('RECURSIVEMATCH');
1277              }
1278  
1279              $cmd->add(array(
1280                  $select_opts,
1281                  ''
1282              ));
1283  
1284              $tmp = new Horde_Imap_Client_Data_Format_List();
1285              foreach ($pattern as $val) {
1286                  if ($subscribed && (strcasecmp($val, 'INBOX') === 0)) {
1287                      $cmds[] = $this->_command('LIST')->add(array(
1288                          '',
1289                          'INBOX'
1290                      ));
1291                  } else {
1292                      $tmp->add(new Horde_Imap_Client_Data_Format_ListMailbox($val));
1293                  }
1294              }
1295  
1296              if (count($tmp)) {
1297                  $cmd->add($tmp);
1298                  $cmds[] = $cmd;
1299              }
1300  
1301              if (!empty($options['children'])) {
1302                  $return_opts->add('CHILDREN');
1303              }
1304  
1305              if (!empty($options['special_use'])) {
1306                  $return_opts->add('SPECIAL-USE');
1307              }
1308          } else {
1309              foreach ($pattern as $val) {
1310                  $cmds[] = $this->_command(
1311                      ($mode == Horde_Imap_Client::MBOX_SUBSCRIBED) ? 'LSUB' : 'LIST'
1312                  )->add(array(
1313                      '',
1314                      new Horde_Imap_Client_Data_Format_ListMailbox($val)
1315                  ));
1316              }
1317          }
1318  
1319          /* LIST-STATUS does NOT depend on LIST-EXTENDED. */
1320          if (!empty($options['status']) &&
1321              $this->queryCapability('LIST-STATUS')) {
1322              $available_status = array(
1323                  Horde_Imap_Client::STATUS_MESSAGES,
1324                  Horde_Imap_Client::STATUS_RECENT,
1325                  Horde_Imap_Client::STATUS_UIDNEXT,
1326                  Horde_Imap_Client::STATUS_UIDVALIDITY,
1327                  Horde_Imap_Client::STATUS_UNSEEN,
1328                  Horde_Imap_Client::STATUS_HIGHESTMODSEQ
1329              );
1330  
1331              $status_opts = array();
1332              foreach (array_intersect($this->_statusFields, $available_status) as $key => $val) {
1333                  if ($options['status'] & $val) {
1334                      $status_opts[] = $key;
1335                  }
1336              }
1337  
1338              if (count($status_opts)) {
1339                  $return_opts->add(array(
1340                      'STATUS',
1341                      new Horde_Imap_Client_Data_Format_List(
1342                          array_map('strtoupper', $status_opts)
1343                      )
1344                  ));
1345              }
1346          }
1347  
1348          foreach ($cmds as $val) {
1349              if (count($return_opts)) {
1350                  $val->add(array(
1351                      'RETURN',
1352                      $return_opts
1353                  ));
1354              }
1355  
1356              $pipeline->add($val);
1357          }
1358  
1359          try {
1360              $lr = $this->_sendCmd($pipeline)->data['listresponse'];
1361          } catch (Horde_Imap_Client_Exception_ServerResponse $e) {
1362              /* Archiveopteryx 3.1.3 can't process empty list-select-opts list.
1363               * Retry using base IMAP4rev1 functionality. */
1364              if (($e->status === Horde_Imap_Client_Interaction_Server::BAD) &&
1365                  $this->queryCapability('LIST-EXTENDED')) {
1366                  $this->_unsetCapability('LIST-EXTENDED');
1367                  return $this->_listMailboxes($pattern, $mode, $options);
1368              }
1369  
1370              throw $e;
1371          }
1372  
1373          if (!empty($options['flat'])) {
1374              return array_values($lr);
1375          }
1376  
1377          /* Add in STATUS return, if needed. */
1378          if (!empty($options['status'])) {
1379              foreach ($pattern as $val) {
1380                  $val_utf8 = Horde_Imap_Client_Utf7imap::Utf7ImapToUtf8($val);
1381                  if (isset($lr[$val_utf8])) {
1382                      $lr[$val_utf8]['status'] = $this->_prepareStatusResponse($status_opts, $val_utf8);
1383                  }
1384              }
1385          }
1386  
1387          return $lr;
1388      }
1389  
1390      /**
1391       * Parse a LIST/LSUB response (RFC 3501 [7.2.2 & 7.2.3]).
1392       *
1393       * @param Horde_Imap_Client_Interaction_Pipeline $pipeline  Pipeline
1394       *                                                          object.
1395       * @param Horde_Imap_Client_Tokenize $data  The server response (includes
1396       *                                          type as first token).
1397       *
1398       * @throws Horde_Imap_Client_Exception
1399       */
1400      protected function _parseList(
1401          Horde_Imap_Client_Interaction_Pipeline $pipeline,
1402          Horde_Imap_Client_Tokenize $data
1403      )
1404      {
1405          $data->next();
1406          $attr = $data->flushIterator();
1407          $delimiter = $data->next();
1408          $mbox = Horde_Imap_Client_Mailbox::get($data->next(), true);
1409          $ml = $pipeline->data['mailboxlist'];
1410  
1411          if ($ml['check'] &&
1412              $ml['subexist'] &&
1413              // Subscribed list is in UTF-8.
1414              !isset($ml['subscribed'][strval($mbox)])) {
1415              return;
1416          } elseif ((!$ml['check'] && $ml['subexist']) ||
1417                     (empty($ml['options']['flat']) &&
1418                      !empty($ml['options']['attributes']))) {
1419              $attr = array_flip(array_map('strtolower', $attr));
1420              if ($ml['subexist'] &&
1421                  !$ml['check'] &&
1422                  isset($attr['\\nonexistent'])) {
1423                  return;
1424              }
1425          }
1426  
1427          if (empty($ml['options']['flat'])) {
1428              $tmp = array(
1429                  'mailbox' => $mbox
1430              );
1431  
1432              if (!empty($ml['options']['attributes'])) {
1433                  /* RFC 5258 [3.4]: inferred attributes. */
1434                  if ($ml['ext']) {
1435                      if (isset($attr['\\noinferiors'])) {
1436                          $attr['\\hasnochildren'] = 1;
1437                      }
1438                      if (isset($attr['\\nonexistent'])) {
1439                          $attr['\\noselect'] = 1;
1440                      }
1441                  }
1442                  $tmp['attributes'] = array_keys($attr);
1443              }
1444              if (!empty($ml['options']['delimiter'])) {
1445                  $tmp['delimiter'] = $delimiter;
1446              }
1447              if ($data->next() !== false) {
1448                  $tmp['extended'] = $data->flushIterator();
1449              }
1450              $pipeline->data['listresponse'][strval($mbox)] = $tmp;
1451          } else {
1452              $pipeline->data['listresponse'][] = $mbox;
1453          }
1454      }
1455  
1456      /**
1457       */
1458      protected function _status($mboxes, $flags)
1459      {
1460          $out = $to_process = array();
1461          $pipeline = $this->_pipeline();
1462          $unseen_flags = array(
1463              Horde_Imap_Client::STATUS_FIRSTUNSEEN,
1464              Horde_Imap_Client::STATUS_UNSEEN
1465          );
1466  
1467          foreach ($mboxes as $mailbox) {
1468              /* If FLAGS/PERMFLAGS/UIDNOTSTICKY/FIRSTUNSEEN are needed, we must
1469               * do a SELECT/EXAMINE to get this information (data will be
1470               * caught in the code below). */
1471              if (($flags & Horde_Imap_Client::STATUS_FIRSTUNSEEN) ||
1472                  ($flags & Horde_Imap_Client::STATUS_FLAGS) ||
1473                  ($flags & Horde_Imap_Client::STATUS_PERMFLAGS) ||
1474                  ($flags & Horde_Imap_Client::STATUS_UIDNOTSTICKY)) {
1475                  $this->openMailbox($mailbox);
1476              }
1477  
1478              $mbox_ob = $this->_mailboxOb($mailbox);
1479              $data = $query = array();
1480  
1481              foreach ($this->_statusFields as $key => $val) {
1482                  if (!($val & $flags)) {
1483                      continue;
1484                  }
1485  
1486                  if ($val == Horde_Imap_Client::STATUS_HIGHESTMODSEQ) {
1487                      /* Don't include modseq returns if server does not support
1488                       * it. */
1489                      if (!$this->queryCapability('CONDSTORE')) {
1490                          continue;
1491                      }
1492  
1493                      /* Even though CONDSTORE is available, it may not yet have
1494                       * been enabled. */
1495                      if (!isset($this->_temp['enabled']['CONDSTORE'])) {
1496                          $this->_enabled(array('CONDSTORE'), 2);
1497                      }
1498                  }
1499  
1500                  if ($mailbox->equals($this->_selected)) {
1501                      if (!is_null($tmp = $mbox_ob->getStatus($val))) {
1502                          $data[$key] = $tmp;
1503                      } elseif (($val == Horde_Imap_Client::STATUS_UIDNEXT) &&
1504                                ($flags & Horde_Imap_Client::STATUS_UIDNEXT_FORCE)) {
1505                          /* UIDNEXT is not mandatory. */
1506                          if ($mbox_ob->getStatus(Horde_Imap_Client::STATUS_MESSAGES) == 0) {
1507                              $data[$key] = 0;
1508                          } else {
1509                              $fquery = new Horde_Imap_Client_Fetch_Query();
1510                              $fquery->uid();
1511                              $fetch_res = $this->fetch($this->_selected, $fquery, array(
1512                                  'ids' => $this->getIdsOb(Horde_Imap_Client_Ids::LARGEST)
1513                              ));
1514                              $data[$key] = $fetch_res->first()->getUid() + 1;
1515                          }
1516                      } elseif (in_array($val, $unseen_flags)) {
1517                          /* RFC 3501 [6.3.1] - FIRSTUNSEEN information is not
1518                           * mandatory. If missing in EXAMINE/SELECT results, we
1519                           * need to do a search. An UNSEEN count also requires
1520                           * a search. */
1521                          $squery = new Horde_Imap_Client_Search_Query();
1522                          $squery->flag(Horde_Imap_Client::FLAG_SEEN, false);
1523                          $search = $this->search($mailbox, $squery, array(
1524                              'results' => array(
1525                                  Horde_Imap_Client::SEARCH_RESULTS_MIN,
1526                                  Horde_Imap_Client::SEARCH_RESULTS_COUNT
1527                              ),
1528                              'sequence' => true
1529                          ));
1530  
1531                          $mbox_ob->setStatus(Horde_Imap_Client::STATUS_FIRSTUNSEEN, $search['min']);
1532                          $mbox_ob->setStatus(Horde_Imap_Client::STATUS_UNSEEN, $search['count']);
1533  
1534                          $data[$key] = $mbox_ob->getStatus($val);
1535                      }
1536                  } else {
1537                      $query[] = $key;
1538                  }
1539              }
1540  
1541              $out[strval($mailbox)] = $data;
1542  
1543              if (count($query)) {
1544                  $pipeline->add(
1545                      $this->_command('STATUS')->add(array(
1546                          new Horde_Imap_Client_Data_Format_Mailbox($mailbox),
1547                          new Horde_Imap_Client_Data_Format_List(
1548                              array_map('strtoupper', $query)
1549                          )
1550                      ))
1551                  );
1552                  $to_process[] = array($query, $mailbox);
1553              }
1554          }
1555  
1556          if (count($pipeline)) {
1557              $this->_sendCmd($pipeline);
1558  
1559              foreach ($to_process as $val) {
1560                  $out[strval($val[1])] += $this->_prepareStatusResponse($val[0], $val[1]);
1561              }
1562          }
1563  
1564          return $out;
1565      }
1566  
1567      /**
1568       * Parse a STATUS response (RFC 3501 [7.2.4]).
1569       *
1570       * @param Horde_Imap_Client_Tokenize $data  Token data
1571       */
1572      protected function _parseStatus(Horde_Imap_Client_Tokenize $data)
1573      {
1574          // Mailbox name is in UTF7-IMAP
1575          $mbox_ob = $this->_mailboxOb(
1576              Horde_Imap_Client_Mailbox::get($data->next(), true)
1577          );
1578  
1579          $data->next();
1580  
1581          while (($k = $data->next()) !== false) {
1582              $mbox_ob->setStatus(
1583                  $this->_statusFields[strtolower($k)],
1584                  $data->next()
1585              );
1586          }
1587      }
1588  
1589      /**
1590       * Prepares a status response for a mailbox.
1591       *
1592       * @param array $request   The status keys to return.
1593       * @param string $mailbox  The mailbox to query.
1594       */
1595      protected function _prepareStatusResponse($request, $mailbox)
1596      {
1597          $mbox_ob = $this->_mailboxOb($mailbox);
1598          $out = array();
1599  
1600          foreach ($request as $val) {
1601              $out[$val] = $mbox_ob->getStatus($this->_statusFields[$val]);
1602          }
1603  
1604          return $out;
1605      }
1606  
1607      /**
1608       */
1609      protected function _append(Horde_Imap_Client_Mailbox $mailbox, $data,
1610                                 $options)
1611      {
1612          // Check for MULTIAPPEND extension (RFC 3502)
1613          if ((count($data) > 1) && !$this->queryCapability('MULTIAPPEND')) {
1614              $result = $this->getIdsOb();
1615              foreach (array_keys($data) as $key) {
1616                  $res = $this->_append($mailbox, array($data[$key]), $options);
1617                  if (($res === true) || ($result === true)) {
1618                      $result = true;
1619                  } else {
1620                      $result->add($res);
1621                  }
1622              }
1623              return $result;
1624          }
1625  
1626          // Check for CATENATE extension (RFC 4469)
1627          $catenate = $this->queryCapability('CATENATE');
1628  
1629          $asize = 0;
1630  
1631          $cmd = $this->_command('APPEND')->add(
1632              new Horde_Imap_Client_Data_Format_Mailbox($mailbox)
1633          );
1634          $cmd->literal8 = true;
1635  
1636          foreach (array_keys($data) as $key) {
1637              if (!empty($data[$key]['flags'])) {
1638                  $tmp = new Horde_Imap_Client_Data_Format_List();
1639                  foreach ($data[$key]['flags'] as $val) {
1640                      /* Ignore recent flag. RFC 3501 [9]: flag definition */
1641                      if (strcasecmp($val, Horde_Imap_Client::FLAG_RECENT) !== 0) {
1642                          $tmp->add($val);
1643                      }
1644                  }
1645                  $cmd->add($tmp);
1646              }
1647  
1648              if (!empty($data[$key]['internaldate'])) {
1649                  $cmd->add(new Horde_Imap_Client_Data_Format_DateTime($data[$key]['internaldate']));
1650              }
1651  
1652              if (is_array($data[$key]['data'])) {
1653                  if ($catenate) {
1654                      $cmd->add('CATENATE');
1655                      $tmp = new Horde_Imap_Client_Data_Format_List();
1656                  } else {
1657                      $data_stream = new Horde_Stream_Temp();
1658                  }
1659  
1660                  reset($data[$key]['data']);
1661                  while (list(,$v) = each($data[$key]['data'])) {
1662                      switch ($v['t']) {
1663                      case 'text':
1664                          if ($catenate) {
1665                              $tmp->add(array(
1666                                  'TEXT',
1667                                  $this->_appendData($v['v'], $asize)
1668                              ));
1669                          } else {
1670                              if (is_resource($v['v'])) {
1671                                  rewind($v['v']);
1672                              }
1673                              $data_stream->add($v['v']);
1674                          }
1675                          break;
1676  
1677                      case 'url':
1678                          if ($catenate) {
1679                              $tmp->add(array(
1680                                  'URL',
1681                                  new Horde_Imap_Client_Data_Format_Astring($v['v'])
1682                              ));
1683                          } else {
1684                              $data_stream->add($this->_convertCatenateUrl($v['v']));
1685                          }
1686                          break;
1687                      }
1688                  }
1689  
1690                  if ($catenate) {
1691                      $cmd->add($tmp);
1692                  } else {
1693                      $cmd->add($this->_appendData($data_stream->stream, $asize));
1694                  }
1695              } else {
1696                  $cmd->add($this->_appendData($data[$key]['data'], $asize));
1697              }
1698          }
1699  
1700          /* Although it is normally more efficient to use LITERAL+, disable if
1701           * payload is over 0.5 MB because it allows the server to throw error
1702           * before we potentially push a lot of data to server that would
1703           * otherwise be ignored (see RFC 4549 [4.2.2.3]).
1704           * Additionally, if using BINARY, since so many IMAP servers have
1705           * issues with APPEND + BINARY, don't use LITERAL+ since servers may
1706           * send BAD after initial command. */
1707          $cmd->literalplus = (($asize < 524288) && !$this->queryCapability('BINARY'));
1708  
1709          // If the mailbox is currently selected read-only, we need to close
1710          // because some IMAP implementations won't allow an append. And some
1711          // implementations don't support append on ANY open mailbox. Be safe
1712          // and always make sure we are in a non-selected state.
1713          $this->close();
1714  
1715          try {
1716              $resp = $this->_sendCmd($cmd);
1717          } catch (Horde_Imap_Client_Exception $e) {
1718              switch ($e->getCode()) {
1719              case $e::CATENATE_BADURL:
1720              case $e::CATENATE_TOOBIG:
1721                  /* Cyrus 2.4 (at least as of .14) has a broken CATENATE (see
1722                   * Bug #11111). Regardless, if CATENATE is broken, we can try
1723                   * to fallback to APPEND. */
1724                  $this->_unsetCapability('CATENATE');
1725                  return $this->_append($mailbox, $data, $options);
1726  
1727              case $e::DISCONNECT:
1728                  /* Workaround broken literal8 on Cyrus. */
1729                  if ($this->queryCapability('BINARY')) {
1730                      // Need to re-login first before removing capability.
1731                      $this->login();
1732                      $this->_unsetCapability('BINARY');
1733                      return $this->_append($mailbox, $data, $options);
1734                  }
1735                  break;
1736              }
1737  
1738              if (!empty($options['create']) &&
1739                  !empty($e->resp_data['trycreate'])) {
1740                  $this->createMailbox($mailbox);
1741                  unset($options['create']);
1742                  return $this->_append($mailbox, $data, $options);
1743              }
1744  
1745              /* RFC 3516/4466 says we should be able to append binary data
1746               * using literal8 "~{#} format", but it doesn't seem to work on
1747               * all servers tried (UW-IMAP/Cyrus). Do a last-ditch check for
1748               * broken BINARY and attempt to fix here. */
1749              if ($this->queryCapability('BINARY') &&
1750                  ($e instanceof Horde_Imap_Client_Exception_ServerResponse)) {
1751                  switch ($e->status) {
1752                  case Horde_Imap_Client_Interaction_Server::BAD:
1753                  case Horde_Imap_Client_Interaction_Server::NO:
1754                      $this->_unsetCapability('BINARY');
1755                      return $this->_append($mailbox, $data, $options);
1756                  }
1757              }
1758  
1759              throw $e;
1760          }
1761  
1762          /* If we reach this point and have data in 'appenduid', UIDPLUS (RFC
1763           * 4315) has done the dirty work for us. */
1764          return isset($resp->data['appenduid'])
1765              ? $resp->data['appenduid']
1766              : true;
1767      }
1768  
1769      /**
1770       * Prepares append message data for insertion into the IMAP command
1771       * string.
1772       *
1773       * @param mixed $data      Either a resource or a string.
1774       * @param integer &$asize  Total append size.
1775       *
1776       * @return Horde_Imap_Client_Data_Format_String  The data object.
1777       */
1778      protected function _appendData($data, &$asize)
1779      {
1780          if (is_resource($data)) {
1781              rewind($data);
1782          }
1783  
1784          $ob = new Horde_Imap_Client_Data_Format_String($data, array(
1785              'eol' => true,
1786              'skipscan' => true
1787          ));
1788  
1789          // APPEND data MUST be sent in a literal (RFC 3501 [6.3.11]).
1790          $ob->forceLiteral();
1791  
1792          $asize += $ob->length();
1793  
1794          return $ob;
1795      }
1796  
1797      /**
1798       * Converts a CATENATE URL to stream data.
1799       *
1800       * @param string $url  The CATENATE URL.
1801       *
1802       * @return resource  A stream containing the data.
1803       */
1804      protected function _convertCatenateUrl($url)
1805      {
1806          $e = $part = null;
1807          $url = new Horde_Imap_Client_Url($url);
1808  
1809          if (!is_null($url->mailbox) && !is_null($url->uid)) {
1810              try {
1811                  $status_res = is_null($url->uidvalidity)
1812                      ? null
1813                      : $this->status($url->mailbox, Horde_Imap_Client::STATUS_UIDVALIDITY);
1814  
1815                  if (is_null($status_res) ||
1816                      ($status_res['uidvalidity'] == $url->uidvalidity)) {
1817                      if (!isset($this->_temp['catenate_ob'])) {
1818                          $this->_temp['catenate_ob'] = new Horde_Imap_Client_Socket_Catenate($this);
1819                      }
1820                      $part = $this->_temp['catenate_ob']->fetchFromUrl($url);
1821                  }
1822              } catch (Horde_Imap_Client_Exception $e) {}
1823          }
1824  
1825          if (is_null($part)) {
1826              $message = 'Bad IMAP URL given in CATENATE data: ' . strval($url);
1827              if ($e) {
1828                  $message .= ' ' . $e->getMessage();
1829              }
1830  
1831              throw new InvalidArgumentException($message);
1832          }
1833  
1834          return $part;
1835      }
1836  
1837      /**
1838       */
1839      protected function _check()
1840      {
1841          // CHECK returns no untagged information (RFC 3501 [6.4.1])
1842          $this->_sendCmd($this->_command('CHECK'));
1843      }
1844  
1845      /**
1846       */
1847      protected function _close($options)
1848      {
1849          if (empty($options['expunge'])) {
1850              if ($this->queryCapability('UNSELECT')) {
1851                  // RFC 3691 defines 'UNSELECT' for precisely this purpose
1852                  $this->_sendCmd($this->_command('UNSELECT'));
1853              } else {
1854                  /* RFC 3501 [6.4.2]: to close a mailbox without expunge,
1855                   * select a non-existent mailbox. */
1856                  try {
1857                      $this->_sendCmd($this->_command('EXAMINE')->add(
1858                          new Horde_Imap_Client_Data_Format_Mailbox("\24nonexist\24")
1859                      ));
1860  
1861                      /* Not pipelining, since the odds that this CLOSE is even
1862                       * needed is tiny; and it returns BAD, which should be
1863                       * avoided, if possible. */
1864                      $this->_sendCmd($this->_command('CLOSE'));
1865                  } catch (Horde_Imap_Client_Exception_ServerResponse $e) {
1866                      // Ignore error; it is expected.
1867                  }
1868              }
1869          } else {
1870              // If caching, we need to know the UIDs being deleted, so call
1871              // expunge() before calling close().
1872              if ($this->_initCache(true)) {
1873                  $this->expunge($this->_selected);
1874              }
1875  
1876              // CLOSE returns no untagged information (RFC 3501 [6.4.2])
1877              $this->_sendCmd($this->_command('CLOSE'));
1878          }
1879      }
1880  
1881      /**
1882       */
1883      protected function _expunge($options)
1884      {
1885          $expunged_ob = $modseq = null;
1886          $ids = $options['ids'];
1887          $list_msgs = !empty($options['list']);
1888          $uidplus = $this->queryCapability('UIDPLUS');
1889          $unflag = array();
1890          $use_cache = $this->_initCache(true);
1891  
1892          if ($ids->all) {
1893              if (!$uidplus || $list_msgs || $use_cache) {
1894                  $ids = $this->resolveIds($this->_selected, $ids, 2);
1895              }
1896          } elseif ($uidplus) {
1897              /* If QRESYNC is not available, and we are returning the list of
1898               * expunged messages (or we are caching), we have to make sure we
1899               * have a mapping of Sequence -> UIDs. If we have QRESYNC, the
1900               * server SHOULD return a VANISHED response with UIDs. However,
1901               * even if the server returns EXPUNGEs instead, we can use
1902               * vanished() to grab the list. */
1903              unset($this->_temp['search_save']);
1904              if (isset($this->_temp['enabled']['QRESYNC'])) {
1905                  $ids = $this->resolveIds($this->_selected, $ids, 1);
1906                  if ($list_msgs) {
1907                      $modseq = $this->_mailboxOb()->getStatus(Horde_Imap_Client::STATUS_HIGHESTMODSEQ);
1908                  }
1909              } else {
1910                  $ids = $this->resolveIds($this->_selected, $ids, ($list_msgs || $use_cache) ? 2 : 1);
1911              }
1912              if (!empty($this->_temp['search_save'])) {
1913                  $ids = $this->getIdsOb(Horde_Imap_Client_Ids::SEARCH_RES);
1914              }
1915          } else {
1916              /* Without UIDPLUS, need to temporarily unflag all messages marked
1917               * as deleted but not a part of requested IDs to delete. Use NOT
1918               * searches to accomplish this goal. */
1919              $squery = new Horde_Imap_Client_Search_Query();
1920              $squery->flag(Horde_Imap_Client::FLAG_DELETED, true);
1921              $squery->ids($ids, true);
1922  
1923              $s_res = $this->search($this->_selected, $squery, array(
1924                  'results' => array(
1925                      Horde_Imap_Client::SEARCH_RESULTS_MATCH,
1926                      Horde_Imap_Client::SEARCH_RESULTS_SAVE
1927                  )
1928              ));
1929  
1930              $this->store($this->_selected, array(
1931                  'ids' => empty($s_res['save']) ? $s_res['match'] : $this->getIdsOb(Horde_Imap_Client_Ids::SEARCH_RES),
1932                  'remove' => array(Horde_Imap_Client::FLAG_DELETED)
1933              ));
1934  
1935              $unflag = $s_res['match'];
1936          }
1937  
1938          if ($list_msgs) {
1939              $expunged_ob = $this->getIdsOb();
1940              $this->_temp['expunged'] = $expunged_ob;
1941          }
1942  
1943          /* Always use UID EXPUNGE if available. */
1944          if ($uidplus) {
1945              /* We can only pipeline STORE w/ EXPUNGE if using UIDs and UIDPLUS
1946               * is available. */
1947              if (empty($options['delete'])) {
1948                  $pipeline = $this->_pipeline();
1949              } else {
1950                  $pipeline = $this->_storeCmd(array(
1951                      'add' => array(
1952                          Horde_Imap_Client::FLAG_DELETED
1953                      ),
1954                      'ids' => $ids
1955                  ));
1956              }
1957  
1958              foreach ($ids->split(2000) as $val) {
1959                  $pipeline->add(
1960                      $this->_command('UID EXPUNGE')->add($val)
1961                  );
1962              }
1963  
1964              $resp = $this->_sendCmd($pipeline);
1965          } else {
1966              if (!empty($options['delete'])) {
1967                  $this->store($this->_selected, array(
1968                      'add' => array(Horde_Imap_Client::FLAG_DELETED),
1969                      'ids' => $ids
1970                  ));
1971              }
1972  
1973              if ($use_cache || $list_msgs) {
1974                  $this->_sendCmd($this->_command('EXPUNGE'));
1975              } else {
1976                  /* This is faster than an EXPUNGE because the server will not
1977                   * return untagged EXPUNGE responses. We can only do this if
1978                   * we are not updating cache information. */
1979                  $this->close(array('expunge' => true));
1980              }
1981          }
1982  
1983          unset($this->_temp['expunged']);
1984  
1985          if (!empty($unflag)) {
1986              $this->store($this->_selected, array(
1987                  'add' => array(Horde_Imap_Client::FLAG_DELETED),
1988                  'ids' => $unflag
1989              ));
1990          }
1991  
1992          if (!is_null($modseq) && !empty($resp->data['expunge_seen'])) {
1993              /* There's a chance we actually did a full map of sequence -> UID,
1994               * but this code should never be reached in the first place so
1995               * be ultra-safe and just do a full VANISHED search. */
1996              $expunged_ob = $this->vanished($this->_selected, $modseq, array(
1997                  'ids' => $ids
1998              ));
1999              $this->_deleteMsgs($this->_selected, $expunged_ob, array(
2000                  'pipeline' => $resp
2001              ));
2002          }
2003  
2004          return $expunged_ob;
2005      }
2006  
2007      /**
2008       * Parse a VANISHED response (RFC 7162 [3.2.10]).
2009       *
2010       * @param Horde_Imap_Client_Interaction_Pipeline $pipeline  Pipeline
2011       *                                                          object.
2012       * @param Horde_Imap_Client_Tokenize $data  The response data.
2013       */
2014      protected function _parseVanished(
2015          Horde_Imap_Client_Interaction_Pipeline $pipeline,
2016          Horde_Imap_Client_Tokenize $data
2017      )
2018      {
2019          /* There are two forms of VANISHED.  VANISHED (EARLIER) will be sent
2020           * in a FETCH (VANISHED) or SELECT/EXAMINE (QRESYNC) call.
2021           * If this is the case, we can go ahead and update the cache
2022           * immediately (we know we are caching or else QRESYNC would not be
2023           * enabled). HIGHESTMODSEQ information will be updated via the tagged
2024           * response. */
2025          if (($curr = $data->next()) === true) {
2026              if (strtoupper($data->next()) === 'EARLIER') {
2027                  /* Caching is guaranteed to be active if we are using
2028                   * QRESYNC. */
2029                  $data->next();
2030                  $vanished = $this->getIdsOb($data->next());
2031                  if (isset($pipeline->data['vanished'])) {
2032                      $pipeline->data['vanished']->add($vanished);
2033                  } else {
2034                      $this->_deleteMsgs($this->_selected, $vanished, array(
2035                          'pipeline' => $pipeline
2036                      ));
2037                  }
2038              }
2039          } else {
2040              /* The second form is just VANISHED. This is analogous to EXPUNGE
2041               * and requires the message count to decrement. */
2042              $this->_deleteMsgs($this->_selected, $this->getIdsOb($curr), array(
2043                  'decrement' => true,
2044                  'pipeline' => $pipeline
2045              ));
2046          }
2047      }
2048  
2049      /**
2050       * Search a mailbox.  This driver supports all IMAP4rev1 search criteria
2051       * as defined in RFC 3501.
2052       */
2053      protected function _search($query, $options)
2054      {
2055          $sort_criteria = array(
2056              Horde_Imap_Client::SORT_ARRIVAL => 'ARRIVAL',
2057              Horde_Imap_Client::SORT_CC => 'CC',
2058              Horde_Imap_Client::SORT_DATE => 'DATE',
2059              Horde_Imap_Client::SORT_DISPLAYFROM => 'DISPLAYFROM',
2060              Horde_Imap_Client::SORT_DISPLAYTO => 'DISPLAYTO',
2061              Horde_Imap_Client::SORT_FROM => 'FROM',
2062              Horde_Imap_Client::SORT_REVERSE => 'REVERSE',
2063              Horde_Imap_Client::SORT_RELEVANCY => 'RELEVANCY',
2064              // This is a bogus entry to allow the sort options check to
2065              // correctly work below.
2066              Horde_Imap_Client::SORT_SEQUENCE => 'SEQUENCE',
2067              Horde_Imap_Client::SORT_SIZE => 'SIZE',
2068              Horde_Imap_Client::SORT_SUBJECT => 'SUBJECT',
2069              Horde_Imap_Client::SORT_TO => 'TO'
2070          );
2071  
2072          $results_criteria = array(
2073              Horde_Imap_Client::SEARCH_RESULTS_COUNT => 'COUNT',
2074              Horde_Imap_Client::SEARCH_RESULTS_MATCH => 'ALL',
2075              Horde_Imap_Client::SEARCH_RESULTS_MAX => 'MAX',
2076              Horde_Imap_Client::SEARCH_RESULTS_MIN => 'MIN',
2077              Horde_Imap_Client::SEARCH_RESULTS_RELEVANCY => 'RELEVANCY',
2078              Horde_Imap_Client::SEARCH_RESULTS_SAVE => 'SAVE'
2079          );
2080  
2081          // Check if the server supports sorting (RFC 5256).
2082          $esearch = $return_sort = $server_seq_sort = $server_sort = false;
2083          if (!empty($options['sort'])) {
2084              /* Make sure sort options are correct. If not, default to no
2085               * sort. */
2086              if (count(array_intersect($options['sort'], array_keys($sort_criteria))) === 0) {
2087                  unset($options['sort']);
2088              } else {
2089                  $return_sort = true;
2090  
2091                  if ($server_sort = $this->queryCapability('SORT')) {
2092                      /* Make sure server supports DISPLAYFROM & DISPLAYTO. */
2093                      $server_sort =
2094                          !array_intersect($options['sort'], array(Horde_Imap_Client::SORT_DISPLAYFROM, Horde_Imap_Client::SORT_DISPLAYTO)) ||
2095                          (is_array($server_sort) &&
2096                           in_array('DISPLAY', $server_sort));
2097                  }
2098  
2099                  /* If doing a sequence sort, need to do this on the client
2100                   * side. */
2101                  if ($server_sort &&
2102                      in_array(Horde_Imap_Client::SORT_SEQUENCE, $options['sort'])) {
2103                      $server_sort = false;
2104  
2105                      /* Optimization: If doing only a sequence sort, just do a
2106                       * simple search and sort UIDs/sequences on client side. */
2107                      switch (count($options['sort'])) {
2108                      case 1:
2109                          $server_seq_sort = true;
2110                          break;
2111  
2112                      case 2:
2113                          $server_seq_sort = (reset($options['sort']) == Horde_Imap_Client::SORT_REVERSE);
2114                          break;
2115                      }
2116                  }
2117              }
2118          }
2119  
2120          $charset = is_null($options['_query']['charset'])
2121              ? 'US-ASCII'
2122              : $options['_query']['charset'];
2123          $partial = false;
2124  
2125          if ($server_sort) {
2126              $cmd = $this->_command(
2127                  empty($options['sequence']) ? 'UID SORT' : 'SORT'
2128              );
2129              $results = array();
2130  
2131              // Use ESEARCH (RFC 4466) response if server supports.
2132              $esearch = false;
2133  
2134              // Check for ESORT capability (RFC 5267)
2135              if ($this->queryCapability('ESORT')) {
2136                  foreach ($options['results'] as $val) {
2137                      if (isset($results_criteria[$val]) &&
2138                          ($val != Horde_Imap_Client::SEARCH_RESULTS_SAVE)) {
2139                          $results[] = $results_criteria[$val];
2140                      }
2141                  }
2142                  $esearch = true;
2143              }
2144  
2145              // Add PARTIAL limiting (RFC 5267 [4.4])
2146              if ((!$esearch || !empty($options['partial'])) &&
2147                  ($cap = $this->queryCapability('CONTEXT')) &&
2148                  in_array('SORT', $cap)) {
2149                  /* RFC 5267 indicates RFC 4466 ESEARCH-like support,
2150                   * notwithstanding "real" RFC 4731 support. */
2151                  $esearch = true;
2152  
2153                  if (!empty($options['partial'])) {
2154                      /* Can't have both ALL and PARTIAL returns. */
2155                      $results = array_diff($results, array('ALL'));
2156  
2157                      $results[] = 'PARTIAL';
2158                      $results[] = $options['partial'];
2159                      $partial = true;
2160                  }
2161              }
2162  
2163              if ($esearch && empty($this->_init['noesearch'])) {
2164                  $cmd->add(array(
2165                      'RETURN',
2166                      new Horde_Imap_Client_Data_Format_List($results)
2167                  ));
2168              }
2169  
2170              $tmp = new Horde_Imap_Client_Data_Format_List();
2171              foreach ($options['sort'] as $val) {
2172                  if (isset($sort_criteria[$val])) {
2173                      $tmp->add($sort_criteria[$val]);
2174                  }
2175              }
2176              $cmd->add($tmp);
2177  
2178              // Charset is mandatory for SORT (RFC 5256 [3]).
2179              $cmd->add($charset);
2180          } else {
2181              $cmd = $this->_command(
2182                  empty($options['sequence']) ? 'UID SEARCH' : 'SEARCH'
2183              );
2184              $esearch = false;
2185              $results = array();
2186  
2187              // Check if the server supports ESEARCH (RFC 4731).
2188              if ($this->queryCapability('ESEARCH')) {
2189                  foreach ($options['results'] as $val) {
2190                      if (isset($results_criteria[$val])) {
2191                          $results[] = $results_criteria[$val];
2192                      }
2193                  }
2194                  $esearch = true;
2195              }
2196  
2197              // Add PARTIAL limiting (RFC 5267 [4.4]).
2198              if ((!$esearch || !empty($options['partial'])) &&
2199                  ($cap = $this->queryCapability('CONTEXT')) &&
2200                  in_array('SEARCH', $cap)) {
2201                  /* RFC 5267 indicates RFC 4466 ESEARCH-like support,
2202                   * notwithstanding "real" RFC 4731 support. */
2203                  $esearch = true;
2204  
2205                  if (!empty($options['partial'])) {
2206                      // Can't have both ALL and PARTIAL returns.
2207                      $results = array_diff($results, array('ALL'));
2208  
2209                      $results[] = 'PARTIAL';
2210                      $results[] = $options['partial'];
2211                      $partial = true;
2212                  }
2213              }
2214  
2215              if ($esearch && empty($this->_init['noesearch'])) {
2216                  // Always use ESEARCH if available because it returns results
2217                  // in a more compact sequence-set list
2218                  $cmd->add(array(
2219                      'RETURN',
2220                      new Horde_Imap_Client_Data_Format_List($results)
2221                  ));
2222              }
2223  
2224              // Charset is optional for SEARCH (RFC 3501 [6.4.4]).
2225              if ($charset != 'US-ASCII') {
2226                  $cmd->add(array(
2227                      'CHARSET',
2228                      $options['_query']['charset']
2229                  ));
2230              }
2231          }
2232  
2233          $cmd->add($options['_query']['query'], true);
2234  
2235          $pipeline = $this->_pipeline($cmd);
2236          $pipeline->data['esearchresp'] = array();
2237          $er = &$pipeline->data['esearchresp'];
2238          $pipeline->data['searchresp'] = $this->getIdsOb(array(), !empty($options['sequence']));
2239          $sr = &$pipeline->data['searchresp'];
2240  
2241          try {
2242              $resp = $this->_sendCmd($pipeline);
2243          } catch (Horde_Imap_Client_Exception $e) {
2244              if (($e instanceof Horde_Imap_Client_Exception_ServerResponse) &&
2245                  ($e->status === Horde_Imap_Client_Interaction_Server::NO) &&
2246                  ($charset != 'US-ASCII')) {
2247                  /* RFC 3501 [6.4.4]: BADCHARSET response code is only a
2248                   * SHOULD return. If it doesn't exist, need to check for
2249                   * command status of 'NO'. List of supported charsets in
2250                   * the BADCHARSET response has already been parsed and stored
2251                   * at this point. */
2252                  $s_charset = $this->_init['s_charset'];
2253                  $s_charset[$charset] = false;
2254                  $this->_setInit('s_charset', $s_charset);
2255                  $e->setCode(Horde_Imap_Client_Exception::BADCHARSET);
2256              }
2257  
2258              if (empty($this->_temp['search_retry'])) {
2259                  $this->_temp['search_retry'] = true;
2260  
2261                  /* Bug #9842: Workaround broken Cyrus servers (as of
2262                   * 2.4.7). */
2263                  if ($esearch && ($charset != 'US-ASCII')) {
2264                      $this->_unsetCapability('ESEARCH');
2265                      $this->_setInit('noesearch', true);
2266  
2267                      try {
2268                          return $this->_search($query, $options);
2269                      } catch (Horde_Imap_Client_Exception $e) {}
2270                  }
2271  
2272                  /* Try to convert charset. */
2273                  if (($e->getCode() === Horde_Imap_Client_Exception::BADCHARSET) &&
2274                      ($charset != 'US-ASCII')) {
2275                      foreach (array_merge(array_keys(array_filter($this->_init['s_charset'])), array('US-ASCII')) as $val) {
2276                          $this->_temp['search_retry'] = 1;
2277                          $new_query = clone($query);
2278                          try {
2279                              $new_query->charset($val);
2280                              $options['_query'] = $new_query->build($this->capability());
2281                              return $this->_search($new_query, $options);
2282                          } catch (Horde_Imap_Client_Exception $e) {}
2283                      }
2284                  }
2285  
2286                  unset($this->_temp['search_retry']);
2287              }
2288  
2289              throw $e;
2290          }
2291  
2292          if ($return_sort && !$server_sort) {
2293              if ($server_seq_sort) {
2294                  $sr->sort();
2295                  if (reset($options['sort']) == Horde_Imap_Client::SORT_REVERSE) {
2296                      $sr->reverse();
2297                  }
2298              } else {
2299                  if (!isset($this->_temp['clientsort'])) {
2300                      $this->_temp['clientsort'] = new Horde_Imap_Client_Socket_ClientSort($this);
2301                  }
2302                  $sr = $this->getIdsOb($this->_temp['clientsort']->clientSort($sr, $options), !empty($options['sequence']));
2303              }
2304          }
2305  
2306          if (!$partial && !empty($options['partial'])) {
2307              $partial = $this->getIdsOb($options['partial'], true);
2308              $min = $partial->min - 1;
2309  
2310              $sr->sort();
2311              $sr = $this->getIdsOb(
2312                  array_slice($sr->ids(), $min, $partial->max - $min),
2313                  !empty($options['sequence'])
2314              );
2315          }
2316  
2317          $ret = array();
2318          foreach ($options['results'] as $val) {
2319              switch ($val) {
2320              case Horde_Imap_Client::SEARCH_RESULTS_COUNT:
2321                  $ret['count'] = ($esearch && !$partial)
2322                      ? $er['count']
2323                      : count($sr);
2324                  break;
2325  
2326              case Horde_Imap_Client::SEARCH_RESULTS_MATCH:
2327                  $ret['match'] = $sr;
2328                  break;
2329  
2330              case Horde_Imap_Client::SEARCH_RESULTS_MAX:
2331                  $ret['max'] = $esearch
2332                      ? (!$partial && isset($er['max']) ? $er['max'] : null)
2333                      : (count($sr) ? max($sr->ids) : null);
2334                  break;
2335  
2336              case Horde_Imap_Client::SEARCH_RESULTS_MIN:
2337                  $ret['min'] = $esearch
2338                      ? (!$partial && isset($er['min']) ? $er['min'] : null)
2339                      : (count($sr) ? min($sr->ids) : null);
2340                  break;
2341  
2342              case Horde_Imap_Client::SEARCH_RESULTS_RELEVANCY:
2343                  $ret['relevancy'] = ($esearch && isset($er['relevancy'])) ? $er['relevancy'] : array();
2344                  break;
2345  
2346              case Horde_Imap_Client::SEARCH_RESULTS_SAVE:
2347                  $this->_temp['search_save'] = $ret['save'] = $esearch ? empty($resp->data['searchnotsaved']) : false;
2348                  break;
2349              }
2350          }
2351  
2352          // Add modseq data, if needed.
2353          if (!empty($er['modseq'])) {
2354              $ret['modseq'] = $er['modseq'];
2355          }
2356  
2357          unset($this->_temp['search_retry']);
2358  
2359          /* Check for EXPUNGEISSUED (RFC 2180 [4.3]/RFC 5530 [3]). */
2360          if (!empty($resp->data['expungeissued'])) {
2361              $this->noop();
2362          }
2363  
2364          return $ret;
2365      }
2366  
2367      /**
2368       * Parse a SEARCH/SORT response (RFC 3501 [7.2.5]; RFC 4466 [3];
2369       * RFC 5256 [4]; RFC 5267 [3]).
2370       *
2371       * @param Horde_Imap_Client_Interaction_Pipeline $pipeline  Pipeline
2372       *                                                          object.
2373       * @param array $data  A list of IDs (message sequence numbers or UIDs).
2374       */
2375      protected function _parseSearch(
2376          Horde_Imap_Client_Interaction_Pipeline $pipeline,
2377          $data
2378      )
2379      {
2380          /* More than one search response may be sent. */
2381          $pipeline->data['searchresp']->add($data);
2382      }
2383  
2384      /**
2385       * Parse an ESEARCH response (RFC 4466 [2.6.2])
2386       * Format: (TAG "a567") UID COUNT 5 ALL 4:19,21,28
2387       *
2388       * @param Horde_Imap_Client_Interaction_Pipeline $pipeline  Pipeline
2389       *                                                          object.
2390       * @param Horde_Imap_Client_Tokenize $data  The server response.
2391       */
2392      protected function _parseEsearch(
2393          Horde_Imap_Client_Interaction_Pipeline $pipeline,
2394          Horde_Imap_Client_Tokenize $data
2395      )
2396      {
2397          // Ignore search correlator information
2398          if ($data->next() === true) {
2399              $data->flushIterator(false);
2400          }
2401  
2402          // Ignore UID tag
2403          $current = $data->next();
2404          if (strtoupper($current) === 'UID') {
2405              $current = $data->next();
2406          }
2407  
2408          do {
2409              $val = $data->next();
2410              $tag = strtoupper($current);
2411  
2412              switch ($tag) {
2413              case 'ALL':
2414                  $this->_parseSearch($pipeline, $val);
2415                  break;
2416  
2417              case 'COUNT':
2418              case 'MAX':
2419              case 'MIN':
2420              case 'MODSEQ':
2421              case 'RELEVANCY':
2422                  $pipeline->data['esearchresp'][strtolower($tag)] = $val;
2423                  break;
2424  
2425              case 'PARTIAL':
2426                  // RFC 5267 [4.4]
2427                  $partial = $val->flushIterator();
2428                  $this->_parseSearch($pipeline, end($partial));
2429                  break;
2430              }
2431          } while (($current = $data->next()) !== false);
2432      }
2433  
2434      /**
2435       */
2436      protected function _setComparator($comparator)
2437      {
2438          $cmd = $this->_command('COMPARATOR');
2439          foreach ($comparator as $val) {
2440              $cmd->add(new Horde_Imap_Client_Data_Format_Astring($val));
2441          }
2442          $this->_sendCmd($cmd);
2443      }
2444  
2445      /**
2446       */
2447      protected function _getComparator()
2448      {
2449          $resp = $this->_sendCmd($this->_command('COMPARATOR'));
2450  
2451          return isset($resp->data['comparator'])
2452              ? $resp->data['comparator']
2453              : null;
2454      }
2455  
2456      /**
2457       * Parse a COMPARATOR response (RFC 5255 [4.8])
2458       *
2459       * @param Horde_Imap_Client_Interaction_Pipeline $pipeline  Pipeline
2460       *                                                          object.
2461       * @param Horde_Imap_Client_Tokenize $data  The server response.
2462       */
2463      protected function _parseComparator(
2464          Horde_Imap_Client_Interaction_Pipeline $pipeline,
2465          $data
2466      )
2467      {
2468          $pipeline->data['comparator'] = $data->next();
2469          // Ignore optional matching comparator list
2470      }
2471  
2472      /**
2473       * @throws Horde_Imap_Client_Exception_NoSupportExtension
2474       */
2475      protected function _thread($options)
2476      {
2477          $thread_criteria = array(
2478              Horde_Imap_Client::THREAD_ORDEREDSUBJECT => 'ORDEREDSUBJECT',
2479              Horde_Imap_Client::THREAD_REFERENCES => 'REFERENCES',
2480              Horde_Imap_Client::THREAD_REFS => 'REFS'
2481          );
2482  
2483          $tsort = (isset($options['criteria']))
2484              ? (is_string($options['criteria']) ? strtoupper($options['criteria']) : $thread_criteria[$options['criteria']])
2485              : 'ORDEREDSUBJECT';
2486  
2487          $cap = $this->queryCapability('THREAD');
2488          if (!$cap || !in_array($tsort, $cap)) {
2489              switch ($tsort) {
2490              case 'ORDEREDSUBJECT':
2491                  if (empty($options['search'])) {
2492                      $ids = $this->getIdsOb(Horde_Imap_Client_Ids::ALL, !empty($options['sequence']));
2493                  } else {
2494                      $search_res = $this->search($this->_selected, $options['search'], array('sequence' => !empty($options['sequence'])));
2495                      $ids = $search_res['match'];
2496                  }
2497  
2498                  /* Do client-side ORDEREDSUBJECT threading. */
2499                  $query = new Horde_Imap_Client_Fetch_Query();
2500                  $query->envelope();
2501                  $query->imapDate();
2502  
2503                  $fetch_res = $this->fetch($this->_selected, $query, array(
2504                      'ids' => $ids
2505                  ));
2506  
2507                  if (!isset($this->_temp['clientsort'])) {
2508                      $this->_temp['clientsort'] = new Horde_Imap_Client_Socket_ClientSort($this);
2509                  }
2510                  return $this->_temp['clientsort']->threadOrderedSubject($fetch_res, empty($options['sequence']));
2511  
2512              case 'REFERENCES':
2513              case 'REFS':
2514                  throw new Horde_Imap_Client_Exception_NoSupportExtension(
2515                      'THREAD',
2516                      sprintf('Server does not support "%s" thread sort.', $tsort)
2517                  );
2518              }
2519          }
2520  
2521          $cmd = $this->_command(
2522              empty($options['sequence']) ? 'UID THREAD' : 'THREAD'
2523          )->add($tsort);
2524  
2525          if (empty($options['search'])) {
2526              $cmd->add(array(
2527                  'US-ASCII',
2528                  'ALL'
2529              ));
2530          } else {
2531              $search_query = $options['search']->build();
2532              $cmd->add(is_null($search_query['charset']) ? 'US-ASCII' : $search_query['charset']);
2533              $cmd->add($search_query['query'], true);
2534          }
2535  
2536          return new Horde_Imap_Client_Data_Thread(
2537              $this->_sendCmd($cmd)->data['threadparse'],
2538              empty($options['sequence']) ? 'uid' : 'sequence'
2539          );
2540      }
2541  
2542      /**
2543       * Parse a THREAD response (RFC 5256 [4]).
2544       *
2545       * @param Horde_Imap_Client_Interaction_Pipeline $pipeline  Pipeline
2546       *                                                          object.
2547       * @param Horde_Imap_Client_Tokenize $data  Thread data.
2548       */
2549      protected function _parseThread(
2550          Horde_Imap_Client_Interaction_Pipeline $pipeline,
2551          Horde_Imap_Client_Tokenize $data
2552      )
2553      {
2554          $out = array();
2555  
2556          while ($data->next() !== false) {
2557              $thread = array();
2558              $this->_parseThreadLevel($thread, $data);
2559              $out[] = $thread;
2560          }
2561  
2562          $pipeline->data['threadparse'] = $out;
2563      }
2564  
2565      /**
2566       * Parse a level of a THREAD response (RFC 5256 [4]).
2567       *
2568       * @param array $thread                     Results.
2569       * @param Horde_Imap_Client_Tokenize $data  Thread data.
2570       * @param integer $level                    The current tree level.
2571       */
2572      protected function _parseThreadLevel(&$thread,
2573                                           Horde_Imap_Client_Tokenize $data,
2574                                           $level = 0)
2575      {
2576          while (($curr = $data->next()) !== false) {
2577              if ($curr === true) {
2578                  $this->_parseThreadLevel($thread, $data, $level);
2579              } elseif (!is_bool($curr)) {
2580                  $thread[$curr] = $level++;
2581              }
2582          }
2583      }
2584  
2585      /**
2586       */
2587      protected function _fetch(Horde_Imap_Client_Fetch_Results $results,
2588                                $queries)
2589      {
2590          $pipeline = $this->_pipeline();
2591          $pipeline->data['fetch_lookup'] = array();
2592          $pipeline->data['fetch_followup'] = array();
2593  
2594          foreach ($queries as $options) {
2595              $this->_fetchCmd($pipeline, $options);
2596              $sequence = $options['ids']->sequence;
2597          }
2598  
2599          try {
2600              $resp = $this->_sendCmd($pipeline);
2601  
2602              /* Check for EXPUNGEISSUED (RFC 2180 [4.1]/RFC 5530 [3]). */
2603              if (!empty($resp->data['expungeissued'])) {
2604                  $this->noop();
2605              }
2606  
2607              foreach ($resp->fetch as $k => $v) {
2608                  $results->get($sequence ? $k : $v->getUid())->merge($v);
2609              }
2610          } catch (Horde_Imap_Client_Exception_ServerResponse $e) {
2611              if ($e->status === Horde_Imap_Client_Interaction_Server::NO) {
2612                  if ($e->getCode() === $e::UNKNOWNCTE) {
2613                      /* UNKNOWN-CTE error. Redo the query without the BINARY
2614                       * elements. */
2615                      $bq = $pipeline->data['binaryquery'];
2616  
2617                      foreach ($queries as $val) {
2618                          foreach ($bq as $key2 => $val2) {
2619                              unset($val2['decode']);
2620                              $val['_query']->bodyPart($key2, $val2);
2621                              $val['_query']->remove(Horde_Imap_Client::FETCH_BODYPARTSIZE, $key2);
2622                          }
2623                          $pipeline->data['fetch_followup'][] = $val;
2624                      }
2625                  } elseif ($sequence) {
2626                      /* A NO response, when coupled with a sequence FETCH, most
2627                       * likely means that messages were expunged. (RFC 2180
2628                       * [4.1]) */
2629                      $this->noop();
2630                  }
2631              }
2632          } catch (Exception $e) {
2633              // For any other error, ignore the Exception - fetch() is nice in
2634              // that the return value explicitly handles missing data for any
2635              // given message.
2636          }
2637  
2638          if (!empty($pipeline->data['fetch_followup'])) {
2639              $this->_fetch($results, $pipeline->data['fetch_followup']);
2640          }
2641      }
2642  
2643      /**
2644       * Add a FETCH command to the given pipeline.
2645       *
2646       * @param Horde_Imap_Client_Interaction_Pipeline $pipeline  Pipeline
2647       *                                                          object.
2648       * @param array $options                                    Fetch query
2649       *                                                          options
2650       */
2651      protected function _fetchCmd(
2652          Horde_Imap_Client_Interaction_Pipeline $pipeline,
2653          $options
2654      )
2655      {
2656          $fetch = new Horde_Imap_Client_Data_Format_List();
2657          $sequence = $options['ids']->sequence;
2658  
2659          /* Build an IMAP4rev1 compliant FETCH query. We handle the following
2660           * criteria:
2661           *   BINARY[.PEEK][<section #>]<<partial>> (RFC 3516)
2662           *     see BODY[] response
2663           *   BINARY.SIZE[<section #>] (RFC 3516)
2664           *   BODY[.PEEK][<section>]<<partial>>
2665           *     <section> = HEADER, HEADER.FIELDS, HEADER.FIELDS.NOT, MIME,
2666           *                 TEXT, empty
2667           *     <<partial>> = 0.# (# of bytes)
2668           *   BODYSTRUCTURE
2669           *   ENVELOPE
2670           *   FLAGS
2671           *   INTERNALDATE
2672           *   MODSEQ (RFC 7162)
2673           *   RFC822.SIZE
2674           *   UID
2675           *
2676           * No need to support these (can be built from other queries):
2677           * ===========================================================
2678           *   ALL macro => (FLAGS INTERNALDATE RFC822.SIZE ENVELOPE)
2679           *   BODY => Use BODYSTRUCTURE instead
2680           *   FAST macro => (FLAGS INTERNALDATE RFC822.SIZE)
2681           *   FULL macro => (FLAGS INTERNALDATE RFC822.SIZE ENVELOPE BODY)
2682           *   RFC822 => BODY[]
2683           *   RFC822.HEADER => BODY[HEADER]
2684           *   RFC822.TEXT => BODY[TEXT]
2685           */
2686  
2687          foreach ($options['_query'] as $type => $c_val) {
2688              switch ($type) {
2689              case Horde_Imap_Client::FETCH_STRUCTURE:
2690                  $fetch->add('BODYSTRUCTURE');
2691                  break;
2692  
2693              case Horde_Imap_Client::FETCH_FULLMSG:
2694                  if (empty($c_val['peek'])) {
2695                      $this->openMailbox($this->_selected, Horde_Imap_Client::OPEN_READWRITE);
2696                  }
2697                  $fetch->add(
2698                      'BODY' .
2699                      (!empty($c_val['peek']) ? '.PEEK' : '') .
2700                      '[]' .
2701                      $this->_partialAtom($c_val)
2702                  );
2703                  break;
2704  
2705              case Horde_Imap_Client::FETCH_HEADERTEXT:
2706              case Horde_Imap_Client::FETCH_BODYTEXT:
2707              case Horde_Imap_Client::FETCH_MIMEHEADER:
2708              case Horde_Imap_Client::FETCH_BODYPART:
2709              case Horde_Imap_Client::FETCH_HEADERS:
2710                  foreach ($c_val as $key => $val) {
2711                      $cmd = ($key == 0)
2712                          ? ''
2713                          : $key . '.';
2714                      $main_cmd = 'BODY';
2715  
2716                      switch ($type) {
2717                      case Horde_Imap_Client::FETCH_HEADERTEXT:
2718                          $cmd .= 'HEADER';
2719                          break;
2720  
2721                      case Horde_Imap_Client::FETCH_BODYTEXT:
2722                          $cmd .= 'TEXT';
2723                          break;
2724  
2725                      case Horde_Imap_Client::FETCH_MIMEHEADER:
2726                          $cmd .= 'MIME';
2727                          break;
2728  
2729                      case Horde_Imap_Client::FETCH_BODYPART:
2730                          // Remove the last dot from the string.
2731                          $cmd = substr($cmd, 0, -1);
2732  
2733                          if (!empty($val['decode']) &&
2734                              $this->queryCapability('BINARY')) {
2735                              $main_cmd = 'BINARY';
2736                              $pipeline->data['binaryquery'][$key] = $val;
2737                          }
2738                          break;
2739  
2740                      case Horde_Imap_Client::FETCH_HEADERS:
2741                          $cmd .= 'HEADER.FIELDS';
2742                          if (!empty($val['notsearch'])) {
2743                              $cmd .= '.NOT';
2744                          }
2745                          $cmd .= ' (' . implode(' ', array_map('strtoupper', $val['headers'])) . ')';
2746  
2747                          // Maintain a command -> label lookup so we can put
2748                          // the results in the proper location.
2749                          $pipeline->data['fetch_lookup'][$cmd] = $key;
2750                      }
2751  
2752                      if (empty($val['peek'])) {
2753                          $this->openMailbox($this->_selected, Horde_Imap_Client::OPEN_READWRITE);
2754                      }
2755  
2756                      $fetch->add(
2757                          $main_cmd .
2758                          (!empty($val['peek']) ? '.PEEK' : '') .
2759                          '[' . $cmd . ']' .
2760                          $this->_partialAtom($val)
2761                      );
2762                  }
2763                  break;
2764  
2765              case Horde_Imap_Client::FETCH_BODYPARTSIZE:
2766                  if ($this->queryCapability('BINARY')) {
2767                      foreach ($c_val as $val) {
2768                          $fetch->add('BINARY.SIZE[' . $val . ']');
2769                      }
2770                  }
2771                  break;
2772  
2773              case Horde_Imap_Client::FETCH_ENVELOPE:
2774                  $fetch->add('ENVELOPE');
2775                  break;
2776  
2777              case Horde_Imap_Client::FETCH_FLAGS:
2778                  $fetch->add('FLAGS');
2779                  break;
2780  
2781              case Horde_Imap_Client::FETCH_IMAPDATE:
2782                  $fetch->add('INTERNALDATE');
2783                  break;
2784  
2785              case Horde_Imap_Client::FETCH_SIZE:
2786                  $fetch->add('RFC822.SIZE');
2787                  break;
2788  
2789              case Horde_Imap_Client::FETCH_UID:
2790                  /* A UID FETCH will always return UID information (RFC 3501
2791                   * [6.4.8]). Don't add to query as it just creates a longer
2792                   * FETCH command. */
2793                  if ($sequence || (count($options['_query']) === 1)) {
2794                      $fetch->add('UID');
2795                  }
2796                  break;
2797  
2798              case Horde_Imap_Client::FETCH_SEQ:
2799                  // Nothing we need to add to fetch request unless sequence is
2800                  // the only criteria.
2801                  if (count($options['_query']) === 1) {
2802                      $fetch->add('UID');
2803                  }
2804                  break;
2805  
2806              case Horde_Imap_Client::FETCH_MODSEQ:
2807                  /* The 'changedsince' modifier implicitly adds the MODSEQ
2808                   * FETCH item (RFC 7162 [3.1.4.1]). Don't add to query as it
2809                   * just creates a longer FETCH command. */
2810                  if (empty($options['changedsince'])) {
2811                      $fetch->add('MODSEQ');
2812                  }
2813                  break;
2814              }
2815          }
2816  
2817          /* Add changedsince parameters. */
2818          if (empty($options['changedsince'])) {
2819              $fetch_cmd = $fetch;
2820          } else {
2821              /* We might just want the list of UIDs changed since a given
2822               * modseq. In that case, we don't have any other FETCH attributes,
2823               * but RFC 3501 requires at least one specified attribute. */
2824              $fetch_cmd = array(
2825                  count($fetch)
2826                      ? $fetch
2827                      : new Horde_Imap_Client_Data_Format_List('UID'),
2828                  new Horde_Imap_Client_Data_Format_List(array(
2829                      'CHANGEDSINCE',
2830                      new Horde_Imap_Client_Data_Format_Number($options['changedsince'])
2831                  ))
2832              );
2833          }
2834  
2835          /* The FETCH command should be the only command issued by this library
2836           * that should ever approach this limit.
2837           * @todo Move this check to a more centralized location (_command()?).
2838           * For simplification, assume that the UID list is the limiting factor
2839           * and split this list at a sequence comma delimiter if it exceeds
2840           * the character limit. */
2841          foreach ($options['ids']->split($this->_init['cmdlength']) as $val) {
2842              $cmd = $this->_command(
2843                  $sequence ? 'FETCH' : 'UID FETCH'
2844              )->add(array(
2845                  $val,
2846                  $fetch_cmd
2847              ));
2848              $pipeline->add($cmd);
2849          }
2850      }
2851  
2852      /**
2853       * Add a partial atom to an IMAP command based on the criteria options.
2854       *
2855       * @param array $opts  Criteria options.
2856       *
2857       * @return string  The partial atom.
2858       */
2859      protected function _partialAtom($opts)
2860      {
2861          if (!empty($opts['length'])) {
2862              return '<' . (empty($opts['start']) ? 0 : intval($opts['start'])) . '.' . intval($opts['length']) . '>';
2863          }
2864  
2865          return empty($opts['start'])
2866              ? ''
2867              : ('<' . intval($opts['start']) . '>');
2868      }
2869  
2870      /**
2871       * Parse a FETCH response (RFC 3501 [7.4.2]). A FETCH response may occur
2872       * due to a FETCH command, or due to a change in a message's state (i.e.
2873       * the flags change).
2874       *
2875       * @param Horde_Imap_Client_Interaction_Pipeline $pipeline  Pipeline
2876       *                                                          object.
2877       * @param integer $id                       The message sequence number.
2878       * @param Horde_Imap_Client_Tokenize $data  The server response.
2879       */
2880      protected function _parseFetch(
2881          Horde_Imap_Client_Interaction_Pipeline $pipeline,
2882          $id,
2883          Horde_Imap_Client_Tokenize $data
2884      )
2885      {
2886          if ($data->next() !== true) {
2887              return;
2888          }
2889  
2890          $ob = $pipeline->fetch->get($id);
2891          $ob->setSeq($id);
2892  
2893          $flags = $modseq = $uid = false;
2894  
2895          while (($tag = $data->next()) !== false) {
2896              $tag = strtoupper($tag);
2897  
2898              switch ($tag) {
2899              case 'BODYSTRUCTURE':
2900                  $data->next();
2901                  $structure = $this->_parseBodystructure($data);
2902                  $structure->buildMimeIds();
2903                  $ob->setStructure($structure);
2904                  break;
2905  
2906              case 'ENVELOPE':
2907                  $data->next();
2908                  $ob->setEnvelope($this->_parseEnvelope($data));
2909                  break;
2910  
2911              case 'FLAGS':
2912                  $data->next();
2913                  $ob->setFlags($data->flushIterator());
2914                  $flags = true;
2915                  break;
2916  
2917              case 'INTERNALDATE':
2918                  $ob->setImapDate($data->next());
2919                  break;
2920  
2921              case 'RFC822.SIZE':
2922                  $ob->setSize($data->next());
2923                  break;
2924  
2925              case 'UID':
2926                  $ob->setUid($data->next());
2927                  $uid = true;
2928                  break;
2929  
2930              case 'MODSEQ':
2931                  $data->next();
2932                  $modseq = $data->next();
2933                  $data->next();
2934  
2935                  /* MODSEQ must be greater than 0, so do sanity checking. */
2936                  if ($modseq > 0) {
2937                      $ob->setModSeq($modseq);
2938  
2939                      /* Store MODSEQ value. It may be used as the highestmodseq
2940                       * once a tagged response is received (RFC 7162 [6]). */
2941                      $pipeline->data['modseqs'][] = $modseq;
2942                  }
2943                  break;
2944  
2945              default:
2946                  // Catch BODY[*]<#> responses
2947                  if (strpos($tag, 'BODY[') === 0) {
2948                      // Remove the beginning 'BODY['
2949                      $tag = substr($tag, 5);
2950  
2951                      // BODY[HEADER.FIELDS] request
2952                      if (!empty($pipeline->data['fetch_lookup']) &&
2953                          (strpos($tag, 'HEADER.FIELDS') !== false)) {
2954                          $data->next();
2955                          $sig = $tag . ' (' . implode(' ', array_map('strtoupper', $data->flushIterator())) . ')';
2956  
2957                          // Ignore the trailing bracket
2958                          $data->next();
2959  
2960                          $ob->setHeaders($pipeline->data['fetch_lookup'][$sig], $data->next());
2961                      } else {
2962                          // Remove trailing bracket and octet start info
2963                          $tag = substr($tag, 0, strrpos($tag, ']'));
2964  
2965                          if (!strlen($tag)) {
2966                              // BODY[] request
2967                              if (!is_null($tmp = $data->next())) {
2968                                  $ob->setFullMsg($tmp);
2969                              }
2970                          } elseif (is_numeric(substr($tag, -1))) {
2971                              // BODY[MIMEID] request
2972                              if (!is_null($tmp = $data->next())) {
2973                                  $ob->setBodyPart($tag, $tmp);
2974                              }
2975                          } else {
2976                              // BODY[HEADER|TEXT|MIME] request
2977                              if (($last_dot = strrpos($tag, '.')) === false) {
2978                                  $mime_id = 0;
2979                              } else {
2980                                  $mime_id = substr($tag, 0, $last_dot);
2981                                  $tag = substr($tag, $last_dot + 1);
2982                              }
2983  
2984                              if (!is_null($tmp = $data->next())) {
2985                                  switch ($tag) {
2986                                  case 'HEADER':
2987                                      $ob->setHeaderText($mime_id, $tmp);
2988                                      break;
2989  
2990                                  case 'TEXT':
2991                                      $ob->setBodyText($mime_id, $tmp);
2992                                      break;
2993  
2994                                  case 'MIME':
2995                                      $ob->setMimeHeader($mime_id, $tmp);
2996                                      break;
2997                                  }
2998                              }
2999                          }
3000                      }
3001                  } elseif (strpos($tag, 'BINARY[') === 0) {
3002                      // Catch BINARY[*]<#> responses
3003                      // Remove the beginning 'BINARY[' and the trailing bracket
3004                      // and octet start info
3005                      $tag = substr($tag, 7, strrpos($tag, ']') - 7);
3006                      $body = $data->next();
3007  
3008                      if (is_null($body)) {
3009                          /* Dovecot bug (as of 2.2.12): binary fetch of body
3010                           * part may fail with NIL return if decoding failed on
3011                           * server. Try again with non-decoded body. */
3012                          $bq = $pipeline->data['binaryquery'][$tag];
3013                          unset($bq['decode']);
3014  
3015                          $query = new Horde_Imap_Client_Fetch_Query();
3016                          $query->bodyPart($tag, $bq);
3017  
3018                          $qids = ($quid = $ob->getUid())
3019                              ? new Horde_Imap_Client_Ids($quid)
3020                              : new Horde_Imap_Client_Ids($id, true);
3021  
3022                          $pipeline->data['fetch_followup'][] = array(
3023                              '_query' => $query,
3024                              'ids' => $qids
3025                          );
3026                      } else {
3027                          $ob->setBodyPart(
3028                              $tag,
3029                              $body,
3030                              empty($this->_temp['literal8']) ? '8bit' : 'binary'
3031                          );
3032                      }
3033                  } elseif (strpos($tag, 'BINARY.SIZE[') === 0) {
3034                      // Catch BINARY.SIZE[*] responses
3035                      // Remove the beginning 'BINARY.SIZE[' and the trailing
3036                      // bracket and octet start info
3037                      $tag = substr($tag, 12, strrpos($tag, ']') - 12);
3038                      $ob->setBodyPartSize($tag, $data->next());
3039                  }
3040                  break;
3041              }
3042          }
3043  
3044          /* MODSEQ issue: Oh joy. Per RFC 5162 (see Errata #1807), FETCH FLAGS
3045           * responses are NOT required to provide UID information, even if
3046           * QRESYNC is explicitly enabled. Caveat: the FLAGS information
3047           * returned during a SELECT/EXAMINE MUST contain UIDs so we are OK
3048           * there.
3049           * The good news: all decent IMAP servers (Cyrus, Dovecot) will always
3050           * provide UID information, so this is not normally an issue.
3051           * The bad news: spec-wise, this behavior cannot be 100% guaranteed.
3052           * Compromise: We will watch for a FLAGS response with a MODSEQ and
3053           * check if a UID exists also. If not, put the sequence number in a
3054           * queue - it is possible the UID information may appear later in an
3055           * untagged response. When the command is over, double check to make
3056           * sure there are none of these MODSEQ/FLAGS that are still UID-less.
3057           * In the (rare) event that there is, don't cache anything and
3058           * immediately close the mailbox: flags will be correctly sync'd next
3059           * mailbox open so we only lose a bit of caching efficiency.
3060           * Otherwise, we could end up with an inconsistent cached state.
3061           * This Errata has been fixed in 7162 [3.2.4]. */
3062          if ($flags && $modseq && !$uid) {
3063              $pipeline->data['modseqs_nouid'][] = $id;
3064          }
3065      }
3066  
3067      /**
3068       * Recursively parse BODYSTRUCTURE data from a FETCH return (see
3069       * RFC 3501 [7.4.2]).
3070       *
3071       * @param Horde_Imap_Client_Tokenize $data  Data returned from the server.
3072       *
3073       * @return Horde_Mime_Part  Mime part object.
3074       */
3075      protected function _parseBodystructure(Horde_Imap_Client_Tokenize $data)
3076      {
3077          $ob = new Horde_Mime_Part();
3078  
3079          // If index 0 is an array, this is a multipart part.
3080          if (($entry = $data->next()) === true) {
3081              do {
3082                  $ob->addPart($this->_parseBodystructure($data));
3083              } while (($entry = $data->next()) === true);
3084  
3085              // The subpart type.
3086              $ob->setType('multipart/' . $entry);
3087  
3088              // After the subtype is further extension information. This
3089              // information MAY appear for BODYSTRUCTURE requests.
3090  
3091              // This is parameter information.
3092              if (($tmp = $data->next()) === false) {
3093                  return $ob;
3094              } elseif ($tmp === true) {
3095                  foreach ($this->_parseStructureParams($data, 'content-type') as $key => $val) {
3096                      $ob->setContentTypeParameter($key, $val);
3097                  }
3098              }
3099          } else {
3100              $ob->setType($entry . '/' . $data->next());
3101  
3102              if ($data->next() === true) {
3103                  foreach ($this->_parseStructureParams($data, 'content-type') as $key => $val) {
3104                      $ob->setContentTypeParameter($key, $val);
3105                  }
3106              }
3107  
3108              if (!is_null($tmp = $data->next())) {
3109                  $ob->setContentId($tmp);
3110              }
3111  
3112              if (!is_null($tmp = $data->next())) {
3113                  $ob->setDescription(Horde_Mime::decode($tmp));
3114              }
3115  
3116              if (!is_null($tmp = $data->next())) {
3117                  $ob->setTransferEncoding($tmp);
3118              }
3119  
3120              $ob->setBytes($data->next());
3121  
3122              // If the type is 'message/rfc822' or 'text/*', several extra
3123              // fields are included
3124              switch ($ob->getPrimaryType()) {
3125              case 'message':
3126                  if ($ob->getSubType() == 'rfc822') {
3127                      if ($data->next() === true) {
3128                          // Ignore: envelope
3129                          $data->flushIterator(false);
3130                      }
3131                      if ($data->next() === true) {
3132                          $ob->addPart($this->_parseBodystructure($data));
3133                      }
3134                      $data->next(); // Ignore: lines
3135                  }
3136                  break;
3137  
3138              case 'text':
3139                  $data->next(); // Ignore: lines
3140                  break;
3141              }
3142  
3143              // After the subtype is further extension information. This
3144              // information MAY appear for BODYSTRUCTURE requests.
3145  
3146              // Ignore: MD5
3147              if ($data->next() === false) {
3148                  return $ob;
3149              }
3150          }
3151  
3152          // This is disposition information
3153          if (($tmp = $data->next()) === false) {
3154              return $ob;
3155          } elseif ($tmp === true) {
3156              $ob->setDisposition($data->next());
3157  
3158              if ($data->next() === true) {
3159                  foreach ($this->_parseStructureParams($data, 'content-disposition') as $key => $val) {
3160                      $ob->setDispositionParameter($key, $val);
3161                  }
3162              }
3163              $data->next();
3164          }
3165  
3166          // This is language information. It is either a single value or a list
3167          // of values.
3168          if (($tmp = $data->next()) === false) {
3169              return $ob;
3170          } elseif (!is_null($tmp)) {
3171              $ob->setLanguage(($tmp === true) ? $data->flushIterator() : $tmp);
3172          }
3173  
3174          // Ignore location (RFC 2557) and consume closing paren.
3175          $data->flushIterator(false);
3176  
3177          return $ob;
3178      }
3179  
3180      /**
3181       * Helper function to parse a parameters-like tokenized array.
3182       *
3183       * @param mixed $data   Message data. Either a Horde_Imap_Client_Tokenize
3184       *                      object or null.
3185       * @param string $type  The header name.
3186       *
3187       * @return array  The parameter array.
3188       */
3189      protected function _parseStructureParams($data, $type)
3190      {
3191          $params = array();
3192  
3193          if (is_null($data)) {
3194              return $params;
3195          }
3196  
3197          while (($name = $data->next()) !== false) {
3198              $params[strtolower($name)] = $data->next();
3199          }
3200  
3201          $ret = Horde_Mime::decodeParam($type, $params);
3202  
3203          return $ret['params'];
3204      }
3205  
3206      /**
3207       * Parse ENVELOPE data from a FETCH return (see RFC 3501 [7.4.2]).
3208       *
3209       * @param Horde_Imap_Client_Tokenize $data  Data returned from the server.
3210       *
3211       * @return Horde_Imap_Client_Data_Envelope  An envelope object.
3212       */
3213      protected function _parseEnvelope(Horde_Imap_Client_Tokenize $data)
3214      {
3215          // 'route', the 2nd element, is deprecated by RFC 2822.
3216          $addr_structure = array(
3217              0 => 'personal',
3218              2 => 'mailbox',
3219              3 => 'host'
3220          );
3221          $env_data = array(
3222              0 => 'date',
3223              1 => 'subject',
3224              2 => 'from',
3225              3 => 'sender',
3226              4 => 'reply_to',
3227              5 => 'to',
3228              6 => 'cc',
3229              7 => 'bcc',
3230              8 => 'in_reply_to',
3231              9 => 'message_id'
3232          );
3233  
3234          $addr_ob = new Horde_Mail_Rfc822_Address();
3235          $env_addrs = $this->getParam('envelope_addrs');
3236          $env_str = $this->getParam('envelope_string');
3237          $key = 0;
3238          $ret = new Horde_Imap_Client_Data_Envelope();
3239  
3240          while (($val = $data->next()) !== false) {
3241              if (!isset($env_data[$key]) || is_null($val)) {
3242                  ++$key;
3243                  continue;
3244              }
3245  
3246              if (is_string($val)) {
3247                  // These entries are text fields.
3248                  $ret->{$env_data[$key]} = substr($val, 0, $env_str);
3249              } else {
3250                  // These entries are address structures.
3251                  $group = null;
3252                  $key2 = 0;
3253                  $tmp = new Horde_Mail_Rfc822_List();
3254  
3255                  while ($data->next() !== false) {
3256                      $a_val = $data->flushIterator();
3257  
3258                      // RFC 3501 [7.4.2]: Group entry when host is NIL.
3259                      // Group end when mailbox is NIL; otherwise, this is
3260                      // mailbox name.
3261                      if (is_null($a_val[3])) {
3262                          if (is_null($a_val[2])) {
3263                              $group = null;
3264                          } else {
3265                              $group = new Horde_Mail_Rfc822_Group($a_val[2]);
3266                              $tmp->add($group);
3267                          }
3268                      } else {
3269                          $addr = clone $addr_ob;
3270  
3271                          foreach ($addr_structure as $add_key => $add_val) {
3272                              if (!is_null($a_val[$add_key])) {
3273                                  $addr->$add_val = $a_val[$add_key];
3274                              }
3275                          }
3276  
3277                          if ($group) {
3278                              $group->addresses->add($addr);
3279                          } else {
3280                              $tmp->add($addr);
3281                          }
3282                      }
3283  
3284                      if (++$key2 >= $env_addrs) {
3285                          $data->flushIterator(false);
3286                          break;
3287                      }
3288                  }
3289  
3290                  $ret->{$env_data[$key]} = $tmp;
3291              }
3292  
3293              ++$key;
3294          }
3295  
3296          return $ret;
3297      }
3298  
3299      /**
3300       */
3301      protected function _vanished($modseq, Horde_Imap_Client_Ids $ids)
3302      {
3303          $pipeline = $this->_pipeline(
3304              $this->_command('UID FETCH')->add(array(
3305                  strval($ids),
3306                  'UID',
3307                  new Horde_Imap_Client_Data_Format_List(array(
3308                      'VANISHED',
3309                      'CHANGEDSINCE',
3310                      new Horde_Imap_Client_Data_Format_Number($modseq)
3311                  ))
3312              ))
3313          );
3314          $pipeline->data['vanished'] = $this->getIdsOb();
3315  
3316          return $this->_sendCmd($pipeline)->data['vanished'];
3317      }
3318  
3319      /**
3320       */
3321      protected function _store($options)
3322      {
3323          $pipeline = $this->_storeCmd($options);
3324          $pipeline->data['modified'] = $this->getIdsOb();
3325  
3326          try {
3327              $resp = $this->_sendCmd($pipeline);
3328  
3329              /* Check for EXPUNGEISSUED (RFC 2180 [4.2]/RFC 5530 [3]). */
3330              if (!empty($resp->data['expungeissued'])) {
3331                  $this->noop();
3332              }
3333  
3334              return $resp->data['modified'];
3335          } catch (Horde_Imap_Client_Exception_ServerResponse $e) {
3336              /* A NO response, when coupled with a sequence STORE and
3337               * non-SILENT behavior, most likely means that messages were
3338               * expunged. RFC 2180 [4.2] */
3339              if (empty($pipeline->data['store_silent']) &&
3340                  !empty($options['sequence']) &&
3341                  ($e->status === Horde_Imap_Client_Interaction_Server::NO)) {
3342                  $this->noop();
3343              }
3344  
3345              return $pipeline->data['modified'];
3346          }
3347      }
3348  
3349      /**
3350       * Create a store command.
3351       *
3352       * @param array $options  See Horde_Imap_Client_Base#_store().
3353       *
3354       * @return Horde_Imap_Client_Interaction_Pipeline  Pipeline object.
3355       */
3356      protected function _storeCmd($options)
3357      {
3358          $cmds = array();
3359          $silent = empty($options['unchangedsince'])
3360               ? !($this->_debug->debug || $this->_initCache(true))
3361               : false;
3362  
3363          if (!empty($options['replace'])) {
3364              $cmds[] = array(
3365                  'FLAGS' . ($silent ? '.SILENT' : ''),
3366                  $options['replace']
3367              );
3368          } else {
3369              foreach (array('add' => '+', 'remove' => '-') as $k => $v) {
3370                  if (!empty($options[$k])) {
3371                      $cmds[] = array(
3372                          $v . 'FLAGS' . ($silent ? '.SILENT' : ''),
3373                          $options[$k]
3374                      );
3375                  }
3376              }
3377          }
3378  
3379          $pipeline = $this->_pipeline();
3380          $pipeline->data['store_silent'] = $silent;
3381  
3382          foreach ($cmds as $val) {
3383              $cmd = $this->_command(
3384                  empty($options['sequence']) ? 'UID STORE' : 'STORE'
3385              )->add(strval($options['ids']));
3386              if (!empty($options['unchangedsince'])) {
3387                  $cmd->add(new Horde_Imap_Client_Data_Format_List(array(
3388                      'UNCHANGEDSINCE',
3389                      new Horde_Imap_Client_Data_Format_Number(intval($options['unchangedsince']))
3390                  )));
3391              }
3392              $cmd->add($val);
3393  
3394              $pipeline->add($cmd);
3395          }
3396  
3397          return $pipeline;
3398      }
3399  
3400      /**
3401       */
3402      protected function _copy(Horde_Imap_Client_Mailbox $dest, $options)
3403      {
3404          /* Check for MOVE command (RFC 6851). */
3405          $move_cmd = (!empty($options['move']) &&
3406                       $this->queryCapability('MOVE'));
3407  
3408          $cmd = $this->_pipeline(
3409              $this->_command(
3410                  ($options['ids']->sequence ? '' : 'UID ') . ($move_cmd ? 'MOVE' : 'COPY')
3411              )->add(array(
3412                  strval($options['ids']),
3413                  new Horde_Imap_Client_Data_Format_Mailbox($dest)
3414              ))
3415          );
3416          $cmd->data['copydest'] = $dest;
3417  
3418          // COPY returns no untagged information (RFC 3501 [6.4.7])
3419          try {
3420              $resp = $this->_sendCmd($cmd);
3421          } catch (Horde_Imap_Client_Exception $e) {
3422              if (!empty($options['create']) &&
3423                  !empty($e->resp_data['trycreate'])) {
3424                  $this->createMailbox($dest);
3425                  unset($options['create']);
3426                  return $this->_copy($dest, $options);
3427              }
3428              throw $e;
3429          }
3430  
3431          // If moving, delete the old messages now. Short-circuit if nothing
3432          // was moved.
3433          if (!$move_cmd &&
3434              !empty($options['move']) &&
3435              (isset($resp->data['copyuid']) ||
3436               !$this->queryCapability('UIDPLUS'))) {
3437              $this->expunge($this->_selected, array(
3438                  'delete' => true,
3439                  'ids' => $options['ids']
3440              ));
3441          }
3442  
3443          return isset($resp->data['copyuid'])
3444              ? $resp->data['copyuid']
3445              : true;
3446      }
3447  
3448      /**
3449       */
3450      protected function _setQuota(Horde_Imap_Client_Mailbox $root, $resources)
3451      {
3452          $limits = new Horde_Imap_Client_Data_Format_List();
3453  
3454          foreach ($resources as $key => $val) {
3455              $limits->add(array(
3456                  strtoupper($key),
3457                  new Horde_Imap_Client_Data_Format_Number($val)
3458              ));
3459          }
3460  
3461          $this->_sendCmd(
3462              $this->_command('SETQUOTA')->add(array(
3463                  new Horde_Imap_Client_Data_Format_Mailbox($root),
3464                  $limits
3465              ))
3466          );
3467      }
3468  
3469      /**
3470       */
3471      protected function _getQuota(Horde_Imap_Client_Mailbox $root)
3472      {
3473          $pipeline = $this->_pipeline(
3474              $this->_command('GETQUOTA')->add(
3475                  new Horde_Imap_Client_Data_Format_Mailbox($root)
3476              )
3477          );
3478          $pipeline->data['quotaresp'] = array();
3479  
3480          return reset($this->_sendCmd($pipeline)->data['quotaresp']);
3481      }
3482  
3483      /**
3484       * Parse a QUOTA response (RFC 2087 [5.1]).
3485       *
3486       * @param Horde_Imap_Client_Interaction_Pipeline $pipeline  Pipeline
3487       *                                                          object.
3488       * @param Horde_Imap_Client_Tokenize $data  The server response.
3489       */
3490      protected function _parseQuota(
3491          Horde_Imap_Client_Interaction_Pipeline $pipeline,
3492          Horde_Imap_Client_Tokenize $data
3493      )
3494      {
3495          $c = &$pipeline->data['quotaresp'];
3496  
3497          $root = $data->next();
3498          $c[$root] = array();
3499  
3500          $data->next();
3501  
3502          while (($curr = $data->next()) !== false) {
3503              $c[$root][strtolower($curr)] = array(
3504                  'usage' => $data->next(),
3505                  'limit' => $data->next()
3506              );
3507          }
3508      }
3509  
3510      /**
3511       */
3512      protected function _getQuotaRoot(Horde_Imap_Client_Mailbox $mailbox)
3513      {
3514          $pipeline = $this->_pipeline(
3515              $this->_command('GETQUOTAROOT')->add(
3516                  new Horde_Imap_Client_Data_Format_Mailbox($mailbox)
3517              )
3518          );
3519          $pipeline->data['quotaresp'] = array();
3520  
3521          return $this->_sendCmd($pipeline)->data['quotaresp'];
3522      }
3523  
3524      /**
3525       */
3526      protected function _setACL(Horde_Imap_Client_Mailbox $mailbox, $identifier,
3527                                 $options)
3528      {
3529          // SETACL returns no untagged information (RFC 4314 [3.1]).
3530          $this->_sendCmd(
3531              $this->_command('SETACL')->add(array(
3532                  new Horde_Imap_Client_Data_Format_Mailbox($mailbox),
3533                  new Horde_Imap_Client_Data_Format_Astring($identifier),
3534                  new Horde_Imap_Client_Data_Format_Astring($options['rights'])
3535              ))
3536          );
3537      }
3538  
3539      /**
3540       */
3541      protected function _deleteACL(Horde_Imap_Client_Mailbox $mailbox, $identifier)
3542      {
3543          // DELETEACL returns no untagged information (RFC 4314 [3.2]).
3544          $this->_sendCmd(
3545              $this->_command('DELETEACL')->add(array(
3546                  new Horde_Imap_Client_Data_Format_Mailbox($mailbox),
3547                  new Horde_Imap_Client_Data_Format_Astring($identifier)
3548              ))
3549          );
3550      }
3551  
3552      /**
3553       */
3554      protected function _getACL(Horde_Imap_Client_Mailbox $mailbox)
3555      {
3556          return $this->_sendCmd(
3557              $this->_command('GETACL')->add(
3558                  new Horde_Imap_Client_Data_Format_Mailbox($mailbox)
3559              )
3560          )->data['getacl'];
3561      }
3562  
3563      /**
3564       * Parse an ACL response (RFC 4314 [3.6]).
3565       *
3566       * @param Horde_Imap_Client_Interaction_Pipeline $pipeline  Pipeline
3567       *                                                          object.
3568       * @param Horde_Imap_Client_Tokenize $data  The server response.
3569       */
3570      protected function _parseACL(
3571          Horde_Imap_Client_Interaction_Pipeline $pipeline,
3572          Horde_Imap_Client_Tokenize $data
3573      )
3574      {
3575          $acl = array();
3576  
3577          // Ignore mailbox argument -> index 1
3578          $data->next();
3579  
3580          while (($curr = $data->next()) !== false) {
3581              $acl[$curr] = ($curr[0] === '-')
3582                  ? new Horde_Imap_Client_Data_AclNegative($data->next())
3583                  : new Horde_Imap_Client_Data_Acl($data->next());
3584          }
3585  
3586          $pipeline->data['getacl'] = $acl;
3587      }
3588  
3589      /**
3590       */
3591      protected function _listACLRights(Horde_Imap_Client_Mailbox $mailbox,
3592                                        $identifier)
3593      {
3594          $resp = $this->_sendCmd(
3595              $this->_command('LISTRIGHTS')->add(array(
3596                  new Horde_Imap_Client_Data_Format_Mailbox($mailbox),
3597                  new Horde_Imap_Client_Data_Format_Astring($identifier)
3598              ))
3599          );
3600  
3601          return isset($resp->data['listaclrights'])
3602              ? $resp->data['listaclrights']
3603              : new Horde_Imap_Client_Data_AclRights();
3604      }
3605  
3606      /**
3607       * Parse a LISTRIGHTS response (RFC 4314 [3.7]).
3608       *
3609       * @param Horde_Imap_Client_Interaction_Pipeline $pipeline  Pipeline
3610       *                                                          object.
3611       * @param Horde_Imap_Client_Tokenize $data  The server response.
3612       */
3613      protected function _parseListRights(
3614          Horde_Imap_Client_Interaction_Pipeline $pipeline,
3615          Horde_Imap_Client_Tokenize $data
3616      )
3617      {
3618          // Ignore mailbox and identifier arguments
3619          $data->next();
3620          $data->next();
3621  
3622          $pipeline->data['listaclrights'] = new Horde_Imap_Client_Data_AclRights(
3623              str_split($data->next()),
3624              $data->flushIterator()
3625          );
3626      }
3627  
3628      /**
3629       */
3630      protected function _getMyACLRights(Horde_Imap_Client_Mailbox $mailbox)
3631      {
3632          $resp = $this->_sendCmd(
3633              $this->_command('MYRIGHTS')->add(
3634                  new Horde_Imap_Client_Data_Format_Mailbox($mailbox)
3635              )
3636          );
3637  
3638          return isset($resp->data['myrights'])
3639              ? $resp->data['myrights']
3640              : new Horde_Imap_Client_Data_Acl();
3641      }
3642  
3643      /**
3644       * Parse a MYRIGHTS response (RFC 4314 [3.8]).
3645       *
3646       * @param Horde_Imap_Client_Interaction_Pipeline $pipeline  Pipeline
3647       *                                                          object.
3648       * @param Horde_Imap_Client_Tokenize $data  The server response.
3649       */
3650      protected function _parseMyRights(
3651          Horde_Imap_Client_Interaction_Pipeline $pipeline,
3652          Horde_Imap_Client_Tokenize $data
3653      )
3654      {
3655          // Ignore 1st token (mailbox name)
3656          $data->next();
3657  
3658          $pipeline->data['myrights'] = new Horde_Imap_Client_Data_Acl($data->next());
3659      }
3660  
3661      /**
3662       */
3663      protected function _getMetadata(Horde_Imap_Client_Mailbox $mailbox,
3664                                      $entries, $options)
3665      {
3666          $pipeline = $this->_pipeline();
3667          $pipeline->data['metadata'] = array();
3668  
3669          if ($this->queryCapability('METADATA') ||
3670              (strlen($mailbox) && $this->queryCapability('METADATA-SERVER'))) {
3671              $cmd_options = new Horde_Imap_Client_Data_Format_List();
3672  
3673              if (!empty($options['maxsize'])) {
3674                  $cmd_options->add(array(
3675                      'MAXSIZE',
3676                      new Horde_Imap_Client_Data_Format_Number($options['maxsize'])
3677                  ));
3678              }
3679              if (!empty($options['depth'])) {
3680                  $cmd_options->add(array(
3681                      'DEPTH',
3682                      new Horde_Imap_Client_Data_Format_Number($options['depth'])
3683                  ));
3684              }
3685  
3686              $queries = new Horde_Imap_Client_Data_Format_List();
3687              foreach ($entries as $md_entry) {
3688                  $queries->add(new Horde_Imap_Client_Data_Format_Astring($md_entry));
3689              }
3690  
3691              $cmd = $this->_command('GETMETADATA')->add(
3692                  new Horde_Imap_Client_Data_Format_Mailbox($mailbox)
3693              );
3694              if (count($cmd_options)) {
3695                  $cmd->add($cmd_options);
3696              }
3697              $cmd->add($queries);
3698  
3699              $pipeline->add($cmd);
3700          } else {
3701              if (!$this->queryCapability('ANNOTATEMORE') &&
3702                  !$this->queryCapability('ANNOTATEMORE2')) {
3703                  throw new Horde_Imap_Client_Exception_NoSupportExtension('METADATA');
3704              }
3705  
3706              $queries = array();
3707              foreach ($entries as $md_entry) {
3708                  list($entry, $type) = $this->_getAnnotateMoreEntry($md_entry);
3709  
3710                  if (!isset($queries[$type])) {
3711                      $queries[$type] = new Horde_Imap_Client_Data_Format_List();
3712                  }
3713                  $queries[$type]->add(new Horde_Imap_Client_Data_Format_String($entry));
3714              }
3715  
3716              foreach ($queries as $key => $val) {
3717                  // TODO: Honor maxsize and depth options.
3718                  $pipeline->add(
3719                      $this->_command('GETANNOTATION')->add(array(
3720                          new Horde_Imap_Client_Data_Format_Mailbox($mailbox),
3721                          $val,
3722                          new Horde_Imap_Client_Data_Format_String($key)
3723                      ))
3724                  );
3725              }
3726          }
3727  
3728          return $this->_sendCmd($pipeline)->data['metadata'];
3729      }
3730  
3731      /**
3732       * Split a name for the METADATA extension into the correct syntax for the
3733       * older ANNOTATEMORE version.
3734       *
3735       * @param string $name  A name for a metadata entry.
3736       *
3737       * @return array  A list of two elements: The entry name and the value
3738       *                type.
3739       *
3740       * @throws Horde_Imap_Client_Exception
3741       */
3742      protected function _getAnnotateMoreEntry($name)
3743      {
3744          if (substr($name, 0, 7) === '/shared') {
3745              return array(substr($name, 7), 'value.shared');
3746          } else if (substr($name, 0, 8) === '/private') {
3747              return array(substr($name, 8), 'value.priv');
3748          }
3749  
3750          throw new Horde_Imap_Client_Exception(
3751              sprintf(Horde_Imap_Client_Translation::r("Invalid METADATA entry: \"%s\"."), $name),
3752              Horde_Imap_Client_Exception::METADATA_INVALID
3753          );
3754      }
3755  
3756      /**
3757       */
3758      protected function _setMetadata(Horde_Imap_Client_Mailbox $mailbox, $data)
3759      {
3760          if ($this->queryCapability('METADATA') ||
3761              (strlen($mailbox) && $this->queryCapability('METADATA-SERVER'))) {
3762              $data_elts = new Horde_Imap_Client_Data_Format_List();
3763  
3764              foreach ($data as $key => $value) {
3765                  $data_elts->add(array(
3766                      new Horde_Imap_Client_Data_Format_Astring($key),
3767                      new Horde_Imap_Client_Data_Format_Nstring($value)
3768                  ));
3769              }
3770  
3771              $cmd = $this->_command('SETMETADATA')->add(array(
3772                  new Horde_Imap_Client_Data_Format_Mailbox($mailbox),
3773                  $data_elts
3774              ));
3775          } else {
3776              if (!$this->queryCapability('ANNOTATEMORE') &&
3777                  !$this->queryCapability('ANNOTATEMORE2')) {
3778                  throw new Horde_Imap_Client_Exception_NoSupportExtension('METADATA');
3779              }
3780  
3781              $cmd = $this->_pipeline();
3782  
3783              foreach ($data as $md_entry => $value) {
3784                  list($entry, $type) = $this->_getAnnotateMoreEntry($md_entry);
3785  
3786                  $cmd->add(
3787                      $this->_command('SETANNOTATION')->add(array(
3788                          new Horde_Imap_Client_Data_Format_Mailbox($mailbox),
3789                          new Horde_Imap_Client_Data_Format_String($entry),
3790                          new Horde_Imap_Client_Data_Format_List(array(
3791                              new Horde_Imap_Client_Data_Format_String($type),
3792                              new Horde_Imap_Client_Data_Format_Nstring($value)
3793                          ))
3794                      ))
3795                  );
3796              }
3797          }
3798  
3799          $this->_sendCmd($cmd);
3800      }
3801  
3802      /**
3803       * Parse an ANNOTATION response (ANNOTATEMORE/ANNOTATEMORE2).
3804       *
3805       * @param Horde_Imap_Client_Interaction_Pipeline $pipeline  Pipeline
3806       *                                                          object.
3807       * @param Horde_Imap_Client_Tokenize $data  The server response.
3808       *
3809       * @throws Horde_Imap_Client_Exception
3810       */
3811      protected function _parseAnnotation(
3812          Horde_Imap_Client_Interaction_Pipeline $pipeline,
3813          Horde_Imap_Client_Tokenize $data
3814      )
3815      {
3816          // Mailbox name is in UTF7-IMAP.
3817          $mbox = Horde_Imap_Client_Mailbox::get($data->next(), true);
3818          $entry = $data->next();
3819  
3820          // Ignore unsolicited responses.
3821          if ($data->next() !== true) {
3822              return;
3823          }
3824  
3825          while (($type = $data->next()) !== false) {
3826              switch ($type) {
3827              case 'value.priv':
3828                  $pipeline->data['metadata'][strval($mbox)]['/private' . $entry] = $data->next();
3829                  break;
3830  
3831              case 'value.shared':
3832                  $pipeline->data['metadata'][strval($mbox)]['/shared' . $entry] = $data->next();
3833                  break;
3834  
3835              default:
3836                  throw new Horde_Imap_Client_Exception(
3837                      sprintf(Horde_Imap_Client_Translation::r("Invalid METADATA value type \"%s\"."), $type),
3838                      Horde_Imap_Client_Exception::METADATA_INVALID
3839                  );
3840              }
3841          }
3842      }
3843  
3844      /**
3845       * Parse a METADATA response (RFC 5464 [4.4]).
3846       *
3847       * @param Horde_Imap_Client_Interaction_Pipeline $pipeline  Pipeline
3848       *                                                          object.
3849       * @param Horde_Imap_Client_Tokenize $data  The server response.
3850       *
3851       * @throws Horde_Imap_Client_Exception
3852       */
3853      protected function _parseMetadata(
3854          Horde_Imap_Client_Interaction_Pipeline $pipeline,
3855          Horde_Imap_Client_Tokenize $data
3856      )
3857      {
3858          // Mailbox name is in UTF7-IMAP.
3859          $mbox = Horde_Imap_Client_Mailbox::get($data->next(), true);
3860  
3861          // Ignore unsolicited responses.
3862          if ($data->next() === true) {
3863              while (($entry = $data->next()) !== false) {
3864                  $pipeline->data['metadata'][strval($mbox)][$entry] = $data->next();
3865              }
3866          }
3867      }
3868  
3869      /* Overriden methods. */
3870  
3871      /**
3872       * @param array $opts  Options:
3873       *   - decrement: (boolean) If true, decrement the message count.
3874       *   - pipeline: (Horde_Imap_Client_Interaction_Pipeline) Pipeline object.
3875       */
3876      protected function _deleteMsgs(Horde_Imap_Client_Mailbox $mailbox,
3877                                     Horde_Imap_Client_Ids $ids,
3878                                     array $opts = array())
3879      {
3880          /* If there are pending FETCH cache writes, we need to write them
3881           * before the UID -> sequence number mapping changes. */
3882          if (isset($opts['pipeline'])) {
3883              $this->_updateCache($opts['pipeline']->fetch);
3884          }
3885  
3886          $res = parent::_deleteMsgs($mailbox, $ids);
3887  
3888          if (isset($this->_temp['expunged'])) {
3889              $this->_temp['expunged']->add($res);
3890          }
3891  
3892          if (!empty($opts['decrement'])) {
3893              $mbox_ob = $this->_mailboxOb();
3894              $mbox_ob->setStatus(
3895                  Horde_Imap_Client::STATUS_MESSAGES,
3896                  $mbox_ob->getStatus(Horde_Imap_Client::STATUS_MESSAGES) - count($ids)
3897              );
3898          }
3899      }
3900  
3901      /* Internal functions. */
3902  
3903      /**
3904       * Sends command(s) to the IMAP server. A connection to the server must
3905       * have already been made.
3906       *
3907       * @param mixed $cmd  Either a Command object or a Pipeline object.
3908       *
3909       * @return Horde_Imap_Client_Interaction_Pipeline  A pipeline object.
3910       * @throws Horde_Imap_Client_Exception
3911       */
3912      protected function _sendCmd($cmd)
3913      {
3914          $pipeline = ($cmd instanceof Horde_Imap_Client_Interaction_Command)
3915              ? $this->_pipeline($cmd)
3916              : $cmd;
3917  
3918          if (!empty($this->_cmdQueue)) {
3919              /* Add commands in reverse order. */
3920              foreach (array_reverse($this->_cmdQueue) as $val) {
3921                  $pipeline->add($val, true);
3922              }
3923  
3924              $this->_cmdQueue = array();
3925          }
3926  
3927          $cmd_list = array();
3928  
3929          foreach ($pipeline as $val) {
3930              if ($val->continuation) {
3931                  $this->_sendCmdChunk($pipeline, $cmd_list);
3932                  $this->_sendCmdChunk($pipeline, array($val));
3933                  $cmd_list = array();
3934              } else {
3935                  $cmd_list[] = $val;
3936              }
3937          }
3938  
3939          $this->_sendCmdChunk($pipeline, $cmd_list);
3940  
3941          /* If any FLAGS responses contain MODSEQs but not UIDs, don't
3942           * cache any data and immediately close the mailbox. */
3943          foreach ($pipeline->data['modseqs_nouid'] as $val) {
3944              if (!$pipeline->fetch[$val]->getUid()) {
3945                  $this->_debug->info(
3946                      'Server provided FLAGS MODSEQ without providing UID.'
3947                  );
3948                  $this->close();
3949                  return $pipeline;
3950              }
3951          }
3952  
3953          /* Update HIGHESTMODSEQ value. */
3954          if (!empty($pipeline->data['modseqs'])) {
3955              $modseq = max($pipeline->data['modseqs']);
3956              $this->_mailboxOb()->setStatus(Horde_Imap_Client::STATUS_HIGHESTMODSEQ, $modseq);
3957              /* CONDSTORE has not yet updated flag information, so don't update
3958               * modseq yet. */
3959              if (!empty($this->_temp['enabled']['QRESYNC'])) {
3960                  $this->_updateModSeq($modseq);
3961              }
3962          }
3963  
3964          /* Update cache items. */
3965          $this->_updateCache($pipeline->fetch);
3966  
3967          return $pipeline;
3968      }
3969  
3970      /**
3971       * Send a chunk of commands and/or continuation fragments to the server.
3972       *
3973       * @param Horde_Imap_Client_Interaction_Pipeline $pipeline  The pipeline
3974       *                                                          object.
3975       * @param array $chunk  List of commands to send.
3976       *
3977       * @throws Horde_Imap_Client_Exception
3978       */
3979      protected function _sendCmdChunk($pipeline, $chunk)
3980      {
3981          if (empty($chunk)) {
3982              return;
3983          }
3984  
3985          $cmd_count = count($chunk);
3986          $exception = null;
3987  
3988          foreach ($chunk as $val) {
3989              try {
3990                  $old_debug = $this->_debug->debug;
3991                  if (!is_null($val->debug)) {
3992                      $this->_debug->raw($val->tag . ' ' . $val->debug . "\n");
3993                      $this->_debug->debug = false;
3994                  }
3995                  if ($this->_processCmd($pipeline, $val, $val)) {
3996                      $this->_connection->write('', true);
3997                  } else {
3998                      $cmd_count = 0;
3999                  }
4000                  $this->_debug->debug = $old_debug;
4001              } catch (Horde_Imap_Client_Exception $e) {
4002                  $this->_debug->debug = $old_debug;
4003  
4004                  switch ($e->getCode()) {
4005                  case Horde_Imap_Client_Exception::SERVER_WRITEERROR:
4006                      $this->_temp['logout'] = true;
4007                      $this->logout();
4008                      break;
4009                  }
4010  
4011                  throw $e;
4012              }
4013          }
4014  
4015          while ($cmd_count) {
4016              try {
4017                  if ($this->_getLine($pipeline) instanceof Horde_Imap_Client_Interaction_Server_Tagged) {
4018                      --$cmd_count;
4019                  }
4020              } catch (Horde_Imap_Client_Exception $e) {
4021                  switch ($e->getCode()) {
4022                  case $e::DISCONNECT:
4023                      /* Guaranteed to have no more data incoming, so we can
4024                       * immediately logout. */
4025                      $this->_temp['logout'] = true;
4026                      $this->logout();
4027                      throw $e;
4028                  }
4029  
4030                  /* For all other issues, catch and store exception; don't
4031                   * throw until all input is read since we need to clear
4032                   * incoming queue. (For now, only store first exception.) */
4033                  if (is_null($exception)) {
4034                      $exception = $e;
4035                  }
4036  
4037                  if (($e instanceof Horde_Imap_Client_Exception_ServerResponse) &&
4038                      $e->command) {
4039                      --$cmd_count;
4040                  }
4041              }
4042          }
4043  
4044          if (!is_null($exception)) {
4045              throw $exception;
4046          }
4047      }
4048  
4049      /**
4050       * Process/send a command to the remote server.
4051       *
4052       * @param Horde_Imap_Client_Interaction_Pipeline $pipeline The pipeline
4053       *                                                         object.
4054       * @param Horde_Imap_Client_Interaction_Command $cmd  The master command.
4055       * @param Horde_Imap_Client_Data_Format_List $data    Commands to send.
4056       *
4057       * @return boolean  True if EOL needed to finish command.
4058       * @throws Horde_Imap_Client_Exception
4059       * @throws Horde_Imap_Client_Exception_NoSupport
4060       */
4061      protected function _processCmd($pipeline, $cmd, $data)
4062      {
4063          if ($this->_debug->debug &&
4064              ($data instanceof Horde_Imap_Client_Interaction_Command)) {
4065              $data->startTimer();
4066          }
4067  
4068          foreach ($data as $key => $val) {
4069              if ($val instanceof Horde_Imap_Client_Interaction_Command_Continuation) {
4070                  $this->_connection->write('', true);
4071  
4072                  /* Check for optional continuation responses when the command
4073                   * has already finished. */
4074                  if (!$cmd_continuation = $this->_processCmdContinuation($pipeline, $val->optional)) {
4075                      return false;
4076                  }
4077  
4078                  $this->_processCmd(
4079                      $pipeline,
4080                      $cmd,
4081                      $val->getCommands($cmd_continuation)
4082                  );
4083                  continue;
4084              }
4085  
4086              if ($key) {
4087                  $this->_connection->write(' ');
4088              }
4089  
4090              if ($val instanceof Horde_Imap_Client_Data_Format_List) {
4091                  $this->_connection->write('(');
4092                  $this->_processCmd($pipeline, $cmd, $val);
4093                  $this->_connection->write(')');
4094              } elseif (($val instanceof Horde_Imap_Client_Data_Format_String) &&
4095                        $val->literal()) {
4096                  /* RFC 3516/4466: Send literal8 if we have binary data. */
4097                  if ($cmd->literal8 &&
4098                      $val->binary() &&
4099                      $this->queryCapability('BINARY')) {
4100                      $binary = true;
4101                      $this->_connection->write('~');
4102                  } else {
4103                      $binary = false;
4104                  }
4105  
4106                  $literal_len = $val->length();
4107                  $this->_connection->write('{' . $literal_len);
4108  
4109                  /* RFC 2088 - If LITERAL+ is available, saves a roundtrip from
4110                   * the server. */
4111                  if ($cmd->literalplus && $this->queryCapability('LITERAL+')) {
4112                      $this->_connection->write('+}', true);
4113                  } else {
4114                      $this->_connection->write('}', true);
4115                      $this->_processCmdContinuation($pipeline);
4116                  }
4117  
4118                  $this->_connection->writeLiteral($val->getStream(), $literal_len, $binary);
4119              } else {
4120                  $this->_connection->write($val->escape());
4121              }
4122          }
4123  
4124          return true;
4125      }
4126  
4127      /**
4128       * Process a command continuation response.
4129       *
4130       * @param Horde_Imap_Client_Interaction_Pipeline $pipeline  The pipeline
4131       *                                                          object.
4132       * @param boolean $noexception                              Don't throw
4133       *                                                          exception if
4134       *                                                          continuation
4135       *                                                          does not occur.
4136       *
4137       * @return mixed  A Horde_Imap_Client_Interaction_Server_Continuation
4138       *                object or false.
4139       *
4140       * @throws Horde_Imap_Client_Exception
4141       */
4142      protected function _processCmdContinuation($pipeline, $noexception = false)
4143      {
4144          do {
4145              $ob = $this->_getLine($pipeline);
4146          } while ($ob instanceof Horde_Imap_Client_Interaction_Server_Untagged);
4147  
4148          if ($ob instanceof Horde_Imap_Client_Interaction_Server_Continuation) {
4149              return $ob;
4150          } elseif ($noexception) {
4151              return false;
4152          }
4153  
4154          $this->_debug->info(
4155              'ERROR: Unexpected response from server while waiting for a continuation request.'
4156          );
4157          $e = new Horde_Imap_Client_Exception(
4158              Horde_Imap_Client_Translation::r("Error when communicating with the mail server."),
4159              Horde_Imap_Client_Exception::SERVER_READERROR
4160          );
4161          $e->details = strval($ob);
4162  
4163          throw $e;
4164      }
4165  
4166      /**
4167       * Shortcut to creating a new IMAP client command object.
4168       *
4169       * @param string $cmd  The IMAP command.
4170       *
4171       * @return Horde_Imap_Client_Interaction_Command  A command object.
4172       */
4173      protected function _command($cmd)
4174      {
4175          return new Horde_Imap_Client_Interaction_Command($cmd, ++$this->_tag);
4176      }
4177  
4178      /**
4179       * Shortcut to creating a new pipeline object.
4180       *
4181       * @param Horde_Imap_Client_Interaction_Command $cmd  An IMAP command to
4182       *                                                    add.
4183       *
4184       * @return Horde_Imap_Client_Interaction_Pipeline  A pipeline object.
4185       */
4186      protected function _pipeline($cmd = null)
4187      {
4188          if (!isset($this->_temp['fetchob'])) {
4189              $this->_temp['fetchob'] = new Horde_Imap_Client_Fetch_Results(
4190                  $this->_fetchDataClass,
4191                  Horde_Imap_Client_Fetch_Results::SEQUENCE
4192              );
4193          }
4194  
4195          $ob = new Horde_Imap_Client_Interaction_Pipeline(
4196              clone $this->_temp['fetchob']
4197          );
4198  
4199          if (!is_null($cmd)) {
4200              $ob->add($cmd);
4201          }
4202  
4203          return $ob;
4204      }
4205  
4206      /**
4207       * Gets data from the IMAP server stream and parses it.
4208       *
4209       * @param Horde_Imap_Client_Interaction_Pipeline $pipeline  Pipeline
4210       *                                                          object.
4211       *
4212       * @return Horde_Imap_Client_Interaction_Server  Server object.
4213       *
4214       * @throws Horde_Imap_Client_Exception
4215       */
4216      protected function _getLine(
4217          Horde_Imap_Client_Interaction_Pipeline $pipeline
4218      )
4219      {
4220          $server = Horde_Imap_Client_Interaction_Server::create(
4221              $this->_connection->read()
4222          );
4223  
4224          switch (get_class($server)) {
4225          case 'Horde_Imap_Client_Interaction_Server_Continuation':
4226              $this->_responseCode($pipeline, $server);
4227              break;
4228  
4229          case 'Horde_Imap_Client_Interaction_Server_Tagged':
4230              $cmd = $pipeline->complete($server);
4231              if ($timer = $cmd->getTimer()) {
4232                  $this->_debug->info(sprintf(
4233                      'Command %s took %s seconds.',
4234                      $cmd->tag,
4235                      $timer
4236                  ));
4237              }
4238              $this->_responseCode($pipeline, $server);
4239              break;
4240  
4241          case 'Horde_Imap_Client_Interaction_Server_Untagged':
4242              if (is_null($server->status)) {
4243                  $this->_serverResponse($pipeline, $server);
4244              } else {
4245                  $this->_responseCode($pipeline, $server);
4246              }
4247              break;
4248          }
4249  
4250          switch ($server->status) {
4251          case $server::BAD:
4252          case $server::NO:
4253              /* A tagged BAD response indicates that the tagged command caused
4254               * the error. This information is unknown if untagged (RFC 3501
4255               * [7.1.3]) - ignore these untagged responses.
4256               * An untagged NO response indicates a warning; ignore and assume
4257               * that it also included response text code that is handled
4258               * elsewhere. Throw exception if tagged; command handlers can
4259               * catch this if able to workaround this issue (RFC 3501
4260               * [7.1.2]). */
4261              if ($server instanceof Horde_Imap_Client_Interaction_Server_Tagged) {
4262                  throw new Horde_Imap_Client_Exception_ServerResponse(
4263                      Horde_Imap_Client_Translation::r("IMAP error reported by server."),
4264                      0,
4265                      $server,
4266                      $pipeline
4267                  );
4268              }
4269              break;
4270  
4271          case $server::BYE:
4272              /* A BYE response received as part of a logout command should be
4273               * be treated like a regular command: a client MUST process the
4274               * entire command until logging out (RFC 3501 [3.4; 7.1.5]). */
4275              if (empty($this->_temp['logout'])) {
4276                  $e = new Horde_Imap_Client_Exception(
4277                      Horde_Imap_Client_Translation::r("IMAP Server closed the connection."),
4278                      Horde_Imap_Client_Exception::DISCONNECT
4279                  );
4280                  $e->details = strval($server);
4281                  throw $e;
4282              }
4283              break;
4284  
4285          case $server::PREAUTH:
4286              /* The user was pre-authenticated. (RFC 3501 [7.1.4]) */
4287              $this->_temp['preauth'] = true;
4288              break;
4289          }
4290  
4291          return $server;
4292      }
4293  
4294      /**
4295       * Handle untagged server responses (see RFC 3501 [2.2.2]).
4296       *
4297       * @param Horde_Imap_Client_Interaction_Pipeline $pipeline  Pipeline
4298       *                                                          object.
4299       * @param Horde_Imap_Client_Interaction_Server $ob          Server
4300       *                                                          response.
4301       */
4302      protected function _serverResponse(
4303          Horde_Imap_Client_Interaction_Pipeline $pipeline,
4304          Horde_Imap_Client_Interaction_Server $ob
4305      )
4306      {
4307          $token = $ob->token;
4308  
4309          /* First, catch untagged responses where the name appears first on the
4310           * line. */
4311          switch ($first = strtoupper($token->current())) {
4312          case 'CAPABILITY':
4313              $this->_parseCapability($pipeline, $token->flushIterator());
4314              break;
4315  
4316          case 'LIST':
4317          case 'LSUB':
4318              $this->_parseList($pipeline, $token);
4319              break;
4320  
4321          case 'STATUS':
4322              // Parse a STATUS response (RFC 3501 [7.2.4]).
4323              $this->_parseStatus($token);
4324              break;
4325  
4326          case 'SEARCH':
4327          case 'SORT':
4328              // Parse a SEARCH/SORT response (RFC 3501 [7.2.5] & RFC 5256 [4]).
4329              $this->_parseSearch($pipeline, $token->flushIterator());
4330              break;
4331  
4332          case 'ESEARCH':
4333              // Parse an ESEARCH response (RFC 4466 [2.6.2]).
4334              $this->_parseEsearch($pipeline, $token);
4335              break;
4336  
4337          case 'FLAGS':
4338              $token->next();
4339              $this->_mailboxOb()->setStatus(Horde_Imap_Client::STATUS_FLAGS, array_map('strtolower', $token->flushIterator()));
4340              break;
4341  
4342          case 'QUOTA':
4343              $this->_parseQuota($pipeline, $token);
4344              break;
4345  
4346          case 'QUOTAROOT':
4347              // Ignore this line - we can get this information from
4348              // the untagged QUOTA responses.
4349              break;
4350  
4351          case 'NAMESPACE':
4352              $this->_parseNamespace($pipeline, $token);
4353              break;
4354  
4355          case 'THREAD':
4356              $this->_parseThread($pipeline, $token);
4357              break;
4358  
4359          case 'ACL':
4360              $this->_parseACL($pipeline, $token);
4361              break;
4362  
4363          case 'LISTRIGHTS':
4364              $this->_parseListRights($pipeline, $token);
4365              break;
4366  
4367          case 'MYRIGHTS':
4368              $this->_parseMyRights($pipeline, $token);
4369              break;
4370  
4371          case 'ID':
4372              // ID extension (RFC 2971)
4373              $this->_parseID($pipeline, $token);
4374              break;
4375  
4376          case 'ENABLED':
4377              // ENABLE extension (RFC 5161)
4378              $this->_parseEnabled($token);
4379              break;
4380  
4381          case 'LANGUAGE':
4382              // LANGUAGE extension (RFC 5255 [3.2])
4383              $this->_parseLanguage($token);
4384              break;
4385  
4386          case 'COMPARATOR':
4387              // I18NLEVEL=2 extension (RFC 5255 [4.7])
4388              $this->_parseComparator($pipeline, $token);
4389              break;
4390  
4391          case 'VANISHED':
4392              // QRESYNC extension (RFC 7162 [3.2.10])
4393              $this->_parseVanished($pipeline, $token);
4394              break;
4395  
4396          case 'ANNOTATION':
4397              // Parse an ANNOTATION response.
4398              $this->_parseAnnotation($pipeline, $token);
4399              break;
4400  
4401          case 'METADATA':
4402              // Parse a METADATA response.
4403              $this->_parseMetadata($pipeline, $token);
4404              break;
4405  
4406          default:
4407              // Next, look for responses where the keywords occur second.
4408              switch (strtoupper($token->next())) {
4409              case 'EXISTS':
4410                  // EXISTS response - RFC 3501 [7.3.2]
4411                  $mbox_ob = $this->_mailboxOb();
4412  
4413                  // Increment UIDNEXT if it is set.
4414                  if ($mbox_ob->open &&
4415                      ($uidnext = $mbox_ob->getStatus(Horde_Imap_Client::STATUS_UIDNEXT))) {
4416                      $mbox_ob->setStatus(Horde_Imap_Client::STATUS_UIDNEXT, $uidnext + $first - $mbox_ob->getStatus(Horde_Imap_Client::STATUS_MESSAGES));
4417                  }
4418  
4419                  $mbox_ob->setStatus(Horde_Imap_Client::STATUS_MESSAGES, $first);
4420                  break;
4421  
4422              case 'RECENT':
4423                  // RECENT response - RFC 3501 [7.3.1]
4424                  $this->_mailboxOb()->setStatus(Horde_Imap_Client::STATUS_RECENT, $first);
4425                  break;
4426  
4427              case 'EXPUNGE':
4428                  // EXPUNGE response - RFC 3501 [7.4.1]
4429                  $this->_deleteMsgs($this->_selected, $this->getIdsOb($first, true), array(
4430                      'decrement' => true,
4431                      'pipeline' => $pipeline
4432                  ));
4433                  $pipeline->data['expunge_seen'] = true;
4434                  break;
4435  
4436              case 'FETCH':
4437                  // FETCH response - RFC 3501 [7.4.2]
4438                  $this->_parseFetch($pipeline, $first, $token);
4439                  break;
4440              }
4441              break;
4442          }
4443      }
4444  
4445      /**
4446       * Handle status responses (see RFC 3501 [7.1]).
4447       *
4448       * @param Horde_Imap_Client_Interaction_Pipeline $pipeline  Pipeline
4449       *                                                          object.
4450       * @param Horde_Imap_Client_Interaction_Server $ob          Server object.
4451       *
4452       * @throws Horde_Imap_Client_Exception_ServerResponse
4453       */
4454      protected function _responseCode(
4455          Horde_Imap_Client_Interaction_Pipeline $pipeline,
4456          Horde_Imap_Client_Interaction_Server $ob
4457      )
4458      {
4459          if (is_null($ob->responseCode)) {
4460              return;
4461          }
4462  
4463          $rc = $ob->responseCode;
4464  
4465          switch ($rc->code) {
4466          case 'ALERT':
4467          // Defined by RFC 5530 [3] - Treat as an alert for now.
4468          case 'CONTACTADMIN':
4469              if (!isset($this->_temp['alerts'])) {
4470                  $this->_temp['alerts'] = array();
4471              }
4472              $this->_temp['alerts'][] = strval($ob->token);
4473              break;
4474  
4475          case 'BADCHARSET':
4476              /* Store valid search charsets if returned by server. */
4477              $s_charset = array();
4478              foreach ($rc->data[0] as $val) {
4479                  $s_charset[$val] = true;
4480              }
4481  
4482              if (!empty($s_charset)) {
4483                  $this->_setInit('s_charset', array_merge(
4484                      $this->_init['s_charset'],
4485                      $s_charset
4486                  ));
4487              }
4488  
4489              throw new Horde_Imap_Client_Exception_ServerResponse(
4490                  Horde_Imap_Client_Translation::r("Charset used in search query is not supported on the mail server."),
4491                  Horde_Imap_Client_Exception::BADCHARSET,
4492                  $ob,
4493                  $pipeline
4494              );
4495  
4496          case 'CAPABILITY':
4497              $this->_parseCapability($pipeline, $rc->data);
4498              break;
4499  
4500          case 'PARSE':
4501              /* Only throw error on NO/BAD. Message is human readable. */
4502              switch ($ob->status) {
4503              case Horde_Imap_Client_Interaction_Server::BAD:
4504              case Horde_Imap_Client_Interaction_Server::NO:
4505                  throw new Horde_Imap_Client_Exception_ServerResponse(
4506                      sprintf(Horde_Imap_Client_Translation::r("The mail server was unable to parse the contents of the mail message: %s"), strval($ob->token)),
4507                      Horde_Imap_Client_Exception::PARSEERROR,
4508                      $ob,
4509                      $pipeline
4510                  );
4511              }
4512              break;
4513  
4514          case 'READ-ONLY':
4515              $this->_mode = Horde_Imap_Client::OPEN_READONLY;
4516              break;
4517  
4518          case 'READ-WRITE':
4519              $this->_mode = Horde_Imap_Client::OPEN_READWRITE;
4520              break;
4521  
4522          case 'TRYCREATE':
4523              // RFC 3501 [7.1]
4524              $pipeline->data['trycreate'] = true;
4525              break;
4526  
4527          case 'PERMANENTFLAGS':
4528              $this->_mailboxOb()->setStatus(Horde_Imap_Client::STATUS_PERMFLAGS, array_map('strtolower', $rc->data[0]));
4529              break;
4530  
4531          case 'UIDNEXT':
4532              $this->_mailboxOb()->setStatus(Horde_Imap_Client::STATUS_UIDNEXT, $rc->data[0]);
4533              break;
4534  
4535          case 'UIDVALIDITY':
4536              $this->_mailboxOb()->setStatus(Horde_Imap_Client::STATUS_UIDVALIDITY, $rc->data[0]);
4537              break;
4538  
4539          case 'UNSEEN':
4540              /* This is different from the STATUS UNSEEN response - this item,
4541               * if defined, returns the first UNSEEN message in the mailbox. */
4542              $this->_mailboxOb()->setStatus(Horde_Imap_Client::STATUS_FIRSTUNSEEN, $rc->data[0]);
4543              break;
4544  
4545          case 'REFERRAL':
4546              // Defined by RFC 2221
4547              $pipeline->data['referral'] = new Horde_Imap_Client_Url($rc->data[0]);
4548              break;
4549  
4550          case 'UNKNOWN-CTE':
4551              // Defined by RFC 3516
4552              throw new Horde_Imap_Client_Exception_ServerResponse(
4553                  Horde_Imap_Client_Translation::r("The mail server was unable to parse the contents of the mail message."),
4554                  Horde_Imap_Client_Exception::UNKNOWNCTE,
4555                  $ob,
4556                  $pipeline
4557              );
4558  
4559          case 'APPENDUID':
4560              // Defined by RFC 4315
4561              // APPENDUID: [0] = UIDVALIDITY, [1] = UID(s)
4562              $pipeline->data['appenduid'] = $this->getIdsOb($rc->data[1]);
4563              break;
4564  
4565          case 'COPYUID':
4566              // Defined by RFC 4315
4567              // COPYUID: [0] = UIDVALIDITY, [1] = UIDFROM, [2] = UIDTO
4568              $pipeline->data['copyuid'] = array_combine(
4569                  $this->getIdsOb($rc->data[1])->ids,
4570                  $this->getIdsOb($rc->data[2])->ids
4571              );
4572  
4573              /* Use UIDPLUS information to move cached data to new mailbox (see
4574               * RFC 4549 [4.2.2.1]). Need to move now, because a MOVE might
4575               * EXPUNGE immediately afterwards. */
4576              $this->_moveCache($pipeline->data['copydest'], $pipeline->data['copyuid'], $rc->data[0]);
4577              break;
4578  
4579          case 'UIDNOTSTICKY':
4580              // Defined by RFC 4315 [3]
4581              $this->_mailboxOb()->setStatus(Horde_Imap_Client::STATUS_UIDNOTSTICKY, true);
4582              break;
4583  
4584          case 'BADURL':
4585              // Defined by RFC 4469 [4.1]
4586              throw new Horde_Imap_Client_Exception_ServerResponse(
4587                  Horde_Imap_Client_Translation::r("Could not save message on server."),
4588                  Horde_Imap_Client_Exception::CATENATE_BADURL,
4589                  $ob,
4590                  $pipeline
4591              );
4592  
4593          case 'TOOBIG':
4594              // Defined by RFC 4469 [4.2]
4595              throw new Horde_Imap_Client_Exception_ServerResponse(
4596                  Horde_Imap_Client_Translation::r("Could not save message data because it is too large."),
4597                  Horde_Imap_Client_Exception::CATENATE_TOOBIG,
4598                  $ob,
4599                  $pipeline
4600              );
4601  
4602          case 'HIGHESTMODSEQ':
4603              // Defined by RFC 7162 [3.1.2.1]
4604              $pipeline->data['modseqs'][] = $rc->data[0];
4605              break;
4606  
4607          case 'NOMODSEQ':
4608              // Defined by RFC 7162 [3.1.2.2]
4609              $pipeline->data['modseqs'][] = 0;
4610              break;
4611  
4612          case 'MODIFIED':
4613              // Defined by RFC 7162 [3.1.3]
4614              $pipeline->data['modified']->add($rc->data[0]);
4615              break;
4616  
4617          case 'CLOSED':
4618              // Defined by RFC 7162 [3.2.11]
4619              if (isset($pipeline->data['qresyncmbox'])) {
4620                  /* If there is any pending FETCH cache entries, flush them
4621                   * now before changing mailboxes. */
4622                  $this->_updateCache($pipeline->fetch);
4623                  $pipeline->fetch->clear();
4624  
4625                  $this->_changeSelected(
4626                      $pipeline->data['qresyncmbox'][0],
4627                      $pipeline->data['qresyncmbox'][1]
4628                  );
4629                  unset($pipeline->data['qresyncmbox']);
4630              }
4631              break;
4632  
4633          case 'NOTSAVED':
4634              // Defined by RFC 5182 [2.5]
4635              $pipeline->data['searchnotsaved'] = true;
4636              break;
4637  
4638          case 'BADCOMPARATOR':
4639              // Defined by RFC 5255 [4.9]
4640              throw new Horde_Imap_Client_Exception_ServerResponse(
4641                  Horde_Imap_Client_Translation::r("The comparison algorithm was not recognized by the server."),
4642                  Horde_Imap_Client_Exception::BADCOMPARATOR,
4643                  $ob,
4644                  $pipeline
4645              );
4646  
4647          case 'METADATA':
4648              $md = $rc->data[0];
4649  
4650              switch ($md[0]) {
4651              case 'LONGENTRIES':
4652                  // Defined by RFC 5464 [4.2.1]
4653                  $pipeline->data['metadata']['*longentries'] = intval($md[1]);
4654                  break;
4655  
4656              case 'MAXSIZE':
4657                  // Defined by RFC 5464 [4.3]
4658                  throw new Horde_Imap_Client_Exception_ServerResponse(
4659                      Horde_Imap_Client_Translation::r("The metadata item could not be saved because it is too large."),
4660                      Horde_Imap_Client_Exception::METADATA_MAXSIZE,
4661                      $ob,
4662                      $pipeline
4663                  );
4664  
4665              case 'NOPRIVATE':
4666                  // Defined by RFC 5464 [4.3]
4667                  throw new Horde_Imap_Client_Exception_ServerResponse(
4668                      Horde_Imap_Client_Translation::r("The metadata item could not be saved because the server does not support private annotations."),
4669                      Horde_Imap_Client_Exception::METADATA_NOPRIVATE,
4670                      $ob,
4671                      $pipeline
4672                  );
4673  
4674              case 'TOOMANY':
4675                  // Defined by RFC 5464 [4.3]
4676                  throw new Horde_Imap_Client_Exception_ServerResponse(
4677                      Horde_Imap_Client_Translation::r("The metadata item could not be saved because the maximum number of annotations has been exceeded."),
4678                      Horde_Imap_Client_Exception::METADATA_TOOMANY,
4679                      $ob,
4680                      $pipeline
4681                  );
4682              }
4683              break;
4684  
4685          case 'UNAVAILABLE':
4686              // Defined by RFC 5530 [3]
4687              $pipeline->data['loginerr'] = new Horde_Imap_Client_Exception(
4688                  Horde_Imap_Client_Translation::r("Remote server is temporarily unavailable."),
4689                  Horde_Imap_Client_Exception::LOGIN_UNAVAILABLE
4690              );
4691              break;
4692  
4693          case 'AUTHENTICATIONFAILED':
4694              // Defined by RFC 5530 [3]
4695              $pipeline->data['loginerr'] = new Horde_Imap_Client_Exception(
4696                  Horde_Imap_Client_Translation::r("Authentication failed."),
4697                  Horde_Imap_Client_Exception::LOGIN_AUTHENTICATIONFAILED
4698              );
4699              break;
4700  
4701          case 'AUTHORIZATIONFAILED':
4702              // Defined by RFC 5530 [3]
4703              $pipeline->data['loginerr'] = new Horde_Imap_Client_Exception(
4704                  Horde_Imap_Client_Translation::r("Authentication was successful, but authorization failed."),
4705                  Horde_Imap_Client_Exception::LOGIN_AUTHORIZATIONFAILED
4706              );
4707              break;
4708  
4709          case 'EXPIRED':
4710              // Defined by RFC 5530 [3]
4711              $pipeline->data['loginerr'] = new Horde_Imap_Client_Exception(
4712                  Horde_Imap_Client_Translation::r("Authentication credentials have expired."),
4713                  Horde_Imap_Client_Exception::LOGIN_EXPIRED
4714              );
4715              break;
4716  
4717          case 'PRIVACYREQUIRED':
4718              // Defined by RFC 5530 [3]
4719              $pipeline->data['loginerr'] = new Horde_Imap_Client_Exception(
4720                  Horde_Imap_Client_Translation::r("Operation failed due to a lack of a secure connection."),
4721                  Horde_Imap_Client_Exception::LOGIN_PRIVACYREQUIRED
4722              );
4723              break;
4724  
4725          case 'NOPERM':
4726              // Defined by RFC 5530 [3]
4727              throw new Horde_Imap_Client_Exception_ServerResponse(
4728                  Horde_Imap_Client_Translation::r("You do not have adequate permissions to carry out this operation."),
4729                  Horde_Imap_Client_Exception::NOPERM,
4730                  $ob,
4731                  $pipeline
4732              );
4733  
4734          case 'INUSE':
4735              // Defined by RFC 5530 [3]
4736              throw new Horde_Imap_Client_Exception_ServerResponse(
4737                  Horde_Imap_Client_Translation::r("There was a temporary issue when attempting this operation. Please try again later."),
4738                  Horde_Imap_Client_Exception::INUSE,
4739                  $ob,
4740                  $pipeline
4741              );
4742  
4743          case 'EXPUNGEISSUED':
4744              // Defined by RFC 5530 [3]
4745              $pipeline->data['expungeissued'] = true;
4746              break;
4747  
4748          case 'CORRUPTION':
4749              // Defined by RFC 5530 [3]
4750              throw new Horde_Imap_Client_Exception_ServerResponse(
4751                  Horde_Imap_Client_Translation::r("The mail server is reporting corrupt data in your mailbox."),
4752                  Horde_Imap_Client_Exception::CORRUPTION,
4753                  $ob,
4754                  $pipeline
4755              );
4756  
4757          case 'SERVERBUG':
4758          case 'CLIENTBUG':
4759          case 'CANNOT':
4760              // Defined by RFC 5530 [3]
4761              $this->_debug->info(
4762                  'ERROR: mail server explicitly reporting an error.'
4763              );
4764              break;
4765  
4766          case 'LIMIT':
4767              // Defined by RFC 5530 [3]
4768              throw new Horde_Imap_Client_Exception_ServerResponse(
4769                  Horde_Imap_Client_Translation::r("The mail server has denied the request."),
4770                  Horde_Imap_Client_Exception::LIMIT,
4771                  $ob,
4772                  $pipeline
4773              );
4774  
4775          case 'OVERQUOTA':
4776              // Defined by RFC 5530 [3]
4777              throw new Horde_Imap_Client_Exception_ServerResponse(
4778                  Horde_Imap_Client_Translation::r("The operation failed because the quota has been exceeded on the mail server."),
4779                  Horde_Imap_Client_Exception::OVERQUOTA,
4780                  $ob,
4781                  $pipeline
4782              );
4783  
4784          case 'ALREADYEXISTS':
4785              // Defined by RFC 5530 [3]
4786              throw new Horde_Imap_Client_Exception_ServerResponse(
4787                  Horde_Imap_Client_Translation::r("The object could not be created because it already exists."),
4788                  Horde_Imap_Client_Exception::ALREADYEXISTS,
4789                  $ob,
4790                  $pipeline
4791              );
4792  
4793          case 'NONEXISTENT':
4794              // Defined by RFC 5530 [3]
4795              throw new Horde_Imap_Client_Exception_ServerResponse(
4796                  Horde_Imap_Client_Translation::r("The object could not be deleted because it does not exist."),
4797                  Horde_Imap_Client_Exception::NONEXISTENT,
4798                  $ob,
4799                  $pipeline
4800              );
4801  
4802          case 'USEATTR':
4803              // Defined by RFC 6154 [3]
4804              throw new Horde_Imap_Client_Exception_ServerResponse(
4805                  Horde_Imap_Client_Translation::r("The special-use attribute requested for the mailbox is not supported."),
4806                  Horde_Imap_Client_Exception::USEATTR,
4807                  $ob,
4808                  $pipeline
4809              );
4810  
4811          case 'DOWNGRADED':
4812              // Defined by RFC 6858 [3]
4813              $downgraded = $this->getIdsOb($rc->data[0]);
4814              foreach ($pipeline->fetch as $val) {
4815                  if (in_array($val->getUid(), $downgraded)) {
4816                      $val->setDowngraded(true);
4817                  }
4818              }
4819              break;
4820  
4821          case 'XPROXYREUSE':
4822              // The proxy connection was reused, so no need to do login tasks.
4823              $pipeline->data['proxyreuse'] = true;
4824              break;
4825  
4826          default:
4827              // Unknown response codes SHOULD be ignored - RFC 3501 [7.1]
4828              break;
4829          }
4830      }
4831  
4832  }


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