Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

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

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