Search moodle.org's
Developer Documentation

See Release Notes

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

Differences Between: [Versions 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 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 ($pattern as $val) {
1547                  $val_utf8 = Horde_Imap_Client_Utf7imap::Utf7ImapToUtf8($val);
1548                  if (isset($lr[$val_utf8])) {
1549                      $lr[$val_utf8]['status'] = $this->_prepareStatusResponse($status_opts, $val_utf8);
1550                  }
1551              }
1552          }
1553  
1554          return $lr;
1555      }
1556  
1557      /**
1558       * Parse a LIST/LSUB response (RFC 3501 [7.2.2 & 7.2.3]).
1559       *
1560       * @param Horde_Imap_Client_Interaction_Pipeline $pipeline  Pipeline
1561       *                                                          object.
1562       * @param Horde_Imap_Client_Tokenize $data  The server response (includes
1563       *                                          type as first token).
1564       *
1565       * @throws Horde_Imap_Client_Exception
1566       */
1567      protected function _parseList(
1568          Horde_Imap_Client_Interaction_Pipeline $pipeline,
1569          Horde_Imap_Client_Tokenize $data
1570      )
1571      {
1572          $data->next();
1573          $attr = null;
1574          $attr_raw = $data->flushIterator();
1575          $delimiter = $data->next();
1576          $mbox = Horde_Imap_Client_Mailbox::get(
1577              $data->next(),
1578              !$this->_capability()->isEnabled('UTF8=ACCEPT')
1579          );
1580          $ml = $pipeline->data['mailboxlist'];
1581  
1582          switch ($ml['mode']) {
1583          case Horde_Imap_Client::MBOX_ALL_SUBSCRIBED:
1584          case Horde_Imap_Client::MBOX_SUBSCRIBED_EXISTS:
1585          case Horde_Imap_Client::MBOX_UNSUBSCRIBED:
1586              $attr = array_flip(array_map('Horde_String::lower', $attr_raw));
1587  
1588              /* Subscribed list is in UTF-8. */
1589              if (is_null($ml['sub']) &&
1590                  !isset($attr['\\subscribed']) &&
1591                  (strcasecmp($mbox, 'INBOX') === 0)) {
1592                  $attr['\\subscribed'] = 1;
1593              } elseif (isset($ml['sub'][strval($mbox)])) {
1594                  $attr['\\subscribed'] = 1;
1595              }
1596              break;
1597          }
1598  
1599          switch ($ml['mode']) {
1600          case Horde_Imap_Client::MBOX_SUBSCRIBED_EXISTS:
1601              if (isset($attr['\\nonexistent']) ||
1602                  !isset($attr['\\subscribed'])) {
1603                  return;
1604              }
1605              break;
1606  
1607          case Horde_Imap_Client::MBOX_UNSUBSCRIBED:
1608              if (isset($attr['\\subscribed'])) {
1609                  return;
1610              }
1611              break;
1612          }
1613  
1614          if (!empty($ml['opts']['flat'])) {
1615              $pipeline->data['listresponse'][] = $mbox;
1616              return;
1617          }
1618  
1619          $tmp = array(
1620              'delimiter' => $delimiter,
1621              'mailbox' => $mbox
1622          );
1623  
1624          if ($attr || !empty($ml['opts']['attributes'])) {
1625              if (is_null($attr)) {
1626                  $attr = array_flip(array_map('Horde_String::lower', $attr_raw));
1627              }
1628  
1629              /* RFC 5258 [3.4]: inferred attributes. */
1630              if ($ml['ext']) {
1631                  if (isset($attr['\\noinferiors'])) {
1632                      $attr['\\hasnochildren'] = 1;
1633                  }
1634                  if (isset($attr['\\nonexistent'])) {
1635                      $attr['\\noselect'] = 1;
1636                  }
1637              }
1638              $tmp['attributes'] = array_keys($attr);
1639          }
1640  
1641          if ($data->next() !== false) {
1642              $tmp['extended'] = $data->flushIterator();
1643          }
1644  
1645          $pipeline->data['listresponse'][strval($mbox)] = $tmp;
1646      }
1647  
1648      /**
1649       */
1650      protected function _status($mboxes, $flags)
1651      {
1652          $on_error = null;
1653          $out = $to_process = array();
1654          $pipeline = $this->_pipeline();
1655          $unseen_flags = array(
1656              Horde_Imap_Client::STATUS_FIRSTUNSEEN,
1657              Horde_Imap_Client::STATUS_UNSEEN
1658          );
1659  
1660          foreach ($mboxes as $mailbox) {
1661              /* If FLAGS/PERMFLAGS/UIDNOTSTICKY/FIRSTUNSEEN are needed, we must
1662               * do a SELECT/EXAMINE to get this information (data will be
1663               * caught in the code below). */
1664              if (($flags & Horde_Imap_Client::STATUS_FIRSTUNSEEN) ||
1665                  ($flags & Horde_Imap_Client::STATUS_FLAGS) ||
1666                  ($flags & Horde_Imap_Client::STATUS_PERMFLAGS) ||
1667                  ($flags & Horde_Imap_Client::STATUS_UIDNOTSTICKY)) {
1668                  $this->openMailbox($mailbox);
1669              }
1670  
1671              $mbox_ob = $this->_mailboxOb($mailbox);
1672              $data = $query = array();
1673  
1674              foreach ($this->_statusFields as $key => $val) {
1675                  if (!($val & $flags)) {
1676                      continue;
1677                  }
1678  
1679                  if ($val == Horde_Imap_Client::STATUS_HIGHESTMODSEQ) {
1680                      $c = $this->_capability();
1681  
1682                      /* Don't include modseq returns if server does not support
1683                       * it. */
1684                      if (!$c->query('CONDSTORE')) {
1685                          continue;
1686                      }
1687  
1688                      /* Even though CONDSTORE is available, it may not yet have
1689                       * been enabled. */
1690                      $c->enable('CONDSTORE');
1691                      $on_error = function() use ($c) {
1692                          $c->enable('CONDSTORE', false);
1693                      };
1694                  }
1695  
1696                  if ($mailbox->equals($this->_selected)) {
1697                      if (!is_null($tmp = $mbox_ob->getStatus($val))) {
1698                          $data[$key] = $tmp;
1699                      } elseif (($val == Horde_Imap_Client::STATUS_UIDNEXT) &&
1700                                ($flags & Horde_Imap_Client::STATUS_UIDNEXT_FORCE)) {
1701                          /* UIDNEXT is not mandatory. */
1702                          if ($mbox_ob->getStatus(Horde_Imap_Client::STATUS_MESSAGES) == 0) {
1703                              $data[$key] = 0;
1704                          } else {
1705                              $fquery = new Horde_Imap_Client_Fetch_Query();
1706                              $fquery->uid();
1707                              $fetch_res = $this->fetch($this->_selected, $fquery, array(
1708                                  'ids' => $this->getIdsOb(Horde_Imap_Client_Ids::LARGEST)
1709                              ));
1710                              $data[$key] = $fetch_res->first()->getUid() + 1;
1711                          }
1712                      } elseif (in_array($val, $unseen_flags)) {
1713                          /* RFC 3501 [6.3.1] - FIRSTUNSEEN information is not
1714                           * mandatory. If missing in EXAMINE/SELECT results, we
1715                           * need to do a search. An UNSEEN count also requires
1716                           * a search. */
1717                          $squery = new Horde_Imap_Client_Search_Query();
1718                          $squery->flag(Horde_Imap_Client::FLAG_SEEN, false);
1719                          $search = $this->search($mailbox, $squery, array(
1720                              'results' => array(
1721                                  Horde_Imap_Client::SEARCH_RESULTS_MIN,
1722                                  Horde_Imap_Client::SEARCH_RESULTS_COUNT
1723                              ),
1724                              'sequence' => true
1725                          ));
1726  
1727                          $mbox_ob->setStatus(Horde_Imap_Client::STATUS_FIRSTUNSEEN, $search['min']);
1728                          $mbox_ob->setStatus(Horde_Imap_Client::STATUS_UNSEEN, $search['count']);
1729  
1730                          $data[$key] = $mbox_ob->getStatus($val);
1731                      }
1732                  } else {
1733                      $query[] = $key;
1734                  }
1735              }
1736  
1737              $out[strval($mailbox)] = $data;
1738  
1739              if (count($query)) {
1740                  $cmd = $this->_command('STATUS')->add(array(
1741                      $this->_getMboxFormatOb($mailbox),
1742                      new Horde_Imap_Client_Data_Format_List(
1743                          array_map('Horde_String::upper', $query)
1744                      )
1745                  ));
1746                  $cmd->on_error = $on_error;
1747  
1748                  $pipeline->add($cmd);
1749                  $to_process[] = array($query, $mailbox);
1750              }
1751          }
1752  
1753          if (count($pipeline)) {
1754              $this->_sendCmd($pipeline);
1755  
1756              foreach ($to_process as $val) {
1757                  $out[strval($val[1])] += $this->_prepareStatusResponse($val[0], $val[1]);
1758              }
1759          }
1760  
1761          return $out;
1762      }
1763  
1764      /**
1765       * Parse a STATUS response (RFC 3501 [7.2.4]).
1766       *
1767       * @param Horde_Imap_Client_Tokenize $data  Token data
1768       */
1769      protected function _parseStatus(Horde_Imap_Client_Tokenize $data)
1770      {
1771          // Mailbox name is in UTF7-IMAP (unless UTF8 has been enabled).
1772          $mbox_ob = $this->_mailboxOb(
1773              Horde_Imap_Client_Mailbox::get(
1774                  $data->next(),
1775                  !$this->_capability()->isEnabled('UTF8=ACCEPT')
1776              )
1777          );
1778  
1779          $data->next();
1780  
1781          while (($k = $data->next()) !== false) {
1782              $mbox_ob->setStatus(
1783                  $this->_statusFields[Horde_String::lower($k)],
1784                  $data->next()
1785              );
1786          }
1787      }
1788  
1789      /**
1790       * Prepares a status response for a mailbox.
1791       *
1792       * @param array $request   The status keys to return.
1793       * @param string $mailbox  The mailbox to query.
1794       */
1795      protected function _prepareStatusResponse($request, $mailbox)
1796      {
1797          $mbox_ob = $this->_mailboxOb($mailbox);
1798          $out = array();
1799  
1800          foreach ($request as $val) {
1801              $out[$val] = $mbox_ob->getStatus($this->_statusFields[$val]);
1802          }
1803  
1804          return $out;
1805      }
1806  
1807      /**
1808       */
1809      protected function _append(Horde_Imap_Client_Mailbox $mailbox, $data,
1810                                 $options)
1811      {
1812          $c = $this->_capability();
1813  
1814          // Check for MULTIAPPEND extension (RFC 3502)
1815          if ((count($data) > 1) && !$c->query('MULTIAPPEND')) {
1816              $result = $this->getIdsOb();
1817              foreach (array_keys($data) as $key) {
1818                  $res = $this->_append($mailbox, array($data[$key]), $options);
1819                  if (($res === true) || ($result === true)) {
1820                      $result = true;
1821                  } else {
1822                      $result->add($res);
1823                  }
1824              }
1825              return $result;
1826          }
1827  
1828          // Check for extensions.
1829          $binary = $c->query('BINARY');
1830          $catenate = $c->query('CATENATE');
1831          $utf8 = $c->isEnabled('UTF8=ACCEPT');
1832  
1833          $asize = 0;
1834  
1835          $cmd = $this->_command('APPEND')->add(
1836              $this->_getMboxFormatOb($mailbox)
1837          );
1838          $cmd->literal8 = true;
1839  
1840          foreach (array_keys($data) as $key) {
1841              if (!empty($data[$key]['flags'])) {
1842                  $tmp = new Horde_Imap_Client_Data_Format_List();
1843                  foreach ($data[$key]['flags'] as $val) {
1844                      /* Ignore recent flag. RFC 3501 [9]: flag definition */
1845                      if (strcasecmp($val, Horde_Imap_Client::FLAG_RECENT) !== 0) {
1846                          $tmp->add($val);
1847                      }
1848                  }
1849                  $cmd->add($tmp);
1850              }
1851  
1852              if (!empty($data[$key]['internaldate'])) {
1853                  $cmd->add(new Horde_Imap_Client_Data_Format_DateTime($data[$key]['internaldate']));
1854              }
1855  
1856              $adata = null;
1857  
1858              if (is_array($data[$key]['data'])) {
1859                  if ($catenate) {
1860                      $cmd->add('CATENATE');
1861                      $tmp = new Horde_Imap_Client_Data_Format_List();
1862                  } else {
1863                      $data_stream = new Horde_Stream_Temp();
1864                  }
1865  
1866                  foreach ($data[$key]['data'] as $v) {
1867                      switch ($v['t']) {
1868                      case 'text':
1869                          if ($catenate) {
1870                              $tdata = $this->_appendData($v['v'], $asize);
1871                              if ($utf8) {
1872                                  /* RFC 6855 [4]: CATENATE UTF8 extension. */
1873                                  $tdata->forceBinary();
1874                                  $tmp->add(array(
1875                                      'UTF8',
1876                                      new Horde_Imap_Client_Data_Format_List($tdata)
1877                                  ));
1878                              } else {
1879                                  $tmp->add(array(
1880                                      'TEXT',
1881                                      $tdata
1882                                  ));
1883                              }
1884                          } else {
1885                              if (is_resource($v['v'])) {
1886                                  rewind($v['v']);
1887                              }
1888                              $data_stream->add($v['v']);
1889                          }
1890                          break;
1891  
1892                      case 'url':
1893                          if ($catenate) {
1894                              $tmp->add(array(
1895                                  'URL',
1896                                  new Horde_Imap_Client_Data_Format_Astring($v['v'])
1897                              ));
1898                          } else {
1899                              $data_stream->add($this->_convertCatenateUrl($v['v']));
1900                          }
1901                          break;
1902                      }
1903                  }
1904  
1905                  if ($catenate) {
1906                      $cmd->add($tmp);
1907                  } else {
1908                      $adata = $this->_appendData($data_stream->stream, $asize);
1909                  }
1910              } else {
1911                  $adata = $this->_appendData($data[$key]['data'], $asize);
1912              }
1913  
1914              if (!is_null($adata)) {
1915                  if ($utf8) {
1916                      /* RFC 6855 [4]: APPEND UTF8 extension. */
1917                      $adata->forceBinary();
1918                      $cmd->add(array(
1919                          'UTF8',
1920                          new Horde_Imap_Client_Data_Format_List($adata)
1921                      ));
1922                  } else {
1923                      $cmd->add($adata);
1924                  }
1925              }
1926          }
1927  
1928          /* Although it is normally more efficient to use LITERAL+, disable if
1929           * payload is over 50 KB because it allows the server to throw error
1930           * before we potentially push a lot of data to server that would
1931           * otherwise be ignored (see RFC 4549 [4.2.2.3]).
1932           * Additionally, since so many IMAP servers have issues with APPEND
1933           * + BINARY, don't use LITERAL+ since servers may send BAD
1934           * (incorrectly) after initial command. */
1935          $cmd->literalplus = (($asize < (1024 * 50)) && !$binary);
1936  
1937          // If the mailbox is currently selected read-only, we need to close
1938          // because some IMAP implementations won't allow an append. And some
1939          // implementations don't support append on ANY open mailbox. Be safe
1940          // and always make sure we are in a non-selected state.
1941          $this->close();
1942  
1943          try {
1944              $resp = $this->_sendCmd($cmd);
1945          } catch (Horde_Imap_Client_Exception $e) {
1946              switch ($e->getCode()) {
1947              case $e::CATENATE_BADURL:
1948              case $e::CATENATE_TOOBIG:
1949                  /* Cyrus 2.4 (at least as of .14) has a broken CATENATE (see
1950                   * Bug #11111). Regardless, if CATENATE is broken, we can try
1951                   * to fallback to APPEND. */
1952                  $c->remove('CATENATE');
1953                  return $this->_append($mailbox, $data, $options);
1954  
1955              case $e::DISCONNECT:
1956                  /* Workaround broken literal8 on Cyrus. */
1957                  if ($binary) {
1958                      // Need to re-login first before removing capability.
1959                      $this->login();
1960                      $c->remove('BINARY');
1961                      return $this->_append($mailbox, $data, $options);
1962                  }
1963                  break;
1964              }
1965  
1966              if (!empty($options['create']) &&
1967                  !empty($e->resp_data['trycreate'])) {
1968                  $this->createMailbox($mailbox);
1969                  unset($options['create']);
1970                  return $this->_append($mailbox, $data, $options);
1971              }
1972  
1973              /* RFC 3516/4466 says we should be able to append binary data
1974               * using literal8 "~{#} format", but it doesn't seem to work on
1975               * all servers tried (UW-IMAP/Cyrus). Do a last-ditch check for
1976               * broken BINARY and attempt to fix here. */
1977              if ($c->query('BINARY') &&
1978                  ($e instanceof Horde_Imap_Client_Exception_ServerResponse)) {
1979                  switch ($e->status) {
1980                  case Horde_Imap_Client_Interaction_Server::BAD:
1981                  case Horde_Imap_Client_Interaction_Server::NO:
1982                      $c->remove('BINARY');
1983                      return $this->_append($mailbox, $data, $options);
1984                  }
1985              }
1986  
1987              throw $e;
1988          }
1989  
1990          /* If we reach this point and have data in 'appenduid', UIDPLUS (RFC
1991           * 4315) has done the dirty work for us. */
1992          return isset($resp->data['appenduid'])
1993              ? $resp->data['appenduid']
1994              : true;
1995      }
1996  
1997      /**
1998       * Prepares append message data for insertion into the IMAP command
1999       * string.
2000       *
2001       * @param mixed $data      Either a resource or a string.
2002       * @param integer &$asize  Total append size.
2003       *
2004       * @return Horde_Imap_Client_Data_Format_String_Nonascii  The data object.
2005       */
2006      protected function _appendData($data, &$asize)
2007      {
2008          if (is_resource($data)) {
2009              rewind($data);
2010          }
2011  
2012          /* Since this is body text, with possible embedded charset
2013           * information, non-ASCII characters are supported. */
2014          $ob = new Horde_Imap_Client_Data_Format_String_Nonascii($data, array(
2015              'eol' => true,
2016              'skipscan' => true
2017          ));
2018  
2019          // APPEND data MUST be sent in a literal (RFC 3501 [6.3.11]).
2020          $ob->forceLiteral();
2021  
2022          $asize += $ob->length();
2023  
2024          return $ob;
2025      }
2026  
2027      /**
2028       * Converts a CATENATE URL to stream data.
2029       *
2030       * @param string $url  The CATENATE URL.
2031       *
2032       * @return resource  A stream containing the data.
2033       */
2034      protected function _convertCatenateUrl($url)
2035      {
2036          $e = $part = null;
2037          $url = new Horde_Imap_Client_Url_Imap($url);
2038  
2039          if (!is_null($url->mailbox) && !is_null($url->uid)) {
2040              try {
2041                  $status_res = is_null($url->uidvalidity)
2042                      ? null
2043                      : $this->status($url->mailbox, Horde_Imap_Client::STATUS_UIDVALIDITY);
2044  
2045                  if (is_null($status_res) ||
2046                      ($status_res['uidvalidity'] == $url->uidvalidity)) {
2047                      if (!isset($this->_temp['catenate_ob'])) {
2048                          $this->_temp['catenate_ob'] = new Horde_Imap_Client_Socket_Catenate($this);
2049                      }
2050                      $part = $this->_temp['catenate_ob']->fetchFromUrl($url);
2051                  }
2052              } catch (Horde_Imap_Client_Exception $e) {}
2053          }
2054  
2055          if (is_null($part)) {
2056              $message = 'Bad IMAP URL given in CATENATE data: ' . strval($url);
2057              if ($e) {
2058                  $message .= ' ' . $e->getMessage();
2059              }
2060  
2061              throw new InvalidArgumentException($message);
2062          }
2063  
2064          return $part;
2065      }
2066  
2067      /**
2068       */
2069      protected function _check()
2070      {
2071          // CHECK returns no untagged information (RFC 3501 [6.4.1])
2072          $this->_sendCmd($this->_command('CHECK'));
2073      }
2074  
2075      /**
2076       */
2077      protected function _close($options)
2078      {
2079          if (empty($options['expunge'])) {
2080              if ($this->_capability('UNSELECT')) {
2081                  // RFC 3691 defines 'UNSELECT' for precisely this purpose
2082                  $this->_sendCmd($this->_command('UNSELECT'));
2083              } else {
2084                  /* RFC 3501 [6.4.2]: to close a mailbox without expunge,
2085                   * select a non-existent mailbox. */
2086                  try {
2087                      $this->_sendCmd($this->_command('EXAMINE')->add(
2088                          $this->_getMboxFormatOb("\24nonexist\24")
2089                      ));
2090  
2091                      /* Not pipelining, since the odds that this CLOSE is even
2092                       * needed is tiny; and it returns BAD, which should be
2093                       * avoided, if possible. */
2094                      $this->_sendCmd($this->_command('CLOSE'));
2095                  } catch (Horde_Imap_Client_Exception_ServerResponse $e) {
2096                      // Ignore error; it is expected.
2097                  }
2098              }
2099          } else {
2100              // If caching, we need to know the UIDs being deleted, so call
2101              // expunge() before calling close().
2102              if ($this->_initCache(true)) {
2103                  $this->expunge($this->_selected);
2104              }
2105  
2106              // CLOSE returns no untagged information (RFC 3501 [6.4.2])
2107              $this->_sendCmd($this->_command('CLOSE'));
2108          }
2109      }
2110  
2111      /**
2112       */
2113      protected function _expunge($options)
2114      {
2115          $expunged_ob = $modseq = null;
2116          $ids = $options['ids'];
2117          $list_msgs = !empty($options['list']);
2118          $mailbox = $this->_selected;
2119          $uidplus = $this->_capability('UIDPLUS');
2120          $unflag = array();
2121          $use_cache = $this->_initCache(true);
2122  
2123          if ($ids->all) {
2124              if (!$uidplus || $list_msgs || $use_cache) {
2125                  $ids = $this->resolveIds($mailbox, $ids, 2);
2126              }
2127          } elseif ($uidplus) {
2128              /* If QRESYNC is not available, and we are returning the list of
2129               * expunged messages (or we are caching), we have to make sure we
2130               * have a mapping of Sequence -> UIDs. If we have QRESYNC, the
2131               * server SHOULD return a VANISHED response with UIDs. However,
2132               * even if the server returns EXPUNGEs instead, we can use
2133               * vanished() to grab the list. */
2134              unset($this->_temp['search_save']);
2135              if ($this->_capability()->isEnabled('QRESYNC')) {
2136                  $ids = $this->resolveIds($mailbox, $ids, 1);
2137                  if ($list_msgs) {
2138                      $modseq = $this->_mailboxOb()->getStatus(Horde_Imap_Client::STATUS_HIGHESTMODSEQ);
2139                  }
2140              } else {
2141                  $ids = $this->resolveIds($mailbox, $ids, ($list_msgs || $use_cache) ? 2 : 1);
2142              }
2143              if (!empty($this->_temp['search_save'])) {
2144                  $ids = $this->getIdsOb(Horde_Imap_Client_Ids::SEARCH_RES);
2145              }
2146          } else {
2147              /* Without UIDPLUS, need to temporarily unflag all messages marked
2148               * as deleted but not a part of requested IDs to delete. Use NOT
2149               * searches to accomplish this goal. */
2150              $squery = new Horde_Imap_Client_Search_Query();
2151              $squery->flag(Horde_Imap_Client::FLAG_DELETED, true);
2152              $squery->ids($ids, true);
2153  
2154              $s_res = $this->search($mailbox, $squery, array(
2155                  'results' => array(
2156                      Horde_Imap_Client::SEARCH_RESULTS_MATCH,
2157                      Horde_Imap_Client::SEARCH_RESULTS_SAVE
2158                  )
2159              ));
2160  
2161              $this->store($mailbox, array(
2162                  'ids' => empty($s_res['save']) ? $s_res['match'] : $this->getIdsOb(Horde_Imap_Client_Ids::SEARCH_RES),
2163                  'remove' => array(Horde_Imap_Client::FLAG_DELETED)
2164              ));
2165  
2166              $unflag = $s_res['match'];
2167          }
2168  
2169          if ($list_msgs) {
2170              $expunged_ob = $this->getIdsOb();
2171              $this->_temp['expunged'] = $expunged_ob;
2172          }
2173  
2174          /* Always use UID EXPUNGE if available. */
2175          if ($uidplus) {
2176              /* We can only pipeline STORE w/ EXPUNGE if using UIDs and UIDPLUS
2177               * is available. */
2178              if (empty($options['delete'])) {
2179                  $pipeline = $this->_pipeline();
2180              } else {
2181                  $pipeline = $this->_storeCmd(array(
2182                      'add' => array(
2183                          Horde_Imap_Client::FLAG_DELETED
2184                      ),
2185                      'ids' => $ids
2186                  ));
2187              }
2188  
2189              foreach ($ids->split(2000) as $val) {
2190                  $pipeline->add(
2191                      $this->_command('UID EXPUNGE')->add($val)
2192                  );
2193              }
2194  
2195              $resp = $this->_sendCmd($pipeline);
2196          } else {
2197              if (!empty($options['delete'])) {
2198                  $this->store($mailbox, array(
2199                      'add' => array(Horde_Imap_Client::FLAG_DELETED),
2200                      'ids' => $ids
2201                  ));
2202              }
2203  
2204              if ($use_cache || $list_msgs) {
2205                  $this->_sendCmd($this->_command('EXPUNGE'));
2206              } else {
2207                  /* This is faster than an EXPUNGE because the server will not
2208                   * return untagged EXPUNGE responses. We can only do this if
2209                   * we are not updating cache information. */
2210                  $this->close(array('expunge' => true));
2211              }
2212          }
2213  
2214          unset($this->_temp['expunged']);
2215  
2216          if (!empty($unflag)) {
2217              $this->store($mailbox, array(
2218                  'add' => array(Horde_Imap_Client::FLAG_DELETED),
2219                  'ids' => $unflag
2220              ));
2221          }
2222  
2223          if (!is_null($modseq) && !empty($resp->data['expunge_seen'])) {
2224              /* There's a chance we actually did a full map of sequence -> UID,
2225               * but this code should never be reached in the first place so
2226               * be ultra-safe and just do a full VANISHED search. */
2227              $expunged_ob = $this->vanished($mailbox, $modseq, array(
2228                  'ids' => $ids
2229              ));
2230              $this->_deleteMsgs($mailbox, $expunged_ob, array(
2231                  'pipeline' => $resp
2232              ));
2233          }
2234  
2235          return $expunged_ob;
2236      }
2237  
2238      /**
2239       * Parse a VANISHED response (RFC 7162 [3.2.10]).
2240       *
2241       * @param Horde_Imap_Client_Interaction_Pipeline $pipeline  Pipeline
2242       *                                                          object.
2243       * @param Horde_Imap_Client_Tokenize $data  The response data.
2244       */
2245      protected function _parseVanished(
2246          Horde_Imap_Client_Interaction_Pipeline $pipeline,
2247          Horde_Imap_Client_Tokenize $data
2248      )
2249      {
2250          /* There are two forms of VANISHED.  VANISHED (EARLIER) will be sent
2251           * in a FETCH (VANISHED) or SELECT/EXAMINE (QRESYNC) call.
2252           * If this is the case, we can go ahead and update the cache
2253           * immediately (we know we are caching or else QRESYNC would not be
2254           * enabled). HIGHESTMODSEQ information will be updated via the tagged
2255           * response. */
2256          if (($curr = $data->next()) === true) {
2257              if (Horde_String::upper($data->next()) === 'EARLIER') {
2258                  /* Caching is guaranteed to be active if we are using
2259                   * QRESYNC. */
2260                  $data->next();
2261                  $vanished = $this->getIdsOb($data->next());
2262                  if (isset($pipeline->data['vanished'])) {
2263                      $pipeline->data['vanished']->add($vanished);
2264                  } else {
2265                      $this->_deleteMsgs($this->_selected, $vanished, array(
2266                          'pipeline' => $pipeline
2267                      ));
2268                  }
2269              }
2270          } else {
2271              /* The second form is just VANISHED. This is analogous to EXPUNGE
2272               * and requires the message count to decrement. */
2273              $this->_deleteMsgs($this->_selected, $this->getIdsOb($curr), array(
2274                  'decrement' => true,
2275                  'pipeline' => $pipeline
2276              ));
2277          }
2278      }
2279  
2280      /**
2281       * Search a mailbox.  This driver supports all IMAP4rev1 search criteria
2282       * as defined in RFC 3501.
2283       */
2284      protected function _search($query, $options)
2285      {
2286          $sort_criteria = array(
2287              Horde_Imap_Client::SORT_ARRIVAL => 'ARRIVAL',
2288              Horde_Imap_Client::SORT_CC => 'CC',
2289              Horde_Imap_Client::SORT_DATE => 'DATE',
2290              Horde_Imap_Client::SORT_DISPLAYFROM => 'DISPLAYFROM',
2291              Horde_Imap_Client::SORT_DISPLAYTO => 'DISPLAYTO',
2292              Horde_Imap_Client::SORT_FROM => 'FROM',
2293              Horde_Imap_Client::SORT_REVERSE => 'REVERSE',
2294              Horde_Imap_Client::SORT_RELEVANCY => 'RELEVANCY',
2295              // This is a bogus entry to allow the sort options check to
2296              // correctly work below.
2297              Horde_Imap_Client::SORT_SEQUENCE => 'SEQUENCE',
2298              Horde_Imap_Client::SORT_SIZE => 'SIZE',
2299              Horde_Imap_Client::SORT_SUBJECT => 'SUBJECT',
2300              Horde_Imap_Client::SORT_TO => 'TO'
2301          );
2302  
2303          $results_criteria = array(
2304              Horde_Imap_Client::SEARCH_RESULTS_COUNT => 'COUNT',
2305              Horde_Imap_Client::SEARCH_RESULTS_MATCH => 'ALL',
2306              Horde_Imap_Client::SEARCH_RESULTS_MAX => 'MAX',
2307              Horde_Imap_Client::SEARCH_RESULTS_MIN => 'MIN',
2308              Horde_Imap_Client::SEARCH_RESULTS_RELEVANCY => 'RELEVANCY',
2309              Horde_Imap_Client::SEARCH_RESULTS_SAVE => 'SAVE'
2310          );
2311  
2312          // Check if the server supports sorting (RFC 5256).
2313          $esearch = $return_sort = $server_seq_sort = $server_sort = false;
2314          if (!empty($options['sort'])) {
2315              /* Make sure sort options are correct. If not, default to no
2316               * sort. */
2317              if (count(array_intersect($options['sort'], array_keys($sort_criteria))) === 0) {
2318                  unset($options['sort']);
2319              } else {
2320                  $return_sort = true;
2321  
2322                  if ($this->_capability('SORT')) {
2323                      /* Make sure server supports DISPLAYFROM & DISPLAYTO. */
2324                      $server_sort =
2325                          !array_intersect($options['sort'], array(Horde_Imap_Client::SORT_DISPLAYFROM, Horde_Imap_Client::SORT_DISPLAYTO)) ||
2326                          $this->_capability('SORT', 'DISPLAY');
2327                  }
2328  
2329                  /* If doing a sequence sort, need to do this on the client
2330                   * side. */
2331                  if ($server_sort &&
2332                      in_array(Horde_Imap_Client::SORT_SEQUENCE, $options['sort'])) {
2333                      $server_sort = false;
2334  
2335                      /* Optimization: If doing only a sequence sort, just do a
2336                       * simple search and sort UIDs/sequences on client side. */
2337                      switch (count($options['sort'])) {
2338                      case 1:
2339                          $server_seq_sort = true;
2340                          break;
2341  
2342                      case 2:
2343                          $server_seq_sort = (reset($options['sort']) == Horde_Imap_Client::SORT_REVERSE);
2344                          break;
2345                      }
2346                  }
2347              }
2348          }
2349  
2350          $charset = is_null($options['_query']['charset'])
2351              ? 'US-ASCII'
2352              : $options['_query']['charset'];
2353          $partial = false;
2354  
2355          if ($server_sort) {
2356              $cmd = $this->_command(
2357                  empty($options['sequence']) ? 'UID SORT' : 'SORT'
2358              );
2359              $results = array();
2360  
2361              // Use ESEARCH (RFC 4466) response if server supports.
2362              $esearch = false;
2363  
2364              // Check for ESORT capability (RFC 5267)
2365              if ($this->_capability('ESORT')) {
2366                  foreach ($options['results'] as $val) {
2367                      if (isset($results_criteria[$val]) &&
2368                          ($val != Horde_Imap_Client::SEARCH_RESULTS_SAVE)) {
2369                          $results[] = $results_criteria[$val];
2370                      }
2371                  }
2372                  $esearch = true;
2373              }
2374  
2375              // Add PARTIAL limiting (RFC 5267 [4.4])
2376              if ((!$esearch || !empty($options['partial'])) &&
2377                  $this->_capability('CONTEXT', 'SORT')) {
2378                  /* RFC 5267 indicates RFC 4466 ESEARCH-like support,
2379                   * notwithstanding "real" RFC 4731 support. */
2380                  $esearch = true;
2381  
2382                  if (!empty($options['partial'])) {
2383                      /* Can't have both ALL and PARTIAL returns. */
2384                      $results = array_diff($results, array('ALL'));
2385  
2386                      $results[] = 'PARTIAL';
2387                      $results[] = $options['partial'];
2388                      $partial = true;
2389                  }
2390              }
2391  
2392              if ($esearch && empty($this->_init['noesearch'])) {
2393                  $cmd->add(array(
2394                      'RETURN',
2395                      new Horde_Imap_Client_Data_Format_List($results)
2396                  ));
2397              }
2398  
2399              $tmp = new Horde_Imap_Client_Data_Format_List();
2400              foreach ($options['sort'] as $val) {
2401                  if (isset($sort_criteria[$val])) {
2402                      $tmp->add($sort_criteria[$val]);
2403                  }
2404              }
2405              $cmd->add($tmp);
2406  
2407              /* Charset is mandatory for SORT (RFC 5256 [3]).
2408               * If UTF-8 support is activated, a client MUST ONLY
2409               * send the 'UTF-8' specification (RFC 6855 [3]; Errata 4029). */
2410              if (!$this->_capability()->isEnabled('UTF8=ACCEPT')) {
2411                  $cmd->add($charset);
2412              } else {
2413                  $cmd->add('UTF-8');
2414              }
2415          } else {
2416              $cmd = $this->_command(
2417                  empty($options['sequence']) ? 'UID SEARCH' : 'SEARCH'
2418              );
2419              $esearch = false;
2420              $results = array();
2421  
2422              // Check if the server supports ESEARCH (RFC 4731).
2423              if ($this->_capability('ESEARCH')) {
2424                  foreach ($options['results'] as $val) {
2425                      if (isset($results_criteria[$val])) {
2426                          $results[] = $results_criteria[$val];
2427                      }
2428                  }
2429                  $esearch = true;
2430              }
2431  
2432              // Add PARTIAL limiting (RFC 5267 [4.4]).
2433              if ((!$esearch || !empty($options['partial'])) &&
2434                  $this->_capability('CONTEXT', 'SEARCH')) {
2435                  /* RFC 5267 indicates RFC 4466 ESEARCH-like support,
2436                   * notwithstanding "real" RFC 4731 support. */
2437                  $esearch = true;
2438  
2439                  if (!empty($options['partial'])) {
2440                      // Can't have both ALL and PARTIAL returns.
2441                      $results = array_diff($results, array('ALL'));
2442  
2443                      $results[] = 'PARTIAL';
2444                      $results[] = $options['partial'];
2445                      $partial = true;
2446                  }
2447              }
2448  
2449              if ($esearch && empty($this->_init['noesearch'])) {
2450                  // Always use ESEARCH if available because it returns results
2451                  // in a more compact sequence-set list
2452                  $cmd->add(array(
2453                      'RETURN',
2454                      new Horde_Imap_Client_Data_Format_List($results)
2455                  ));
2456              }
2457  
2458              /* Charset is optional for SEARCH (RFC 3501 [6.4.4]).
2459               * If UTF-8 support is activated, a client MUST NOT
2460               * send the charset specification (RFC 6855 [3]; Errata 4029). */
2461              if (($charset != 'US-ASCII') &&
2462                  !$this->_capability()->isEnabled('UTF8=ACCEPT')) {
2463                  $cmd->add(array(
2464                      'CHARSET',
2465                      $options['_query']['charset']
2466                  ));
2467              }
2468          }
2469  
2470          $cmd->add($options['_query']['query'], true);
2471  
2472          $pipeline = $this->_pipeline($cmd);
2473          $pipeline->data['esearchresp'] = array();
2474          $er = &$pipeline->data['esearchresp'];
2475          $pipeline->data['searchresp'] = $this->getIdsOb(array(), !empty($options['sequence']));
2476          $sr = &$pipeline->data['searchresp'];
2477  
2478          try {
2479              $resp = $this->_sendCmd($pipeline);
2480          } catch (Horde_Imap_Client_Exception $e) {
2481              if (($e instanceof Horde_Imap_Client_Exception_ServerResponse) &&
2482                  ($e->status === Horde_Imap_Client_Interaction_Server::NO) &&
2483                  ($charset != 'US-ASCII')) {
2484                  /* RFC 3501 [6.4.4]: BADCHARSET response code is only a
2485                   * SHOULD return. If it doesn't exist, need to check for
2486                   * command status of 'NO'. List of supported charsets in
2487                   * the BADCHARSET response has already been parsed and stored
2488                   * at this point. */
2489                  $this->search_charset->setValid($charset, false);
2490                  $e->setCode(Horde_Imap_Client_Exception::BADCHARSET);
2491              }
2492  
2493              if (empty($this->_temp['search_retry'])) {
2494                  $this->_temp['search_retry'] = true;
2495  
2496                  /* Bug #9842: Workaround broken Cyrus servers (as of
2497                   * 2.4.7). */
2498                  if ($esearch && ($charset != 'US-ASCII')) {
2499                      $this->_capability()->remove('ESEARCH');
2500                      $this->_setInit('noesearch', true);
2501  
2502                      try {
2503                          return $this->_search($query, $options);
2504                      } catch (Horde_Imap_Client_Exception $e) {}
2505                  }
2506  
2507                  /* Try to convert charset. */
2508                  if (($e->getCode() === Horde_Imap_Client_Exception::BADCHARSET) &&
2509                      ($charset != 'US-ASCII')) {
2510                      foreach ($this->search_charset->charsets as $val) {
2511                          $this->_temp['search_retry'] = 1;
2512                          $new_query = clone($query);
2513                          try {
2514                              $new_query->charset($val);
2515                              $options['_query'] = $new_query->build($this);
2516                              return $this->_search($new_query, $options);
2517                          } catch (Horde_Imap_Client_Exception $e) {}
2518                      }
2519                  }
2520  
2521                  unset($this->_temp['search_retry']);
2522              }
2523  
2524              throw $e;
2525          }
2526  
2527          if ($return_sort && !$server_sort) {
2528              if ($server_seq_sort) {
2529                  $sr->sort();
2530                  if (reset($options['sort']) == Horde_Imap_Client::SORT_REVERSE) {
2531                      $sr->reverse();
2532                  }
2533              } else {
2534                  if (!isset($this->_temp['clientsort'])) {
2535                      $this->_temp['clientsort'] = new Horde_Imap_Client_Socket_ClientSort($this);
2536                  }
2537                  $sr = $this->getIdsOb($this->_temp['clientsort']->clientSort($sr, $options), !empty($options['sequence']));
2538              }
2539          }
2540  
2541          if (!$partial && !empty($options['partial'])) {
2542              $partial = $this->getIdsOb($options['partial'], true);
2543              $min = $partial->min - 1;
2544  
2545              $sr = $this->getIdsOb(
2546                  array_slice($sr->ids, $min, $partial->max - $min),
2547                  !empty($options['sequence'])
2548              );
2549          }
2550  
2551          $ret = array();
2552          foreach ($options['results'] as $val) {
2553              switch ($val) {
2554              case Horde_Imap_Client::SEARCH_RESULTS_COUNT:
2555                  $ret['count'] = ($esearch && !$partial)
2556                      ? $er['count']
2557                      : count($sr);
2558                  break;
2559  
2560              case Horde_Imap_Client::SEARCH_RESULTS_MATCH:
2561                  $ret['match'] = $sr;
2562                  break;
2563  
2564              case Horde_Imap_Client::SEARCH_RESULTS_MAX:
2565                  $ret['max'] = $esearch
2566                      ? (!$partial && isset($er['max']) ? $er['max'] : null)
2567                      : (count($sr) ? max($sr->ids) : null);
2568                  break;
2569  
2570              case Horde_Imap_Client::SEARCH_RESULTS_MIN:
2571                  $ret['min'] = $esearch
2572                      ? (!$partial && isset($er['min']) ? $er['min'] : null)
2573                      : (count($sr) ? min($sr->ids) : null);
2574                  break;
2575  
2576              case Horde_Imap_Client::SEARCH_RESULTS_RELEVANCY:
2577                  $ret['relevancy'] = ($esearch && isset($er['relevancy'])) ? $er['relevancy'] : array();
2578                  break;
2579  
2580              case Horde_Imap_Client::SEARCH_RESULTS_SAVE:
2581                  $this->_temp['search_save'] = $ret['save'] = $esearch ? empty($resp->data['searchnotsaved']) : false;
2582                  break;
2583              }
2584          }
2585  
2586          // Add modseq data, if needed.
2587          if (!empty($er['modseq'])) {
2588              $ret['modseq'] = $er['modseq'];
2589          }
2590  
2591          unset($this->_temp['search_retry']);
2592  
2593          /* Check for EXPUNGEISSUED (RFC 2180 [4.3]/RFC 5530 [3]). */
2594          if (!empty($resp->data['expungeissued'])) {
2595              $this->noop();
2596          }
2597  
2598          return $ret;
2599      }
2600  
2601      /**
2602       * Parse a SEARCH/SORT response (RFC 3501 [7.2.5]; RFC 4466 [3];
2603       * RFC 5256 [4]; RFC 5267 [3]).
2604       *
2605       * @param Horde_Imap_Client_Interaction_Pipeline $pipeline  Pipeline
2606       *                                                          object.
2607       * @param array $data  A list of IDs (message sequence numbers or UIDs).
2608       */
2609      protected function _parseSearch(
2610          Horde_Imap_Client_Interaction_Pipeline $pipeline,
2611          $data
2612      )
2613      {
2614          /* More than one search response may be sent. */
2615          $pipeline->data['searchresp']->add($data);
2616      }
2617  
2618      /**
2619       * Parse an ESEARCH response (RFC 4466 [2.6.2])
2620       * Format: (TAG "a567") UID COUNT 5 ALL 4:19,21,28
2621       *
2622       * @param Horde_Imap_Client_Interaction_Pipeline $pipeline  Pipeline
2623       *                                                          object.
2624       * @param Horde_Imap_Client_Tokenize $data  The server response.
2625       */
2626      protected function _parseEsearch(
2627          Horde_Imap_Client_Interaction_Pipeline $pipeline,
2628          Horde_Imap_Client_Tokenize $data
2629      )
2630      {
2631          // Ignore search correlator information
2632          if ($data->next() === true) {
2633              $data->flushIterator(false);
2634          }
2635  
2636          // Ignore UID tag
2637          $current = $data->next();
2638          if (Horde_String::upper($current) === 'UID') {
2639              $current = $data->next();
2640          }
2641  
2642          do {
2643              $val = $data->next();
2644              $tag = Horde_String::upper($current);
2645  
2646              switch ($tag) {
2647              case 'ALL':
2648                  $this->_parseSearch($pipeline, $val);
2649                  break;
2650  
2651              case 'COUNT':
2652              case 'MAX':
2653              case 'MIN':
2654              case 'MODSEQ':
2655              case 'RELEVANCY':
2656                  $pipeline->data['esearchresp'][Horde_String::lower($tag)] = $val;
2657                  break;
2658  
2659              case 'PARTIAL':
2660                  // RFC 5267 [4.4]
2661                  $partial = $val->flushIterator();
2662                  $this->_parseSearch($pipeline, end($partial));
2663                  break;
2664              }
2665          } while (($current = $data->next()) !== false);
2666      }
2667  
2668      /**
2669       */
2670      protected function _setComparator($comparator)
2671      {
2672          $cmd = $this->_command('COMPARATOR');
2673          foreach ($comparator as $val) {
2674              $cmd->add(new Horde_Imap_Client_Data_Format_Astring($val));
2675          }
2676          $this->_sendCmd($cmd);
2677      }
2678  
2679      /**
2680       */
2681      protected function _getComparator()
2682      {
2683          $resp = $this->_sendCmd($this->_command('COMPARATOR'));
2684  
2685          return isset($resp->data['comparator'])
2686              ? $resp->data['comparator']
2687              : null;
2688      }
2689  
2690      /**
2691       * Parse a COMPARATOR response (RFC 5255 [4.8])
2692       *
2693       * @param Horde_Imap_Client_Interaction_Pipeline $pipeline  Pipeline
2694       *                                                          object.
2695       * @param Horde_Imap_Client_Tokenize $data  The server response.
2696       */
2697      protected function _parseComparator(
2698          Horde_Imap_Client_Interaction_Pipeline $pipeline,
2699          $data
2700      )
2701      {
2702          $pipeline->data['comparator'] = $data->next();
2703          // Ignore optional matching comparator list
2704      }
2705  
2706      /**
2707       * @throws Horde_Imap_Client_Exception_NoSupportExtension
2708       */
2709      protected function _thread($options)
2710      {
2711          $thread_criteria = array(
2712              Horde_Imap_Client::THREAD_ORDEREDSUBJECT => 'ORDEREDSUBJECT',
2713              Horde_Imap_Client::THREAD_REFERENCES => 'REFERENCES',
2714              Horde_Imap_Client::THREAD_REFS => 'REFS'
2715          );
2716  
2717          $tsort = (isset($options['criteria']))
2718              ? (is_string($options['criteria']) ? Horde_String::upper($options['criteria']) : $thread_criteria[$options['criteria']])
2719              : 'ORDEREDSUBJECT';
2720  
2721          if (!$this->_capability('THREAD', $tsort)) {
2722              switch ($tsort) {
2723              case 'ORDEREDSUBJECT':
2724                  if (empty($options['search'])) {
2725                      $ids = $this->getIdsOb(Horde_Imap_Client_Ids::ALL, !empty($options['sequence']));
2726                  } else {
2727                      $search_res = $this->search($this->_selected, $options['search'], array('sequence' => !empty($options['sequence'])));
2728                      $ids = $search_res['match'];
2729                  }
2730  
2731                  /* Do client-side ORDEREDSUBJECT threading. */
2732                  $query = new Horde_Imap_Client_Fetch_Query();
2733                  $query->envelope();
2734                  $query->imapDate();
2735  
2736                  $fetch_res = $this->fetch($this->_selected, $query, array(
2737                      'ids' => $ids
2738                  ));
2739  
2740                  if (!isset($this->_temp['clientsort'])) {
2741                      $this->_temp['clientsort'] = new Horde_Imap_Client_Socket_ClientSort($this);
2742                  }
2743                  return $this->_temp['clientsort']->threadOrderedSubject($fetch_res, empty($options['sequence']));
2744  
2745              case 'REFERENCES':
2746              case 'REFS':
2747                  throw new Horde_Imap_Client_Exception_NoSupportExtension(
2748                      'THREAD',
2749                      sprintf('Server does not support "%s" thread sort.', $tsort)
2750                  );
2751              }
2752          }
2753  
2754          $cmd = $this->_command(
2755              empty($options['sequence']) ? 'UID THREAD' : 'THREAD'
2756          )->add($tsort);
2757  
2758          /* If UTF-8 support is activated, a client MUST send the UTF-8
2759           * charset specification since charset is mandatory for this
2760           * command (RFC 6855 [3]; Errata 4029). */
2761          if (empty($options['search'])) {
2762              if (!$this->_capability()->isEnabled('UTF8=ACCEPT')) {
2763                  $cmd->add('US-ASCII');
2764              } else {
2765                  $cmd->add('UTF-8');
2766              }
2767              $cmd->add('ALL');
2768          } else {
2769              $search_query = $options['search']->build();
2770              if (!$this->_capability()->isEnabled('UTF8=ACCEPT')) {
2771                  $cmd->add(is_null($search_query['charset']) ? 'US-ASCII' : $search_query['charset']);
2772              }
2773              $cmd->add($search_query['query'], true);
2774          }
2775  
2776          return new Horde_Imap_Client_Data_Thread(
2777              $this->_sendCmd($cmd)->data['threadparse'],
2778              empty($options['sequence']) ? 'uid' : 'sequence'
2779          );
2780      }
2781  
2782      /**
2783       * Parse a THREAD response (RFC 5256 [4]).
2784       *
2785       * @param Horde_Imap_Client_Interaction_Pipeline $pipeline  Pipeline
2786       *                                                          object.
2787       * @param Horde_Imap_Client_Tokenize $data  Thread data.
2788       */
2789      protected function _parseThread(
2790          Horde_Imap_Client_Interaction_Pipeline $pipeline,
2791          Horde_Imap_Client_Tokenize $data
2792      )
2793      {
2794          $out = array();
2795  
2796          while ($data->next() !== false) {
2797              $thread = array();
2798              $this->_parseThreadLevel($thread, $data);
2799              $out[] = $thread;
2800          }
2801  
2802          $pipeline->data['threadparse'] = $out;
2803      }
2804  
2805      /**
2806       * Parse a level of a THREAD response (RFC 5256 [4]).
2807       *
2808       * @param array $thread                     Results.
2809       * @param Horde_Imap_Client_Tokenize $data  Thread data.
2810       * @param integer $level                    The current tree level.
2811       */
2812      protected function _parseThreadLevel(&$thread,
2813                                           Horde_Imap_Client_Tokenize $data,
2814                                           $level = 0)
2815      {
2816          while (($curr = $data->next()) !== false) {
2817              if ($curr === true) {
2818                  $this->_parseThreadLevel($thread, $data, $level);
2819              } elseif (!is_bool($curr)) {
2820                  $thread[$curr] = $level++;
2821              }
2822          }
2823      }
2824  
2825      /**
2826       */
2827      protected function _fetch(Horde_Imap_Client_Fetch_Results $results,
2828                                $queries)
2829      {
2830          $pipeline = $this->_pipeline();
2831          $pipeline->data['fetch_lookup'] = array();
2832          $pipeline->data['fetch_followup'] = array();
2833  
2834          foreach ($queries as $options) {
2835              $this->_fetchCmd($pipeline, $options);
2836              $sequence = $options['ids']->sequence;
2837          }
2838  
2839          try {
2840              $resp = $this->_sendCmd($pipeline);
2841  
2842              /* Check for EXPUNGEISSUED (RFC 2180 [4.1]/RFC 5530 [3]). */
2843              if (!empty($resp->data['expungeissued'])) {
2844                  $this->noop();
2845              }
2846  
2847              foreach ($resp->fetch as $k => $v) {
2848                  $results->get($sequence ? $k : $v->getUid())->merge($v);
2849              }
2850          } catch (Horde_Imap_Client_Exception_ServerResponse $e) {
2851              if ($e->status === Horde_Imap_Client_Interaction_Server::NO) {
2852                  if ($e->getCode() === $e::UNKNOWNCTE ||
2853                      $e->getCode() === $e::PARSEERROR) {
2854                      /* UNKNOWN-CTE error. Redo the query without the BINARY
2855                       * elements. Also include PARSEERROR in this as
2856                       * Dovecot >= 2.2 binary fetch treats broken email as PARSE
2857                       * error and no longer UNKNOWN-CTE
2858                       */
2859                      if (!empty($pipeline->data['binaryquery'])) {
2860                          foreach ($queries as $val) {
2861                              foreach ($pipeline->data['binaryquery'] as $key2 => $val2) {
2862                                  unset($val2['decode']);
2863                                  $val['_query']->bodyPart($key2, $val2);
2864                                  $val['_query']->remove(Horde_Imap_Client::FETCH_BODYPARTSIZE, $key2);
2865                              }
2866                              $pipeline->data['fetch_followup'][] = $val;
2867                          }
2868                      } else {
2869                          $this->noop();
2870                      }
2871                  } elseif ($sequence) {
2872                      /* A NO response, when coupled with a sequence FETCH, most
2873                       * likely means that messages were expunged. (RFC 2180
2874                       * [4.1]) */
2875                      $this->noop();
2876                  }
2877              }
2878          } catch (Exception $e) {
2879              // For any other error, ignore the Exception - fetch() is nice in
2880              // that the return value explicitly handles missing data for any
2881              // given message.
2882          }
2883  
2884          if (!empty($pipeline->data['fetch_followup'])) {
2885              $this->_fetch($results, $pipeline->data['fetch_followup']);
2886          }
2887      }
2888  
2889      /**
2890       * Add a FETCH command to the given pipeline.
2891       *
2892       * @param Horde_Imap_Client_Interaction_Pipeline $pipeline  Pipeline
2893       *                                                          object.
2894       * @param array $options                                    Fetch query
2895       *                                                          options
2896       */
2897      protected function _fetchCmd(
2898          Horde_Imap_Client_Interaction_Pipeline $pipeline,
2899          $options
2900      )
2901      {
2902          $fetch = new Horde_Imap_Client_Data_Format_List();
2903          $sequence = $options['ids']->sequence;
2904  
2905          /* Build an IMAP4rev1 compliant FETCH query. We handle the following
2906           * criteria:
2907           *   BINARY[.PEEK][<section #>]<<partial>> (RFC 3516)
2908           *     see BODY[] response
2909           *   BINARY.SIZE[<section #>] (RFC 3516)
2910           *   BODY[.PEEK][<section>]<<partial>>
2911           *     <section> = HEADER, HEADER.FIELDS, HEADER.FIELDS.NOT, MIME,
2912           *                 TEXT, empty
2913           *     <<partial>> = 0.# (# of bytes)
2914           *   BODYSTRUCTURE
2915           *   ENVELOPE
2916           *   FLAGS
2917           *   INTERNALDATE
2918           *   MODSEQ (RFC 7162)
2919           *   RFC822.SIZE
2920           *   UID
2921           *
2922           * No need to support these (can be built from other queries):
2923           * ===========================================================
2924           *   ALL macro => (FLAGS INTERNALDATE RFC822.SIZE ENVELOPE)
2925           *   BODY => Use BODYSTRUCTURE instead
2926           *   FAST macro => (FLAGS INTERNALDATE RFC822.SIZE)
2927           *   FULL macro => (FLAGS INTERNALDATE RFC822.SIZE ENVELOPE BODY)
2928           *   RFC822 => BODY[]
2929           *   RFC822.HEADER => BODY[HEADER]
2930           *   RFC822.TEXT => BODY[TEXT]
2931           */
2932  
2933          foreach ($options['_query'] as $type => $c_val) {
2934              switch ($type) {
2935              case Horde_Imap_Client::FETCH_STRUCTURE:
2936                  $fetch->add('BODYSTRUCTURE');
2937                  break;
2938  
2939              case Horde_Imap_Client::FETCH_FULLMSG:
2940                  if (empty($c_val['peek'])) {
2941                      $this->openMailbox($this->_selected, Horde_Imap_Client::OPEN_READWRITE);
2942                  }
2943                  $fetch->add(
2944                      'BODY' .
2945                      (!empty($c_val['peek']) ? '.PEEK' : '') .
2946                      '[]' .
2947                      $this->_partialAtom($c_val)
2948                  );
2949                  break;
2950  
2951              case Horde_Imap_Client::FETCH_HEADERTEXT:
2952              case Horde_Imap_Client::FETCH_BODYTEXT:
2953              case Horde_Imap_Client::FETCH_MIMEHEADER:
2954              case Horde_Imap_Client::FETCH_BODYPART:
2955              case Horde_Imap_Client::FETCH_HEADERS:
2956                  foreach ($c_val as $key => $val) {
2957                      $cmd = ($key == 0)
2958                          ? ''
2959                          : $key . '.';
2960                      $main_cmd = 'BODY';
2961  
2962                      switch ($type) {
2963                      case Horde_Imap_Client::FETCH_HEADERTEXT:
2964                          $cmd .= 'HEADER';
2965                          break;
2966  
2967                      case Horde_Imap_Client::FETCH_BODYTEXT:
2968                          $cmd .= 'TEXT';
2969                          break;
2970  
2971                      case Horde_Imap_Client::FETCH_MIMEHEADER:
2972                          $cmd .= 'MIME';
2973                          break;
2974  
2975                      case Horde_Imap_Client::FETCH_BODYPART:
2976                          // Remove the last dot from the string.
2977                          $cmd = substr($cmd, 0, -1);
2978  
2979                          if (!empty($val['decode']) &&
2980                              $this->_capability('BINARY')) {
2981                              $main_cmd = 'BINARY';
2982                              $pipeline->data['binaryquery'][$key] = $val;
2983                          }
2984                          break;
2985  
2986                      case Horde_Imap_Client::FETCH_HEADERS:
2987                          $cmd .= 'HEADER.FIELDS';
2988                          if (!empty($val['notsearch'])) {
2989                              $cmd .= '.NOT';
2990                          }
2991                          $cmd .= ' (' . implode(' ', array_map('Horde_String::upper', $val['headers'])) . ')';
2992  
2993                          // Maintain a command -> label lookup so we can put
2994                          // the results in the proper location.
2995                          $pipeline->data['fetch_lookup'][$cmd] = $key;
2996                      }
2997  
2998                      if (empty($val['peek'])) {
2999                          $this->openMailbox($this->_selected, Horde_Imap_Client::OPEN_READWRITE);
3000                      }
3001  
3002                      $fetch->add(
3003                          $main_cmd .
3004                          (!empty($val['peek']) ? '.PEEK' : '') .
3005                          '[' . $cmd . ']' .
3006                          $this->_partialAtom($val)
3007                      );
3008                  }
3009                  break;
3010  
3011              case Horde_Imap_Client::FETCH_BODYPARTSIZE:
3012                  if ($this->_capability('BINARY')) {
3013                      foreach ($c_val as $val) {
3014                          $fetch->add('BINARY.SIZE[' . $val . ']');
3015                      }
3016                  }
3017                  break;
3018  
3019              case Horde_Imap_Client::FETCH_ENVELOPE:
3020                  $fetch->add('ENVELOPE');
3021                  break;
3022  
3023              case Horde_Imap_Client::FETCH_FLAGS:
3024                  $fetch->add('FLAGS');
3025                  break;
3026  
3027              case Horde_Imap_Client::FETCH_IMAPDATE:
3028                  $fetch->add('INTERNALDATE');
3029                  break;
3030  
3031              case Horde_Imap_Client::FETCH_SIZE:
3032                  $fetch->add('RFC822.SIZE');
3033                  break;
3034  
3035              case Horde_Imap_Client::FETCH_UID:
3036                  /* A UID FETCH will always return UID information (RFC 3501
3037                   * [6.4.8]). Don't add to query as it just creates a longer
3038                   * FETCH command. */
3039                  if ($sequence) {
3040                      $fetch->add('UID');
3041                  }
3042                  break;
3043  
3044              case Horde_Imap_Client::FETCH_SEQ:
3045                  /* Nothing we need to add to fetch request unless sequence is
3046                   * the only criteria (see below). */
3047                  break;
3048  
3049              case Horde_Imap_Client::FETCH_MODSEQ:
3050                  /* The 'changedsince' modifier implicitly adds the MODSEQ
3051                   * FETCH item (RFC 7162 [3.1.4.1]). Don't add to query as it
3052                   * just creates a longer FETCH command. */
3053                  if (empty($options['changedsince'])) {
3054                      $fetch->add('MODSEQ');
3055                  }
3056                  break;
3057              }
3058          }
3059  
3060          /* If empty fetch, add UID to make command valid. */
3061          if (!count($fetch)) {
3062              $fetch->add('UID');
3063          }
3064  
3065          /* Add changedsince parameters. */
3066          if (empty($options['changedsince'])) {
3067              $fetch_cmd = $fetch;
3068          } else {
3069              /* We might just want the list of UIDs changed since a given
3070               * modseq. In that case, we don't have any other FETCH attributes,
3071               * but RFC 3501 requires at least one specified attribute. */
3072              $fetch_cmd = array(
3073                  $fetch,
3074                  new Horde_Imap_Client_Data_Format_List(array(
3075                      'CHANGEDSINCE',
3076                      new Horde_Imap_Client_Data_Format_Number($options['changedsince'])
3077                  ))
3078              );
3079          }
3080  
3081          /* The FETCH command should be the only command issued by this library
3082           * that should ever approach the command length limit.
3083           * @todo Move this check to a more centralized location (_command()?).
3084           * For simplification, assume that the UID list is the limiting factor
3085           * and split this list at a sequence comma delimiter if it exceeds
3086           * the character limit. */
3087          foreach ($options['ids']->split($this->_capability()->cmdlength) as $val) {
3088              $cmd = $this->_command(
3089                  $sequence ? 'FETCH' : 'UID FETCH'
3090              )->add(array(
3091                  $val,
3092                  $fetch_cmd
3093              ));
3094              $pipeline->add($cmd);
3095          }
3096      }
3097  
3098      /**
3099       * Add a partial atom to an IMAP command based on the criteria options.
3100       *
3101       * @param array $opts  Criteria options.
3102       *
3103       * @return string  The partial atom.
3104       */
3105      protected function _partialAtom($opts)
3106      {
3107          if (!empty($opts['length'])) {
3108              return '<' . (empty($opts['start']) ? 0 : intval($opts['start'])) . '.' . intval($opts['length']) . '>';
3109          }
3110  
3111          return empty($opts['start'])
3112              ? ''
3113              : ('<' . intval($opts['start']) . '>');
3114      }
3115  
3116      /**
3117       * Parse a FETCH response (RFC 3501 [7.4.2]). A FETCH response may occur
3118       * due to a FETCH command, or due to a change in a message's state (i.e.
3119       * the flags change).
3120       *
3121       * @param Horde_Imap_Client_Interaction_Pipeline $pipeline  Pipeline
3122       *                                                          object.
3123       * @param integer $id                       The message sequence number.
3124       * @param Horde_Imap_Client_Tokenize $data  The server response.
3125       */
3126      protected function _parseFetch(
3127          Horde_Imap_Client_Interaction_Pipeline $pipeline,
3128          $id,
3129          Horde_Imap_Client_Tokenize $data
3130      )
3131      {
3132          if ($data->next() !== true) {
3133              return;
3134          }
3135  
3136          $ob = $pipeline->fetch->get($id);
3137          $ob->setSeq($id);
3138  
3139          $flags = $modseq = $uid = false;
3140  
3141          while (($tag = $data->next()) !== false) {
3142              $tag = Horde_String::upper($tag);
3143  
3144              /* Catch equivalent RFC822 tags, in case server returns them
3145               * (in error, since we only use BODY in FETCH requests). */
3146              switch ($tag) {
3147              case 'RFC822':
3148                  $tag = 'BODY[]';
3149                  break;
3150  
3151              case 'RFC822.HEADER':
3152                  $tag = 'BODY[HEADER]';
3153                  break;
3154  
3155              case 'RFC822.TEXT':
3156                  $tag = 'BODY[TEXT]';
3157                  break;
3158              }
3159  
3160              switch ($tag) {
3161              case 'BODYSTRUCTURE':
3162                  $data->next();
3163                  $structure = $this->_parseBodystructure($data);
3164                  $structure->buildMimeIds();
3165                  $ob->setStructure($structure);
3166                  break;
3167  
3168              case 'ENVELOPE':
3169                  $data->next();
3170                  $ob->setEnvelope($this->_parseEnvelope($data));
3171                  break;
3172  
3173              case 'FLAGS':
3174                  $data->next();
3175                  $ob->setFlags($data->flushIterator());
3176                  $flags = true;
3177                  break;
3178  
3179              case 'INTERNALDATE':
3180                  $ob->setImapDate($data->next());
3181                  break;
3182  
3183              case 'RFC822.SIZE':
3184                  $ob->setSize($data->next());
3185                  break;
3186  
3187              case 'UID':
3188                  $ob->setUid($data->next());
3189                  $uid = true;
3190                  break;
3191  
3192              case 'MODSEQ':
3193                  $data->next();
3194                  $modseq = $data->next();
3195                  $data->next();
3196  
3197                  /* MODSEQ must be greater than 0, so do sanity checking. */
3198                  if ($modseq > 0) {
3199                      $ob->setModSeq($modseq);
3200  
3201                      /* Store MODSEQ value. It may be used as the highestmodseq
3202                       * once a tagged response is received (RFC 7162 [6]). */
3203                      $pipeline->data['modseqs'][] = $modseq;
3204                  }
3205                  break;
3206  
3207              default:
3208                  // Catch BODY[*]<#> responses
3209                  if (strpos($tag, 'BODY[') === 0) {
3210                      // Remove the beginning 'BODY['
3211                      $tag = substr($tag, 5);
3212  
3213                      // BODY[HEADER.FIELDS] request
3214                      if (!empty($pipeline->data['fetch_lookup']) &&
3215                          (strpos($tag, 'HEADER.FIELDS') !== false)) {
3216                          $data->next();
3217                          $sig = $tag . ' (' . implode(' ', array_map('Horde_String::upper', $data->flushIterator())) . ')';
3218  
3219                          // Ignore the trailing bracket
3220                          $data->next();
3221  
3222                          $ob->setHeaders($pipeline->data['fetch_lookup'][$sig], $data->next());
3223                      } else {
3224                          // Remove trailing bracket and octet start info
3225                          $tag = substr($tag, 0, strrpos($tag, ']'));
3226  
3227                          if (!strlen($tag)) {
3228                              // BODY[] request
3229                              if (!is_null($tmp = $data->nextStream())) {
3230                                  $ob->setFullMsg($tmp);
3231                              }
3232                          } elseif (is_numeric(substr($tag, -1))) {
3233                              // BODY[MIMEID] request
3234                              if (!is_null($tmp = $data->nextStream())) {
3235                                  $ob->setBodyPart($tag, $tmp);
3236                              }
3237                          } else {
3238                              // BODY[HEADER|TEXT|MIME] request
3239                              if (($last_dot = strrpos($tag, '.')) === false) {
3240                                  $mime_id = 0;
3241                              } else {
3242                                  $mime_id = substr($tag, 0, $last_dot);
3243                                  $tag = substr($tag, $last_dot + 1);
3244                              }
3245  
3246                              if (!is_null($tmp = $data->nextStream())) {
3247                                  switch ($tag) {
3248                                  case 'HEADER':
3249                                      $ob->setHeaderText($mime_id, $tmp);
3250                                      break;
3251  
3252                                  case 'TEXT':
3253                                      $ob->setBodyText($mime_id, $tmp);
3254                                      break;
3255  
3256                                  case 'MIME':
3257                                      $ob->setMimeHeader($mime_id, $tmp);
3258                                      break;
3259                                  }
3260                              }
3261                          }
3262                      }
3263                  } elseif (strpos($tag, 'BINARY[') === 0) {
3264                      // Catch BINARY[*]<#> responses
3265                      // Remove the beginning 'BINARY[' and the trailing bracket
3266                      // and octet start info
3267                      $tag = substr($tag, 7, strrpos($tag, ']') - 7);
3268                      $body = $data->nextStream();
3269  
3270                      if (is_null($body)) {
3271                          /* Dovecot bug (as of 2.2.12): binary fetch of body
3272                           * part may fail with NIL return if decoding failed on
3273                           * server. Try again with non-decoded body. */
3274                          $bq = $pipeline->data['binaryquery'][$tag];
3275                          unset($bq['decode']);
3276  
3277                          $query = new Horde_Imap_Client_Fetch_Query();
3278                          $query->bodyPart($tag, $bq);
3279  
3280                          $qids = ($quid = $ob->getUid())
3281                              ? new Horde_Imap_Client_Ids($quid)
3282                              : new Horde_Imap_Client_Ids($id, true);
3283  
3284                          $pipeline->data['fetch_followup'][] = array(
3285                              '_query' => $query,
3286                              'ids' => $qids
3287                          );
3288                      } else {
3289                          $ob->setBodyPart(
3290                              $tag,
3291                              $body,
3292                              empty($this->_temp['literal8']) ? '8bit' : 'binary'
3293                          );
3294                      }
3295                  } elseif (strpos($tag, 'BINARY.SIZE[') === 0) {
3296                      // Catch BINARY.SIZE[*] responses
3297                      // Remove the beginning 'BINARY.SIZE[' and the trailing
3298                      // bracket and octet start info
3299                      $tag = substr($tag, 12, strrpos($tag, ']') - 12);
3300                      $ob->setBodyPartSize($tag, $data->next());
3301                  }
3302                  break;
3303              }
3304          }
3305  
3306          /* MODSEQ issue: Oh joy. Per RFC 5162 (see Errata #1807), FETCH FLAGS
3307           * responses are NOT required to provide UID information, even if
3308           * QRESYNC is explicitly enabled. Caveat: the FLAGS information
3309           * returned during a SELECT/EXAMINE MUST contain UIDs so we are OK
3310           * there.
3311           * The good news: all decent IMAP servers (Cyrus, Dovecot) will always
3312           * provide UID information, so this is not normally an issue.
3313           * The bad news: spec-wise, this behavior cannot be 100% guaranteed.
3314           * Compromise: We will watch for a FLAGS response with a MODSEQ and
3315           * check if a UID exists also. If not, put the sequence number in a
3316           * queue - it is possible the UID information may appear later in an
3317           * untagged response. When the command is over, double check to make
3318           * sure there are none of these MODSEQ/FLAGS that are still UID-less.
3319           * In the (rare) event that there is, don't cache anything and
3320           * immediately close the mailbox: flags will be correctly sync'd next
3321           * mailbox open so we only lose a bit of caching efficiency.
3322           * Otherwise, we could end up with an inconsistent cached state.
3323           * This Errata has been fixed in 7162 [3.2.4]. */
3324          if ($flags && $modseq && !$uid) {
3325              $pipeline->data['modseqs_nouid'][] = $id;
3326          }
3327      }
3328  
3329      /**
3330       * Recursively parse BODYSTRUCTURE data from a FETCH return (see
3331       * RFC 3501 [7.4.2]).
3332       *
3333       * @param Horde_Imap_Client_Tokenize $data  Data returned from the server.
3334       *
3335       * @return Horde_Mime_Part  Mime part object.
3336       */
3337      protected function _parseBodystructure(Horde_Imap_Client_Tokenize $data)
3338      {
3339          $ob = new Horde_Mime_Part();
3340  
3341          // If index 0 is an array, this is a multipart part.
3342          if (($entry = $data->next()) === true) {
3343              do {
3344                  $ob->addPart($this->_parseBodystructure($data));
3345              } while (($entry = $data->next()) === true);
3346  
3347              // The subpart type.
3348              $ob->setType('multipart/' . $entry);
3349  
3350              // After the subtype is further extension information. This
3351              // information MAY appear for BODYSTRUCTURE requests.
3352  
3353              // This is parameter information.
3354              if (($tmp = $data->next()) === false) {
3355                  return $ob;
3356              } elseif ($tmp === true) {
3357                  foreach ($this->_parseStructureParams($data) as $key => $val) {
3358                      $ob->setContentTypeParameter($key, $val);
3359                  }
3360              }
3361          } else {
3362              $ob->setType($entry . '/' . $data->next());
3363  
3364              if ($data->next() === true) {
3365                  foreach ($this->_parseStructureParams($data) as $key => $val) {
3366                      $ob->setContentTypeParameter($key, $val);
3367                  }
3368              }
3369  
3370              if (!is_null($tmp = $data->next())) {
3371                  $ob->setContentId($tmp);
3372              }
3373  
3374              if (!is_null($tmp = $data->next())) {
3375                  $ob->setDescription(Horde_Mime::decode($tmp));
3376              }
3377  
3378              $te = $data->next();
3379              $bytes = $data->next();
3380  
3381              if (!is_null($te)) {
3382                  $ob->setTransferEncoding($te);
3383  
3384                  /* Base64 transfer encoding is approx. 33% larger than
3385                   * original data size (RFC 2045 [6.8]). Return from
3386                   * BODYSTRUCTURE is the size of the ENCODED data (RFC 3501
3387                   * [7.4.2]). */
3388                  if (strcasecmp($te, 'base64') === 0) {
3389                      $bytes *= 0.75;
3390                  }
3391              }
3392  
3393              $ob->setBytes($bytes);
3394  
3395              // If the type is 'message/rfc822' or 'text/*', several extra
3396              // fields are included
3397              switch ($ob->getPrimaryType()) {
3398              case 'message':
3399                  if ($ob->getSubType() == 'rfc822') {
3400                      if ($data->next() === true) {
3401                          // Ignore: envelope
3402                          $data->flushIterator(false);
3403                      }
3404                      if ($data->next() === true) {
3405                          $ob->addPart($this->_parseBodystructure($data));
3406                      }
3407                      $data->next(); // Ignore: lines
3408                  }
3409                  break;
3410  
3411              case 'text':
3412                  $data->next(); // Ignore: lines
3413                  break;
3414              }
3415  
3416              // After the subtype is further extension information. This
3417              // information MAY appear for BODYSTRUCTURE requests.
3418  
3419              // Ignore: MD5
3420              if ($data->next() === false) {
3421                  return $ob;
3422              }
3423          }
3424  
3425          // This is disposition information
3426          if (($tmp = $data->next()) === false) {
3427              return $ob;
3428          } elseif ($tmp === true) {
3429              $ob->setDisposition($data->next());
3430  
3431              if ($data->next() === true) {
3432                  foreach ($this->_parseStructureParams($data) as $key => $val) {
3433                      $ob->setDispositionParameter($key, $val);
3434                  }
3435              }
3436              $data->next();
3437          }
3438  
3439          // This is language information. It is either a single value or a list
3440          // of values.
3441          if (($tmp = $data->next()) === false) {
3442              return $ob;
3443          } elseif (!is_null($tmp)) {
3444              $ob->setLanguage(($tmp === true) ? $data->flushIterator() : $tmp);
3445          }
3446  
3447          // Ignore location (RFC 2557) and consume closing paren.
3448          $data->flushIterator(false);
3449  
3450          return $ob;
3451      }
3452  
3453      /**
3454       * Helper function to parse a parameters-like tokenized array.
3455       *
3456       * @param mixed $data  Message data. Either a Horde_Imap_Client_Tokenize
3457       *                     object or null.
3458       *
3459       * @return array  The parameter array.
3460       */
3461      protected function _parseStructureParams($data)
3462      {
3463          $params = array();
3464  
3465          if (is_null($data)) {
3466              return $params;
3467          }
3468  
3469          while (($name = $data->next()) !== false) {
3470              $params[Horde_String::lower($name)] = $data->next();
3471          }
3472  
3473          $cp = new Horde_Mime_Headers_ContentParam('Unused', $params);
3474  
3475          return $cp->params;
3476      }
3477  
3478      /**
3479       * Parse ENVELOPE data from a FETCH return (see RFC 3501 [7.4.2]).
3480       *
3481       * @param Horde_Imap_Client_Tokenize $data  Data returned from the server.
3482       *
3483       * @return Horde_Imap_Client_Data_Envelope  An envelope object.
3484       */
3485      protected function _parseEnvelope(Horde_Imap_Client_Tokenize $data)
3486      {
3487          // 'route', the 2nd element, is deprecated by RFC 2822.
3488          $addr_structure = array(
3489              0 => 'personal',
3490              2 => 'mailbox',
3491              3 => 'host'
3492          );
3493          $env_data = array(
3494              0 => 'date',
3495              1 => 'subject',
3496              2 => 'from',
3497              3 => 'sender',
3498              4 => 'reply_to',
3499              5 => 'to',
3500              6 => 'cc',
3501              7 => 'bcc',
3502              8 => 'in_reply_to',
3503              9 => 'message_id'
3504          );
3505  
3506          $addr_ob = new Horde_Mail_Rfc822_Address();
3507          $env_addrs = $this->getParam('envelope_addrs');
3508          $env_str = $this->getParam('envelope_string');
3509          $key = 0;
3510          $ret = new Horde_Imap_Client_Data_Envelope();
3511  
3512          while (($val = $data->next()) !== false) {
3513              if (!isset($env_data[$key]) || is_null($val)) {
3514                  ++$key;
3515                  continue;
3516              }
3517  
3518              if (is_string($val)) {
3519                  // These entries are text fields.
3520                  $ret->{$env_data[$key]} = substr($val, 0, $env_str);
3521              } else {
3522                  // These entries are address structures.
3523                  $group = null;
3524                  $key2 = 0;
3525                  $tmp = new Horde_Mail_Rfc822_List();
3526  
3527                  while ($data->next() !== false) {
3528                      $a_val = $data->flushIterator();
3529  
3530                      // RFC 3501 [7.4.2]: Group entry when host is NIL.
3531                      // Group end when mailbox is NIL; otherwise, this is
3532                      // mailbox name.
3533                      if (is_null($a_val[3])) {
3534                          if (is_null($a_val[2])) {
3535                              $group = null;
3536                          } else {
3537                              $group = new Horde_Mail_Rfc822_Group($a_val[2]);
3538                              $tmp->add($group);
3539                          }
3540                      } else {
3541                          $addr = clone $addr_ob;
3542  
3543                          foreach ($addr_structure as $add_key => $add_val) {
3544                              if (!is_null($a_val[$add_key])) {
3545                                  $addr->$add_val = $a_val[$add_key];
3546                              }
3547                          }
3548  
3549                          if ($group) {
3550                              $group->addresses->add($addr);
3551                          } else {
3552                              $tmp->add($addr);
3553                          }
3554                      }
3555  
3556                      if (++$key2 >= $env_addrs) {
3557                          $data->flushIterator(false);
3558                          break;
3559                      }
3560                  }
3561  
3562                  $ret->{$env_data[$key]} = $tmp;
3563              }
3564  
3565              ++$key;
3566          }
3567  
3568          return $ret;
3569      }
3570  
3571      /**
3572       */
3573      protected function _vanished($modseq, Horde_Imap_Client_Ids $ids)
3574      {
3575          $pipeline = $this->_pipeline(
3576              $this->_command('UID FETCH')->add(array(
3577                  strval($ids),
3578                  'UID',
3579                  new Horde_Imap_Client_Data_Format_List(array(
3580                      'VANISHED',
3581                      'CHANGEDSINCE',
3582                      new Horde_Imap_Client_Data_Format_Number($modseq)
3583                  ))
3584              ))
3585          );
3586          $pipeline->data['vanished'] = $this->getIdsOb();
3587  
3588          return $this->_sendCmd($pipeline)->data['vanished'];
3589      }
3590  
3591      /**
3592       */
3593      protected function _store($options)
3594      {
3595          $pipeline = $this->_storeCmd($options);
3596          $pipeline->data['modified'] = $this->getIdsOb();
3597  
3598          try {
3599              $resp = $this->_sendCmd($pipeline);
3600  
3601              /* Check for EXPUNGEISSUED (RFC 2180 [4.2]/RFC 5530 [3]). */
3602              if (!empty($resp->data['expungeissued'])) {
3603                  $this->noop();
3604              }
3605  
3606              return $resp->data['modified'];
3607          } catch (Horde_Imap_Client_Exception_ServerResponse $e) {
3608              /* A NO response, when coupled with a sequence STORE and
3609               * non-SILENT behavior, most likely means that messages were
3610               * expunged. RFC 2180 [4.2] */
3611              if (empty($pipeline->data['store_silent']) &&
3612                  !empty($options['sequence']) &&
3613                  ($e->status === Horde_Imap_Client_Interaction_Server::NO)) {
3614                  $this->noop();
3615              }
3616  
3617              return $pipeline->data['modified'];
3618          }
3619      }
3620  
3621      /**
3622       * Create a store command.
3623       *
3624       * @param array $options  See Horde_Imap_Client_Base#_store().
3625       *
3626       * @return Horde_Imap_Client_Interaction_Pipeline  Pipeline object.
3627       */
3628      protected function _storeCmd($options)
3629      {
3630          $cmds = array();
3631          $silent = empty($options['unchangedsince'])
3632               ? !($this->_debug->debug || $this->_initCache(true))
3633               : false;
3634  
3635          if (!empty($options['replace'])) {
3636              $cmds[] = array(
3637                  'FLAGS' . ($silent ? '.SILENT' : ''),
3638                  $options['replace']
3639              );
3640          } else {
3641              foreach (array('add' => '+', 'remove' => '-') as $k => $v) {
3642                  if (!empty($options[$k])) {
3643                      $cmds[] = array(
3644                          $v . 'FLAGS' . ($silent ? '.SILENT' : ''),
3645                          $options[$k]
3646                      );
3647                  }
3648              }
3649          }
3650  
3651          $pipeline = $this->_pipeline();
3652          $pipeline->data['store_silent'] = $silent;
3653  
3654          foreach ($cmds as $val) {
3655              $cmd = $this->_command(
3656                  empty($options['sequence']) ? 'UID STORE' : 'STORE'
3657              )->add(strval($options['ids']));
3658              if (!empty($options['unchangedsince'])) {
3659                  $cmd->add(new Horde_Imap_Client_Data_Format_List(array(
3660                      'UNCHANGEDSINCE',
3661                      new Horde_Imap_Client_Data_Format_Number(intval($options['unchangedsince']))
3662                  )));
3663              }
3664              $cmd->add($val);
3665  
3666              $pipeline->add($cmd);
3667          }
3668  
3669          return $pipeline;
3670      }
3671  
3672      /**
3673       */
3674      protected function _copy(Horde_Imap_Client_Mailbox $dest, $options)
3675      {
3676          /* Check for MOVE command (RFC 6851). */
3677          $move_cmd = (!empty($options['move']) &&
3678                       $this->_capability('MOVE'));
3679  
3680          $cmd = $this->_pipeline(
3681              $this->_command(
3682                  ($options['ids']->sequence ? '' : 'UID ') . ($move_cmd ? 'MOVE' : 'COPY')
3683              )->add(array(
3684                  strval($options['ids']),
3685                  $this->_getMboxFormatOb($dest)
3686              ))
3687          );
3688          $cmd->data['copydest'] = $dest;
3689  
3690          // COPY returns no untagged information (RFC 3501 [6.4.7])
3691          try {
3692              $resp = $this->_sendCmd($cmd);
3693          } catch (Horde_Imap_Client_Exception $e) {
3694              if (!empty($options['create']) &&
3695                  !empty($e->resp_data['trycreate'])) {
3696                  $this->createMailbox($dest);
3697                  unset($options['create']);
3698                  return $this->_copy($dest, $options);
3699              }
3700              throw $e;
3701          }
3702  
3703          // If moving, delete the old messages now. Short-circuit if nothing
3704          // was moved.
3705          if (!$move_cmd &&
3706              !empty($options['move']) &&
3707              (isset($resp->data['copyuid']) ||
3708               !$this->_capability('UIDPLUS'))) {
3709              $this->expunge($this->_selected, array(
3710                  'delete' => true,
3711                  'ids' => $options['ids']
3712              ));
3713          }
3714  
3715          return isset($resp->data['copyuid'])
3716              ? $resp->data['copyuid']
3717              : true;
3718      }
3719  
3720      /**
3721       */
3722      protected function _setQuota(Horde_Imap_Client_Mailbox $root, $resources)
3723      {
3724          $limits = new Horde_Imap_Client_Data_Format_List();
3725  
3726          foreach ($resources as $key => $val) {
3727              $limits->add(array(
3728                  Horde_String::upper($key),
3729                  new Horde_Imap_Client_Data_Format_Number($val)
3730              ));
3731          }
3732  
3733          $this->_sendCmd(
3734              $this->_command('SETQUOTA')->add(array(
3735                  $this->_getMboxFormatOb($root),
3736                  $limits
3737              ))
3738          );
3739      }
3740  
3741      /**
3742       */
3743      protected function _getQuota(Horde_Imap_Client_Mailbox $root)
3744      {
3745          $pipeline = $this->_pipeline(
3746              $this->_command('GETQUOTA')->add(
3747                  $this->_getMboxFormatOb($root)
3748              )
3749          );
3750          $pipeline->data['quotaresp'] = array();
3751  
3752          return reset($this->_sendCmd($pipeline)->data['quotaresp']);
3753      }
3754  
3755      /**
3756       * Parse a QUOTA response (RFC 2087 [5.1]).
3757       *
3758       * @param Horde_Imap_Client_Interaction_Pipeline $pipeline  Pipeline
3759       *                                                          object.
3760       * @param Horde_Imap_Client_Tokenize $data  The server response.
3761       */
3762      protected function _parseQuota(
3763          Horde_Imap_Client_Interaction_Pipeline $pipeline,
3764          Horde_Imap_Client_Tokenize $data
3765      )
3766      {
3767          $c = &$pipeline->data['quotaresp'];
3768  
3769          $root = $data->next();
3770          $c[$root] = array();
3771  
3772          $data->next();
3773  
3774          while (($curr = $data->next()) !== false) {
3775              $c[$root][Horde_String::lower($curr)] = array(
3776                  'usage' => $data->next(),
3777                  'limit' => $data->next()
3778              );
3779          }
3780      }
3781  
3782      /**
3783       */
3784      protected function _getQuotaRoot(Horde_Imap_Client_Mailbox $mailbox)
3785      {
3786          $pipeline = $this->_pipeline(
3787              $this->_command('GETQUOTAROOT')->add(
3788                  $this->_getMboxFormatOb($mailbox)
3789              )
3790          );
3791          $pipeline->data['quotaresp'] = array();
3792  
3793          return $this->_sendCmd($pipeline)->data['quotaresp'];
3794      }
3795  
3796      /**
3797       */
3798      protected function _setACL(Horde_Imap_Client_Mailbox $mailbox, $identifier,
3799                                 $options)
3800      {
3801          // SETACL returns no untagged information (RFC 4314 [3.1]).
3802          $this->_sendCmd(
3803              $this->_command('SETACL')->add(array(
3804                  $this->_getMboxFormatOb($mailbox),
3805                  new Horde_Imap_Client_Data_Format_Astring($identifier),
3806                  new Horde_Imap_Client_Data_Format_Astring($options['rights'])
3807              ))
3808          );
3809      }
3810  
3811      /**
3812       */
3813      protected function _deleteACL(Horde_Imap_Client_Mailbox $mailbox, $identifier)
3814      {
3815          // DELETEACL returns no untagged information (RFC 4314 [3.2]).
3816          $this->_sendCmd(
3817              $this->_command('DELETEACL')->add(array(
3818                  $this->_getMboxFormatOb($mailbox),
3819                  new Horde_Imap_Client_Data_Format_Astring($identifier)
3820              ))
3821          );
3822      }
3823  
3824      /**
3825       */
3826      protected function _getACL(Horde_Imap_Client_Mailbox $mailbox)
3827      {
3828          return $this->_sendCmd(
3829              $this->_command('GETACL')->add(
3830                  $this->_getMboxFormatOb($mailbox)
3831              )
3832          )->data['getacl'];
3833      }
3834  
3835      /**
3836       * Parse an ACL response (RFC 4314 [3.6]).
3837       *
3838       * @param Horde_Imap_Client_Interaction_Pipeline $pipeline  Pipeline
3839       *                                                          object.
3840       * @param Horde_Imap_Client_Tokenize $data  The server response.
3841       */
3842      protected function _parseACL(
3843          Horde_Imap_Client_Interaction_Pipeline $pipeline,
3844          Horde_Imap_Client_Tokenize $data
3845      )
3846      {
3847          $acl = array();
3848  
3849          // Ignore mailbox argument -> index 1
3850          $data->next();
3851  
3852          while (($curr = $data->next()) !== false) {
3853              $acl[$curr] = ($curr[0] === '-')
3854                  ? new Horde_Imap_Client_Data_AclNegative($data->next())
3855                  : new Horde_Imap_Client_Data_Acl($data->next());
3856          }
3857  
3858          $pipeline->data['getacl'] = $acl;
3859      }
3860  
3861      /**
3862       */
3863      protected function _listACLRights(Horde_Imap_Client_Mailbox $mailbox,
3864                                        $identifier)
3865      {
3866          $resp = $this->_sendCmd(
3867              $this->_command('LISTRIGHTS')->add(array(
3868                  $this->_getMboxFormatOb($mailbox),
3869                  new Horde_Imap_Client_Data_Format_Astring($identifier)
3870              ))
3871          );
3872  
3873          return isset($resp->data['listaclrights'])
3874              ? $resp->data['listaclrights']
3875              : new Horde_Imap_Client_Data_AclRights();
3876      }
3877  
3878      /**
3879       * Parse a LISTRIGHTS response (RFC 4314 [3.7]).
3880       *
3881       * @param Horde_Imap_Client_Interaction_Pipeline $pipeline  Pipeline
3882       *                                                          object.
3883       * @param Horde_Imap_Client_Tokenize $data  The server response.
3884       */
3885      protected function _parseListRights(
3886          Horde_Imap_Client_Interaction_Pipeline $pipeline,
3887          Horde_Imap_Client_Tokenize $data
3888      )
3889      {
3890          // Ignore mailbox and identifier arguments
3891          $data->next();
3892          $data->next();
3893  
3894          $pipeline->data['listaclrights'] = new Horde_Imap_Client_Data_AclRights(
3895              str_split($data->next()),
3896              $data->flushIterator()
3897          );
3898      }
3899  
3900      /**
3901       */
3902      protected function _getMyACLRights(Horde_Imap_Client_Mailbox $mailbox)
3903      {
3904          $resp = $this->_sendCmd(
3905              $this->_command('MYRIGHTS')->add(
3906                  $this->_getMboxFormatOb($mailbox)
3907              )
3908          );
3909  
3910          return isset($resp->data['myrights'])
3911              ? $resp->data['myrights']
3912              : new Horde_Imap_Client_Data_Acl();
3913      }
3914  
3915      /**
3916       * Parse a MYRIGHTS response (RFC 4314 [3.8]).
3917       *
3918       * @param Horde_Imap_Client_Interaction_Pipeline $pipeline  Pipeline
3919       *                                                          object.
3920       * @param Horde_Imap_Client_Tokenize $data  The server response.
3921       */
3922      protected function _parseMyRights(
3923          Horde_Imap_Client_Interaction_Pipeline $pipeline,
3924          Horde_Imap_Client_Tokenize $data
3925      )
3926      {
3927          // Ignore 1st token (mailbox name)
3928          $data->next();
3929  
3930          $pipeline->data['myrights'] = new Horde_Imap_Client_Data_Acl($data->next());
3931      }
3932  
3933      /**
3934       */
3935      protected function _getMetadata(Horde_Imap_Client_Mailbox $mailbox,
3936                                      $entries, $options)
3937      {
3938          $pipeline = $this->_pipeline();
3939          $pipeline->data['metadata'] = array();
3940  
3941          if ($this->_capability('METADATA') ||
3942              (strlen($mailbox) && $this->_capability('METADATA-SERVER'))) {
3943              $cmd_options = new Horde_Imap_Client_Data_Format_List();
3944  
3945              if (!empty($options['maxsize'])) {
3946                  $cmd_options->add(array(
3947                      'MAXSIZE',
3948                      new Horde_Imap_Client_Data_Format_Number($options['maxsize'])
3949                  ));
3950              }
3951              if (!empty($options['depth'])) {
3952                  $cmd_options->add(array(
3953                      'DEPTH',
3954                      new Horde_Imap_Client_Data_Format_Number($options['depth'])
3955                  ));
3956              }
3957  
3958              $queries = new Horde_Imap_Client_Data_Format_List();
3959              foreach ($entries as $md_entry) {
3960                  $queries->add(new Horde_Imap_Client_Data_Format_Astring($md_entry));
3961              }
3962  
3963              $cmd = $this->_command('GETMETADATA')->add(
3964                  $this->_getMboxFormatOb($mailbox)
3965              );
3966              if (count($cmd_options)) {
3967                  $cmd->add($cmd_options);
3968              }
3969              $cmd->add($queries);
3970  
3971              $pipeline->add($cmd);
3972          } else {
3973              if (!$this->_capability('ANNOTATEMORE') &&
3974                  !$this->_capability('ANNOTATEMORE2')) {
3975                  throw new Horde_Imap_Client_Exception_NoSupportExtension('METADATA');
3976              }
3977  
3978              $queries = array();
3979              foreach ($entries as $md_entry) {
3980                  list($entry, $type) = $this->_getAnnotateMoreEntry($md_entry);
3981  
3982                  if (!isset($queries[$type])) {
3983                      $queries[$type] = new Horde_Imap_Client_Data_Format_List();
3984                  }
3985                  $queries[$type]->add(new Horde_Imap_Client_Data_Format_String($entry));
3986              }
3987  
3988              foreach ($queries as $key => $val) {
3989                  // TODO: Honor maxsize and depth options.
3990                  $pipeline->add(
3991                      $this->_command('GETANNOTATION')->add(array(
3992                          $this->_getMboxFormatOb($mailbox),
3993                          $val,
3994                          new Horde_Imap_Client_Data_Format_String($key)
3995                      ))
3996                  );
3997              }
3998          }
3999  
4000          return $this->_sendCmd($pipeline)->data['metadata'];
4001      }
4002  
4003      /**
4004       * Split a name for the METADATA extension into the correct syntax for the
4005       * older ANNOTATEMORE version.
4006       *
4007       * @param string $name  A name for a metadata entry.
4008       *
4009       * @return array  A list of two elements: The entry name and the value
4010       *                type.
4011       *
4012       * @throws Horde_Imap_Client_Exception
4013       */
4014      protected function _getAnnotateMoreEntry($name)
4015      {
4016          if (substr($name, 0, 7) === '/shared') {
4017              return array(substr($name, 7), 'value.shared');
4018          } else if (substr($name, 0, 8) === '/private') {
4019              return array(substr($name, 8), 'value.priv');
4020          }
4021  
4022          $e = new Horde_Imap_Client_Exception(
4023              Horde_Imap_Client_Translation::r("Invalid METADATA entry: \"%s\"."),
4024              Horde_Imap_Client_Exception::METADATA_INVALID
4025          );
4026          $e->messagePrintf(array($name));
4027          throw $e;
4028      }
4029  
4030      /**
4031       */
4032      protected function _setMetadata(Horde_Imap_Client_Mailbox $mailbox, $data)
4033      {
4034          if ($this->_capability('METADATA') ||
4035              (strlen($mailbox) && $this->_capability('METADATA-SERVER'))) {
4036              $data_elts = new Horde_Imap_Client_Data_Format_List();
4037  
4038              foreach ($data as $key => $value) {
4039                  $data_elts->add(array(
4040                      new Horde_Imap_Client_Data_Format_Astring($key),
4041                      /* METADATA supports literal8 - thus, it implicitly
4042                       * supports non-ASCII characters in the data. */
4043                      new Horde_Imap_Client_Data_Format_Nstring_Nonascii($value)
4044                  ));
4045              }
4046  
4047              $cmd = $this->_command('SETMETADATA')->add(array(
4048                  $this->_getMboxFormatOb($mailbox),
4049                  $data_elts
4050              ));
4051          } else {
4052              if (!$this->_capability('ANNOTATEMORE') &&
4053                  !$this->_capability('ANNOTATEMORE2')) {
4054                  throw new Horde_Imap_Client_Exception_NoSupportExtension('METADATA');
4055              }
4056  
4057              $cmd = $this->_pipeline();
4058  
4059              foreach ($data as $md_entry => $value) {
4060                  list($entry, $type) = $this->_getAnnotateMoreEntry($md_entry);
4061  
4062                  $cmd->add(
4063                      $this->_command('SETANNOTATION')->add(array(
4064                          $this->_getMboxFormatOb($mailbox),
4065                          new Horde_Imap_Client_Data_Format_String($entry),
4066                          new Horde_Imap_Client_Data_Format_List(array(
4067                              new Horde_Imap_Client_Data_Format_String($type),
4068                              /* ANNOTATEMORE supports literal8 - thus, it
4069                               * implicitly supports non-ASCII characters in the
4070                               * data. */
4071                              new Horde_Imap_Client_Data_Format_Nstring_Nonascii($value)
4072                          ))
4073                      ))
4074                  );
4075              }
4076          }
4077  
4078          $this->_sendCmd($cmd);
4079      }
4080  
4081      /**
4082       * Parse an ANNOTATION response (ANNOTATEMORE/ANNOTATEMORE2).
4083       *
4084       * @param Horde_Imap_Client_Interaction_Pipeline $pipeline  Pipeline
4085       *                                                          object.
4086       * @param Horde_Imap_Client_Tokenize $data  The server response.
4087       *
4088       * @throws Horde_Imap_Client_Exception
4089       */
4090      protected function _parseAnnotation(
4091          Horde_Imap_Client_Interaction_Pipeline $pipeline,
4092          Horde_Imap_Client_Tokenize $data
4093      )
4094      {
4095          // Mailbox name is in UTF7-IMAP.
4096          $mbox = Horde_Imap_Client_Mailbox::get($data->next(), true);
4097          $entry = $data->next();
4098  
4099          // Ignore unsolicited responses.
4100          if ($data->next() !== true) {
4101              return;
4102          }
4103  
4104          while (($type = $data->next()) !== false) {
4105              switch ($type) {
4106              case 'value.priv':
4107                  $pipeline->data['metadata'][strval($mbox)]['/private' . $entry] = $data->next();
4108                  break;
4109  
4110              case 'value.shared':
4111                  $pipeline->data['metadata'][strval($mbox)]['/shared' . $entry] = $data->next();
4112                  break;
4113  
4114              default:
4115                  $e = new Horde_Imap_Client_Exception(
4116                      Horde_Imap_Client_Translation::r("Invalid METADATA value type \"%s\"."),
4117                      Horde_Imap_Client_Exception::METADATA_INVALID
4118                  );
4119                  $e->messagePrintf(array($type));
4120                  throw $e;
4121              }
4122          }
4123      }
4124  
4125      /**
4126       * Parse a METADATA response (RFC 5464 [4.4]).
4127       *
4128       * @param Horde_Imap_Client_Interaction_Pipeline $pipeline  Pipeline
4129       *                                                          object.
4130       * @param Horde_Imap_Client_Tokenize $data  The server response.
4131       *
4132       * @throws Horde_Imap_Client_Exception
4133       */
4134      protected function _parseMetadata(
4135          Horde_Imap_Client_Interaction_Pipeline $pipeline,
4136          Horde_Imap_Client_Tokenize $data
4137      )
4138      {
4139          // Mailbox name is in UTF7-IMAP.
4140          $mbox = Horde_Imap_Client_Mailbox::get($data->next(), true);
4141  
4142          // Ignore unsolicited responses.
4143          if ($data->next() === true) {
4144              while (($entry = $data->next()) !== false) {
4145                  $pipeline->data['metadata'][strval($mbox)][$entry] = $data->next();
4146              }
4147          }
4148      }
4149  
4150      /* Overriden methods. */
4151  
4152      /**
4153       * @param array $opts  Options:
4154       *   - decrement: (boolean) If true, decrement the message count.
4155       *   - pipeline: (Horde_Imap_Client_Interaction_Pipeline) Pipeline object.
4156       */
4157      protected function _deleteMsgs(Horde_Imap_Client_Mailbox $mailbox,
4158                                     Horde_Imap_Client_Ids $ids,
4159                                     array $opts = array())
4160      {
4161          /* If there are pending FETCH cache writes, we need to write them
4162           * before the UID -> sequence number mapping changes. */
4163          if (isset($opts['pipeline'])) {
4164              $this->_updateCache($opts['pipeline']->fetch);
4165          }
4166  
4167          $res = parent::_deleteMsgs($mailbox, $ids);
4168  
4169          if (isset($this->_temp['expunged'])) {
4170              $this->_temp['expunged']->add($res);
4171          }
4172  
4173          if (!empty($opts['decrement'])) {
4174              $mbox_ob = $this->_mailboxOb();
4175              $mbox_ob->setStatus(
4176                  Horde_Imap_Client::STATUS_MESSAGES,
4177                  $mbox_ob->getStatus(Horde_Imap_Client::STATUS_MESSAGES) - count($ids)
4178              );
4179          }
4180      }
4181  
4182      /* Internal functions. */
4183  
4184      /**
4185       * Return the proper mailbox format object based on the server's
4186       * capabilities.
4187       *
4188       * @param string $mailbox  The mailbox.
4189       * @param boolean $list    Is this object used in a LIST command?
4190       *
4191       * @return Horde_Imap_Client_Data_Format_Mailbox  A mailbox format object.
4192       */
4193      protected function _getMboxFormatOb($mailbox, $list = false)
4194      {
4195          if ($this->_capability()->isEnabled('UTF8=ACCEPT')) {
4196              try {
4197                  return $list
4198                      ? new Horde_Imap_Client_Data_Format_ListMailbox_Utf8($mailbox)
4199                      : new Horde_Imap_Client_Data_Format_Mailbox_Utf8($mailbox);
4200              } catch (Horde_Imap_Client_Data_Format_Exception $e) {}
4201          }
4202  
4203          return $list
4204              ? new Horde_Imap_Client_Data_Format_ListMailbox($mailbox)
4205              : new Horde_Imap_Client_Data_Format_Mailbox($mailbox);
4206      }
4207  
4208      /**
4209       * Sends command(s) to the IMAP server. A connection to the server must
4210       * have already been made.
4211       *
4212       * @param mixed $cmd  Either a Command object or a Pipeline object.
4213       *
4214       * @return Horde_Imap_Client_Interaction_Pipeline  A pipeline object.
4215       * @throws Horde_Imap_Client_Exception
4216       */
4217      protected function _sendCmd($cmd)
4218      {
4219          $pipeline = ($cmd instanceof Horde_Imap_Client_Interaction_Command)
4220              ? $this->_pipeline($cmd)
4221              : $cmd;
4222  
4223          if (!empty($this->_cmdQueue)) {
4224              /* Add commands in reverse order. */
4225              foreach (array_reverse($this->_cmdQueue) as $val) {
4226                  $pipeline->add($val, true);
4227              }
4228  
4229              $this->_cmdQueue = array();
4230          }
4231  
4232          $cmd_list = array();
4233  
4234          foreach ($pipeline as $val) {
4235              if ($val->continuation) {
4236                  $this->_sendCmdChunk($pipeline, $cmd_list);
4237                  $this->_sendCmdChunk($pipeline, array($val));
4238                  $cmd_list = array();
4239              } else {
4240                  $cmd_list[] = $val;
4241              }
4242          }
4243  
4244          $this->_sendCmdChunk($pipeline, $cmd_list);
4245  
4246          /* If any FLAGS responses contain MODSEQs but not UIDs, don't
4247           * cache any data and immediately close the mailbox. */
4248          foreach ($pipeline->data['modseqs_nouid'] as $val) {
4249              if (!$pipeline->fetch[$val]->getUid()) {
4250                  $this->_debug->info(
4251                      'Server provided FLAGS MODSEQ without providing UID.'
4252                  );
4253                  $this->close();
4254                  return $pipeline;
4255              }
4256          }
4257  
4258          /* Update HIGHESTMODSEQ value. */
4259          if (!empty($pipeline->data['modseqs'])) {
4260              $modseq = max($pipeline->data['modseqs']);
4261              $this->_mailboxOb()->setStatus(Horde_Imap_Client::STATUS_HIGHESTMODSEQ, $modseq);
4262              /* CONDSTORE has not yet updated flag information, so don't update
4263               * modseq yet. */
4264              if ($this->_capability()->isEnabled('QRESYNC')) {
4265                  $this->_updateModSeq($modseq);
4266              }
4267          }
4268  
4269          /* Update cache items. */
4270          $this->_updateCache($pipeline->fetch);
4271  
4272          return $pipeline;
4273      }
4274  
4275      /**
4276       * Send a chunk of commands and/or continuation fragments to the server.
4277       *
4278       * @param Horde_Imap_Client_Interaction_Pipeline $pipeline  The pipeline
4279       *                                                          object.
4280       * @param array $chunk  List of commands to send.
4281       *
4282       * @throws Horde_Imap_Client_Exception
4283       */
4284      protected function _sendCmdChunk($pipeline, $chunk)
4285      {
4286          if (empty($chunk)) {
4287              return;
4288          }
4289  
4290          $cmd_count = count($chunk);
4291          $exception = null;
4292  
4293          foreach ($chunk as $val) {
4294              $val->pipeline = $pipeline;
4295  
4296              try {
4297                  if ($this->_processCmd($pipeline, $val, $val)) {
4298                      $this->_connection->write('', true);
4299                  } else {
4300                      $cmd_count = 0;
4301                  }
4302              } catch (Horde_Imap_Client_Exception $e) {
4303                  switch ($e->getCode()) {
4304                  case Horde_Imap_Client_Exception::SERVER_WRITEERROR:
4305                      $this->_temp['logout'] = true;
4306                      $this->logout();
4307                      break;
4308                  }
4309  
4310                  throw $e;
4311              }
4312          }
4313  
4314          while ($cmd_count) {
4315              try {
4316                  if ($this->_getLine($pipeline) instanceof Horde_Imap_Client_Interaction_Server_Tagged) {
4317                      --$cmd_count;
4318                  }
4319              } catch (Horde_Imap_Client_Exception $e) {
4320                  switch ($e->getCode()) {
4321                  case $e::DISCONNECT:
4322                      /* Guaranteed to have no more data incoming, so we can
4323                       * immediately logout. */
4324                      $this->_temp['logout'] = true;
4325                      $this->logout();
4326                      throw $e;
4327                  }
4328  
4329                  /* For all other issues, catch and store exception; don't
4330                   * throw until all input is read since we need to clear
4331                   * incoming queue. (For now, only store first exception.) */
4332                  if (is_null($exception)) {
4333                      $exception = $e;
4334                  }
4335  
4336                  if (($e instanceof Horde_Imap_Client_Exception_ServerResponse) &&
4337                      $e->command) {
4338                      --$cmd_count;
4339                  }
4340              }
4341          }
4342  
4343          if (!is_null($exception)) {
4344              throw $exception;
4345          }
4346      }
4347  
4348      /**
4349       * Process/send a command to the remote server.
4350       *
4351       * @param Horde_Imap_Client_Interaction_Pipeline $pipeline The pipeline
4352       *                                                         object.
4353       * @param Horde_Imap_Client_Interaction_Command $cmd  The master command.
4354       * @param Horde_Imap_Client_Data_Format_List $data    Commands to send.
4355       *
4356       * @return boolean  True if EOL needed to finish command.
4357       * @throws Horde_Imap_Client_Exception
4358       * @throws Horde_Imap_Client_Exception_NoSupport
4359       */
4360      protected function _processCmd($pipeline, $cmd, $data)
4361      {
4362          if ($this->_debug->debug &&
4363              ($data instanceof Horde_Imap_Client_Interaction_Command)) {
4364              $data->startTimer();
4365          }
4366  
4367          foreach ($data as $key => $val) {
4368              if ($val instanceof Horde_Imap_Client_Interaction_Command_Continuation) {
4369                  $this->_connection->write('', true);
4370  
4371                  /* Check for optional continuation responses when the command
4372                   * has already finished. */
4373                  if (!$cmd_continuation = $this->_processCmdContinuation($pipeline, $val->optional)) {
4374                      return false;
4375                  }
4376  
4377                  $this->_processCmd(
4378                      $pipeline,
4379                      $cmd,
4380                      $val->getCommands($cmd_continuation)
4381                  );
4382                  continue;
4383              }
4384  
4385              if (!is_null($debug_msg = array_shift($cmd->debug))) {
4386                  $this->_debug->client(
4387                      (($cmd == $data) ? $cmd->tag . ' ' : '') .  $debug_msg
4388                  );
4389                  $this->_connection->client_debug = false;
4390              }
4391  
4392              if ($key) {
4393                  $this->_connection->write(' ');
4394              }
4395  
4396              if ($val instanceof Horde_Imap_Client_Data_Format_List) {
4397                  $this->_connection->write('(');
4398                  $this->_processCmd($pipeline, $cmd, $val);
4399                  $this->_connection->write(')');
4400              } elseif (($val instanceof Horde_Imap_Client_Data_Format_String) &&
4401                        $val->literal()) {
4402                  $c = $this->_capability();
4403  
4404                  /* RFC 6855: If UTF8 extension is available, quote short
4405                   * strings instead of sending as literal. */
4406                  if ($c->isEnabled('UTF8=ACCEPT') && ($val->length() < 100)) {
4407                      $val->forceQuoted();
4408                      $this->_connection->write($val->escape());
4409                  } else {
4410                      /* RFC 3516/4466: Send literal8 if we have binary data. */
4411                      if ($cmd->literal8 &&
4412                          $val->binary() &&
4413                          ($c->query('BINARY') || $c->isEnabled('UTF8=ACCEPT'))) {
4414                          $binary = true;
4415                          $this->_connection->write('~');
4416                      } else {
4417                          $binary = false;
4418                      }
4419  
4420                      $literal_len = $val->length();
4421                      $this->_connection->write('{' . $literal_len);
4422  
4423                      /* RFC 2088 - If LITERAL+ is available, saves a roundtrip
4424                       * from the server. */
4425                      if ($cmd->literalplus && $c->query('LITERAL+')) {
4426                          $this->_connection->write('+}', true);
4427                      } else {
4428                          $this->_connection->write('}', true);
4429                          $this->_processCmdContinuation($pipeline);
4430                      }
4431  
4432                      if ($debug_msg) {
4433                          $this->_connection->client_debug = false;
4434                      }
4435  
4436                      $this->_connection->writeLiteral(
4437                          $val->getStream(),
4438                          $literal_len,
4439                          $binary
4440                      );
4441                  }
4442              } else {
4443                  $this->_connection->write($val->escape());
4444              }
4445          }
4446  
4447          return true;
4448      }
4449  
4450      /**
4451       * Process a command continuation response.
4452       *
4453       * @param Horde_Imap_Client_Interaction_Pipeline $pipeline  The pipeline
4454       *                                                          object.
4455       * @param boolean $noexception                              Don't throw
4456       *                                                          exception if
4457       *                                                          continuation
4458       *                                                          does not occur.
4459       *
4460       * @return mixed  A Horde_Imap_Client_Interaction_Server_Continuation
4461       *                object or false.
4462       *
4463       * @throws Horde_Imap_Client_Exception
4464       */
4465      protected function _processCmdContinuation($pipeline, $noexception = false)
4466      {
4467          do {
4468              $ob = $this->_getLine($pipeline);
4469          } while ($ob instanceof Horde_Imap_Client_Interaction_Server_Untagged);
4470  
4471          if ($ob instanceof Horde_Imap_Client_Interaction_Server_Continuation) {
4472              return $ob;
4473          } elseif ($noexception) {
4474              return false;
4475          }
4476  
4477          $this->_debug->info(
4478              'ERROR: Unexpected response from server while waiting for a continuation request.'
4479          );
4480          $e = new Horde_Imap_Client_Exception(
4481              Horde_Imap_Client_Translation::r("Error when communicating with the mail server."),
4482              Horde_Imap_Client_Exception::SERVER_READERROR
4483          );
4484          $e->details = strval($ob);
4485  
4486          throw $e;
4487      }
4488  
4489      /**
4490       * Shortcut to creating a new IMAP client command object.
4491       *
4492       * @param string $cmd  The IMAP command.
4493       *
4494       * @return Horde_Imap_Client_Interaction_Command  A command object.
4495       */
4496      protected function _command($cmd)
4497      {
4498          return new Horde_Imap_Client_Interaction_Command($cmd, ++$this->_tag);
4499      }
4500  
4501      /**
4502       * Shortcut to creating a new pipeline object.
4503       *
4504       * @param Horde_Imap_Client_Interaction_Command $cmd  An IMAP command to
4505       *                                                    add.
4506       *
4507       * @return Horde_Imap_Client_Interaction_Pipeline  A pipeline object.
4508       */
4509      protected function _pipeline($cmd = null)
4510      {
4511          if (!isset($this->_temp['fetchob'])) {
4512              $this->_temp['fetchob'] = new Horde_Imap_Client_Fetch_Results(
4513                  $this->_fetchDataClass,
4514                  Horde_Imap_Client_Fetch_Results::SEQUENCE
4515              );
4516          }
4517  
4518          $ob = new Horde_Imap_Client_Interaction_Pipeline(
4519              clone $this->_temp['fetchob']
4520          );
4521  
4522          if (!is_null($cmd)) {
4523              $ob->add($cmd);
4524          }
4525  
4526          return $ob;
4527      }
4528  
4529      /**
4530       * Gets data from the IMAP server stream and parses it.
4531       *
4532       * @param Horde_Imap_Client_Interaction_Pipeline $pipeline  Pipeline
4533       *                                                          object.
4534       *
4535       * @return Horde_Imap_Client_Interaction_Server  Server object.
4536       *
4537       * @throws Horde_Imap_Client_Exception
4538       */
4539      protected function _getLine(
4540          Horde_Imap_Client_Interaction_Pipeline $pipeline
4541      )
4542      {
4543          $server = Horde_Imap_Client_Interaction_Server::create(
4544              $this->_connection->read()
4545          );
4546  
4547          switch (get_class($server)) {
4548          case 'Horde_Imap_Client_Interaction_Server_Continuation':
4549              $this->_responseCode($pipeline, $server);
4550              break;
4551  
4552          case 'Horde_Imap_Client_Interaction_Server_Tagged':
4553              $cmd = $pipeline->complete($server);
4554              if (is_null($cmd)) {
4555                  /* This indicates a "dangling" tagged response - it was either
4556                   * generated by an aborted previous pipeline object or is the
4557                   * result of spurious output by the server. Ignore. */
4558                  return $this->_getLine($pipeline);
4559              }
4560  
4561              if ($timer = $cmd->getTimer()) {
4562                  $this->_debug->info(sprintf(
4563                      'Command %s took %s seconds.',
4564                      $cmd->tag,
4565                      $timer
4566                  ));
4567              }
4568              $this->_responseCode($pipeline, $server);
4569  
4570              if (is_callable($cmd->on_success)) {
4571                  call_user_func($cmd->on_success);
4572              }
4573              break;
4574  
4575          case 'Horde_Imap_Client_Interaction_Server_Untagged':
4576              if (is_null($server->status)) {
4577                  $this->_serverResponse($pipeline, $server);
4578              } else {
4579                  $this->_responseCode($pipeline, $server);
4580              }
4581              break;
4582          }
4583  
4584          switch ($server->status) {
4585          case $server::BAD:
4586          case $server::NO:
4587              /* A tagged BAD response indicates that the tagged command caused
4588               * the error. This information is unknown if untagged (RFC 3501
4589               * [7.1.3]) - ignore these untagged responses.
4590               * An untagged NO response indicates a warning; ignore and assume
4591               * that it also included response text code that is handled
4592               * elsewhere. Throw exception if tagged; command handlers can
4593               * catch this if able to workaround this issue (RFC 3501
4594               * [7.1.2]). */
4595              if ($server instanceof Horde_Imap_Client_Interaction_Server_Tagged) {
4596                  /* Check for a on_error callback. If function returns true,
4597                   * ignore the error. */
4598                  if (($cmd = $pipeline->getCmd($server->tag)) &&
4599                      is_callable($cmd->on_error) &&
4600                      call_user_func($cmd->on_error)) {
4601                      break;
4602                  }
4603  
4604                  throw new Horde_Imap_Client_Exception_ServerResponse(
4605                      Horde_Imap_Client_Translation::r("IMAP error reported by server."),
4606                      0,
4607                      $server,
4608                      $pipeline
4609                  );
4610              }
4611              break;
4612  
4613          case $server::BYE:
4614              /* A BYE response received as part of a logout command should be
4615               * be treated like a regular command: a client MUST process the
4616               * entire command until logging out (RFC 3501 [3.4; 7.1.5]). */
4617              if (empty($this->_temp['logout'])) {
4618                  $e = new Horde_Imap_Client_Exception(
4619                      Horde_Imap_Client_Translation::r("IMAP Server closed the connection."),
4620                      Horde_Imap_Client_Exception::DISCONNECT
4621                  );
4622                  $e->details = strval($server);
4623                  throw $e;
4624              }
4625              break;
4626  
4627          case $server::PREAUTH:
4628              /* The user was pre-authenticated. (RFC 3501 [7.1.4]) */
4629              $this->_temp['preauth'] = true;
4630              break;
4631          }
4632  
4633          return $server;
4634      }
4635  
4636      /**
4637       * Handle untagged server responses (see RFC 3501 [2.2.2]).
4638       *
4639       * @param Horde_Imap_Client_Interaction_Pipeline $pipeline  Pipeline
4640       *                                                          object.
4641       * @param Horde_Imap_Client_Interaction_Server $ob          Server
4642       *                                                          response.
4643       */
4644      protected function _serverResponse(
4645          Horde_Imap_Client_Interaction_Pipeline $pipeline,
4646          Horde_Imap_Client_Interaction_Server $ob
4647      )
4648      {
4649          $token = $ob->token;
4650  
4651          /* First, catch untagged responses where the name appears first on the
4652           * line. */
4653          switch ($first = Horde_String::upper($token->current())) {
4654          case 'CAPABILITY':
4655              $this->_parseCapability($pipeline, $token->flushIterator());
4656              break;
4657  
4658          case 'LIST':
4659          case 'LSUB':
4660              $this->_parseList($pipeline, $token);
4661              break;
4662  
4663          case 'STATUS':
4664              // Parse a STATUS response (RFC 3501 [7.2.4]).
4665              $this->_parseStatus($token);
4666              break;
4667  
4668          case 'SEARCH':
4669          case 'SORT':
4670              // Parse a SEARCH/SORT response (RFC 3501 [7.2.5] & RFC 5256 [4]).
4671              $this->_parseSearch($pipeline, $token->flushIterator());
4672              break;
4673  
4674          case 'ESEARCH':
4675              // Parse an ESEARCH response (RFC 4466 [2.6.2]).
4676              $this->_parseEsearch($pipeline, $token);
4677              break;
4678  
4679          case 'FLAGS':
4680              $token->next();
4681              $this->_mailboxOb()->setStatus(Horde_Imap_Client::STATUS_FLAGS, array_map('Horde_String::lower', $token->flushIterator()));
4682              break;
4683  
4684          case 'QUOTA':
4685              $this->_parseQuota($pipeline, $token);
4686              break;
4687  
4688          case 'QUOTAROOT':
4689              // Ignore this line - we can get this information from
4690              // the untagged QUOTA responses.
4691              break;
4692  
4693          case 'NAMESPACE':
4694              $this->_parseNamespace($pipeline, $token);
4695              break;
4696  
4697          case 'THREAD':
4698              $this->_parseThread($pipeline, $token);
4699              break;
4700  
4701          case 'ACL':
4702              $this->_parseACL($pipeline, $token);
4703              break;
4704  
4705          case 'LISTRIGHTS':
4706              $this->_parseListRights($pipeline, $token);
4707              break;
4708  
4709          case 'MYRIGHTS':
4710              $this->_parseMyRights($pipeline, $token);
4711              break;
4712  
4713          case 'ID':
4714              // ID extension (RFC 2971)
4715              $this->_parseID($pipeline, $token);
4716              break;
4717  
4718          case 'ENABLED':
4719              // ENABLE extension (RFC 5161)
4720              $this->_parseEnabled($token);
4721              break;
4722  
4723          case 'LANGUAGE':
4724              // LANGUAGE extension (RFC 5255 [3.2])
4725              $this->_parseLanguage($token);
4726              break;
4727  
4728          case 'COMPARATOR':
4729              // I18NLEVEL=2 extension (RFC 5255 [4.7])
4730              $this->_parseComparator($pipeline, $token);
4731              break;
4732  
4733          case 'VANISHED':
4734              // QRESYNC extension (RFC 7162 [3.2.10])
4735              $this->_parseVanished($pipeline, $token);
4736              break;
4737  
4738          case 'ANNOTATION':
4739              // Parse an ANNOTATION response.
4740              $this->_parseAnnotation($pipeline, $token);
4741              break;
4742  
4743          case 'METADATA':
4744              // Parse a METADATA response.
4745              $this->_parseMetadata($pipeline, $token);
4746              break;
4747  
4748          default:
4749              // Next, look for responses where the keywords occur second.
4750              switch (Horde_String::upper($token->next())) {
4751              case 'EXISTS':
4752                  // EXISTS response - RFC 3501 [7.3.2]
4753                  $mbox_ob = $this->_mailboxOb();
4754  
4755                  // Increment UIDNEXT if it is set.
4756                  if ($mbox_ob->open &&
4757                      ($uidnext = $mbox_ob->getStatus(Horde_Imap_Client::STATUS_UIDNEXT))) {
4758                      $mbox_ob->setStatus(Horde_Imap_Client::STATUS_UIDNEXT, $uidnext + $first - $mbox_ob->getStatus(Horde_Imap_Client::STATUS_MESSAGES));
4759                  }
4760  
4761                  $mbox_ob->setStatus(Horde_Imap_Client::STATUS_MESSAGES, $first);
4762                  break;
4763  
4764              case 'RECENT':
4765                  // RECENT response - RFC 3501 [7.3.1]
4766                  $this->_mailboxOb()->setStatus(Horde_Imap_Client::STATUS_RECENT, $first);
4767                  break;
4768  
4769              case 'EXPUNGE':
4770                  // EXPUNGE response - RFC 3501 [7.4.1]
4771                  $this->_deleteMsgs($this->_selected, $this->getIdsOb($first, true), array(
4772                      'decrement' => true,
4773                      'pipeline' => $pipeline
4774                  ));
4775                  $pipeline->data['expunge_seen'] = true;
4776                  break;
4777  
4778              case 'FETCH':
4779                  // FETCH response - RFC 3501 [7.4.2]
4780                  $this->_parseFetch($pipeline, $first, $token);
4781                  break;
4782              }
4783              break;
4784          }
4785      }
4786  
4787      /**
4788       * Handle status responses (see RFC 3501 [7.1]).
4789       *
4790       * @param Horde_Imap_Client_Interaction_Pipeline $pipeline  Pipeline
4791       *                                                          object.
4792       * @param Horde_Imap_Client_Interaction_Server $ob          Server object.
4793       *
4794       * @throws Horde_Imap_Client_Exception_ServerResponse
4795       */
4796      protected function _responseCode(
4797          Horde_Imap_Client_Interaction_Pipeline $pipeline,
4798          Horde_Imap_Client_Interaction_Server $ob
4799      )
4800      {
4801          if (is_null($ob->responseCode)) {
4802              return;
4803          }
4804  
4805          $rc = $ob->responseCode;
4806  
4807          switch ($rc->code) {
4808          case 'ALERT':
4809          // Defined by RFC 5530 [3] - Treat as an alert for now.
4810          case 'CONTACTADMIN':
4811          // Used by Gmail - Treat as an alert for now.
4812          // http://mailman13.u.washington.edu/pipermail/imap-protocol/2014-September/002324.html
4813          case 'WEBALERT':
4814              $this->_alerts->add(strval($ob->token), $rc->code);
4815              break;
4816  
4817          case 'BADCHARSET':
4818              /* Store valid search charsets if returned by server. */
4819              $s = $this->search_charset;
4820              foreach ($rc->data[0] as $val) {
4821                  $s->setValid($val, true);
4822              }
4823  
4824              throw new Horde_Imap_Client_Exception_ServerResponse(
4825                  Horde_Imap_Client_Translation::r("Charset used in search query is not supported on the mail server."),
4826                  Horde_Imap_Client_Exception::BADCHARSET,
4827                  $ob,
4828                  $pipeline
4829              );
4830  
4831          case 'CAPABILITY':
4832              $this->_parseCapability($pipeline, $rc->data);
4833              break;
4834  
4835          case 'PARSE':
4836              /* Only throw error on NO/BAD. Message is human readable. */
4837              switch ($ob->status) {
4838              case Horde_Imap_Client_Interaction_Server::BAD:
4839              case Horde_Imap_Client_Interaction_Server::NO:
4840                  $e = new Horde_Imap_Client_Exception_ServerResponse(
4841                      Horde_Imap_Client_Translation::r("The mail server was unable to parse the contents of the mail message: %s"),
4842                      Horde_Imap_Client_Exception::PARSEERROR,
4843                      $ob,
4844                      $pipeline
4845                  );
4846                  $e->messagePrintf(array(strval($ob->token)));
4847                  throw $e;
4848              }
4849              break;
4850  
4851          case 'READ-ONLY':
4852              $this->_mode = Horde_Imap_Client::OPEN_READONLY;
4853              break;
4854  
4855          case 'READ-WRITE':
4856              $this->_mode = Horde_Imap_Client::OPEN_READWRITE;
4857              break;
4858  
4859          case 'TRYCREATE':
4860              // RFC 3501 [7.1]
4861              $pipeline->data['trycreate'] = true;
4862              break;
4863  
4864          case 'PERMANENTFLAGS':
4865              $this->_mailboxOb()->setStatus(Horde_Imap_Client::STATUS_PERMFLAGS, array_map('Horde_String::lower', $rc->data[0]));
4866              break;
4867  
4868          case 'UIDNEXT':
4869              $this->_mailboxOb()->setStatus(Horde_Imap_Client::STATUS_UIDNEXT, $rc->data[0]);
4870              break;
4871  
4872          case 'UIDVALIDITY':
4873              $this->_mailboxOb()->setStatus(Horde_Imap_Client::STATUS_UIDVALIDITY, $rc->data[0]);
4874              break;
4875  
4876          case 'UNSEEN':
4877              /* This is different from the STATUS UNSEEN response - this item,
4878               * if defined, returns the first UNSEEN message in the mailbox. */
4879              $this->_mailboxOb()->setStatus(Horde_Imap_Client::STATUS_FIRSTUNSEEN, $rc->data[0]);
4880              break;
4881  
4882          case 'REFERRAL':
4883              // Defined by RFC 2221
4884              $pipeline->data['referral'] = new Horde_Imap_Client_Url_Imap($rc->data[0]);
4885              break;
4886  
4887          case 'UNKNOWN-CTE':
4888              // Defined by RFC 3516
4889              throw new Horde_Imap_Client_Exception_ServerResponse(
4890                  Horde_Imap_Client_Translation::r("The mail server was unable to parse the contents of the mail message."),
4891                  Horde_Imap_Client_Exception::UNKNOWNCTE,
4892                  $ob,
4893                  $pipeline
4894              );
4895  
4896          case 'APPENDUID':
4897              // Defined by RFC 4315
4898              // APPENDUID: [0] = UIDVALIDITY, [1] = UID(s)
4899              $pipeline->data['appenduid'] = $this->getIdsOb($rc->data[1]);
4900              break;
4901  
4902          case 'COPYUID':
4903              // Defined by RFC 4315
4904              // COPYUID: [0] = UIDVALIDITY, [1] = UIDFROM, [2] = UIDTO
4905              $pipeline->data['copyuid'] = array_combine(
4906                  $this->getIdsOb($rc->data[1])->ids,
4907                  $this->getIdsOb($rc->data[2])->ids
4908              );
4909  
4910              /* Use UIDPLUS information to move cached data to new mailbox (see
4911               * RFC 4549 [4.2.2.1]). Need to move now, because a MOVE might
4912               * EXPUNGE immediately afterwards. */
4913              $this->_moveCache($pipeline->data['copydest'], $pipeline->data['copyuid'], $rc->data[0]);
4914              break;
4915  
4916          case 'UIDNOTSTICKY':
4917              // Defined by RFC 4315 [3]
4918              $this->_mailboxOb()->setStatus(Horde_Imap_Client::STATUS_UIDNOTSTICKY, true);
4919              break;
4920  
4921          case 'BADURL':
4922              // Defined by RFC 4469 [4.1]
4923              throw new Horde_Imap_Client_Exception_ServerResponse(
4924                  Horde_Imap_Client_Translation::r("Could not save message on server."),
4925                  Horde_Imap_Client_Exception::CATENATE_BADURL,
4926                  $ob,
4927                  $pipeline
4928              );
4929  
4930          case 'TOOBIG':
4931              // Defined by RFC 4469 [4.2]
4932              throw new Horde_Imap_Client_Exception_ServerResponse(
4933                  Horde_Imap_Client_Translation::r("Could not save message data because it is too large."),
4934                  Horde_Imap_Client_Exception::CATENATE_TOOBIG,
4935                  $ob,
4936                  $pipeline
4937              );
4938  
4939          case 'HIGHESTMODSEQ':
4940              // Defined by RFC 7162 [3.1.2.1]
4941              $pipeline->data['modseqs'][] = $rc->data[0];
4942              break;
4943  
4944          case 'NOMODSEQ':
4945              // Defined by RFC 7162 [3.1.2.2]
4946              $pipeline->data['modseqs'][] = 0;
4947              break;
4948  
4949          case 'MODIFIED':
4950              // Defined by RFC 7162 [3.1.3]
4951              $pipeline->data['modified']->add($rc->data[0]);
4952              break;
4953  
4954          case 'CLOSED':
4955              // Defined by RFC 7162 [3.2.11]
4956              if (isset($pipeline->data['qresyncmbox'])) {
4957                  /* If there is any pending FETCH cache entries, flush them
4958                   * now before changing mailboxes. */
4959                  $this->_updateCache($pipeline->fetch);
4960                  $pipeline->fetch->clear();
4961  
4962                  $this->_changeSelected(
4963                      $pipeline->data['qresyncmbox'][0],
4964                      $pipeline->data['qresyncmbox'][1]
4965                  );
4966                  unset($pipeline->data['qresyncmbox']);
4967              }
4968              break;
4969  
4970          case 'NOTSAVED':
4971              // Defined by RFC 5182 [2.5]
4972              $pipeline->data['searchnotsaved'] = true;
4973              break;
4974  
4975          case 'BADCOMPARATOR':
4976              // Defined by RFC 5255 [4.9]
4977              throw new Horde_Imap_Client_Exception_ServerResponse(
4978                  Horde_Imap_Client_Translation::r("The comparison algorithm was not recognized by the server."),
4979                  Horde_Imap_Client_Exception::BADCOMPARATOR,
4980                  $ob,
4981                  $pipeline
4982              );
4983  
4984          case 'METADATA':
4985              $md = $rc->data[0];
4986  
4987              switch ($md[0]) {
4988              case 'LONGENTRIES':
4989                  // Defined by RFC 5464 [4.2.1]
4990                  $pipeline->data['metadata']['*longentries'] = intval($md[1]);
4991                  break;
4992  
4993              case 'MAXSIZE':
4994                  // Defined by RFC 5464 [4.3]
4995                  throw new Horde_Imap_Client_Exception_ServerResponse(
4996                      Horde_Imap_Client_Translation::r("The metadata item could not be saved because it is too large."),
4997                      Horde_Imap_Client_Exception::METADATA_MAXSIZE,
4998                      $ob,
4999                      $pipeline
5000                  );
5001  
5002              case 'NOPRIVATE':
5003                  // Defined by RFC 5464 [4.3]
5004                  throw new Horde_Imap_Client_Exception_ServerResponse(
5005                      Horde_Imap_Client_Translation::r("The metadata item could not be saved because the server does not support private annotations."),
5006                      Horde_Imap_Client_Exception::METADATA_NOPRIVATE,
5007                      $ob,
5008                      $pipeline
5009                  );
5010  
5011              case 'TOOMANY':
5012                  // Defined by RFC 5464 [4.3]
5013                  throw new Horde_Imap_Client_Exception_ServerResponse(
5014                      Horde_Imap_Client_Translation::r("The metadata item could not be saved because the maximum number of annotations has been exceeded."),
5015                      Horde_Imap_Client_Exception::METADATA_TOOMANY,
5016                      $ob,
5017                      $pipeline
5018                  );
5019              }
5020              break;
5021  
5022          case 'UNAVAILABLE':
5023              // Defined by RFC 5530 [3]
5024              $pipeline->data['loginerr'] = new Horde_Imap_Client_Exception(
5025                  Horde_Imap_Client_Translation::r("Remote server is temporarily unavailable."),
5026                  Horde_Imap_Client_Exception::LOGIN_UNAVAILABLE
5027              );
5028              break;
5029  
5030          case 'AUTHENTICATIONFAILED':
5031              // Defined by RFC 5530 [3]
5032              $pipeline->data['loginerr'] = new Horde_Imap_Client_Exception(
5033                  Horde_Imap_Client_Translation::r("Authentication failed."),
5034                  Horde_Imap_Client_Exception::LOGIN_AUTHENTICATIONFAILED
5035              );
5036              break;
5037  
5038          case 'AUTHORIZATIONFAILED':
5039              // Defined by RFC 5530 [3]
5040              $pipeline->data['loginerr'] = new Horde_Imap_Client_Exception(
5041                  Horde_Imap_Client_Translation::r("Authentication was successful, but authorization failed."),
5042                  Horde_Imap_Client_Exception::LOGIN_AUTHORIZATIONFAILED
5043              );
5044              break;
5045  
5046          case 'EXPIRED':
5047              // Defined by RFC 5530 [3]
5048              $pipeline->data['loginerr'] = new Horde_Imap_Client_Exception(
5049                  Horde_Imap_Client_Translation::r("Authentication credentials have expired."),
5050                  Horde_Imap_Client_Exception::LOGIN_EXPIRED
5051              );
5052              break;
5053  
5054          case 'PRIVACYREQUIRED':
5055              // Defined by RFC 5530 [3]
5056              $pipeline->data['loginerr'] = new Horde_Imap_Client_Exception(
5057                  Horde_Imap_Client_Translation::r("Operation failed due to a lack of a secure connection."),
5058                  Horde_Imap_Client_Exception::LOGIN_PRIVACYREQUIRED
5059              );
5060              break;
5061  
5062          case 'NOPERM':
5063              // Defined by RFC 5530 [3]
5064              throw new Horde_Imap_Client_Exception_ServerResponse(
5065                  Horde_Imap_Client_Translation::r("You do not have adequate permissions to carry out this operation."),
5066                  Horde_Imap_Client_Exception::NOPERM,
5067                  $ob,
5068                  $pipeline
5069              );
5070  
5071          case 'INUSE':
5072              // Defined by RFC 5530 [3]
5073              throw new Horde_Imap_Client_Exception_ServerResponse(
5074                  Horde_Imap_Client_Translation::r("There was a temporary issue when attempting this operation. Please try again later."),
5075                  Horde_Imap_Client_Exception::INUSE,
5076                  $ob,
5077                  $pipeline
5078              );
5079  
5080          case 'EXPUNGEISSUED':
5081              // Defined by RFC 5530 [3]
5082              $pipeline->data['expungeissued'] = true;
5083              break;
5084  
5085          case 'CORRUPTION':
5086              // Defined by RFC 5530 [3]
5087              throw new Horde_Imap_Client_Exception_ServerResponse(
5088                  Horde_Imap_Client_Translation::r("The mail server is reporting corrupt data in your mailbox."),
5089                  Horde_Imap_Client_Exception::CORRUPTION,
5090                  $ob,
5091                  $pipeline
5092              );
5093  
5094          case 'SERVERBUG':
5095          case 'CLIENTBUG':
5096          case 'CANNOT':
5097              // Defined by RFC 5530 [3]
5098              $this->_debug->info(
5099                  'ERROR: mail server explicitly reporting an error.'
5100              );
5101              break;
5102  
5103          case 'LIMIT':
5104              // Defined by RFC 5530 [3]
5105              throw new Horde_Imap_Client_Exception_ServerResponse(
5106                  Horde_Imap_Client_Translation::r("The mail server has denied the request."),
5107                  Horde_Imap_Client_Exception::LIMIT,
5108                  $ob,
5109                  $pipeline
5110              );
5111  
5112          case 'OVERQUOTA':
5113              // Defined by RFC 5530 [3]
5114              throw new Horde_Imap_Client_Exception_ServerResponse(
5115                  Horde_Imap_Client_Translation::r("The operation failed because the quota has been exceeded on the mail server."),
5116                  Horde_Imap_Client_Exception::OVERQUOTA,
5117                  $ob,
5118                  $pipeline
5119              );
5120  
5121          case 'ALREADYEXISTS':
5122              // Defined by RFC 5530 [3]
5123              throw new Horde_Imap_Client_Exception_ServerResponse(
5124                  Horde_Imap_Client_Translation::r("The object could not be created because it already exists."),
5125                  Horde_Imap_Client_Exception::ALREADYEXISTS,
5126                  $ob,
5127                  $pipeline
5128              );
5129  
5130          case 'NONEXISTENT':
5131              // Defined by RFC 5530 [3]
5132              throw new Horde_Imap_Client_Exception_ServerResponse(
5133                  Horde_Imap_Client_Translation::r("The object could not be deleted because it does not exist."),
5134                  Horde_Imap_Client_Exception::NONEXISTENT,
5135                  $ob,
5136                  $pipeline
5137              );
5138  
5139          case 'USEATTR':
5140              // Defined by RFC 6154 [3]
5141              throw new Horde_Imap_Client_Exception_ServerResponse(
5142                  Horde_Imap_Client_Translation::r("The special-use attribute requested for the mailbox is not supported."),
5143                  Horde_Imap_Client_Exception::USEATTR,
5144                  $ob,
5145                  $pipeline
5146              );
5147  
5148          case 'DOWNGRADED':
5149              // Defined by RFC 6858 [3]
5150              $downgraded = $this->getIdsOb($rc->data[0]);
5151              foreach ($pipeline->fetch as $val) {
5152                  if (in_array($val->getUid(), $downgraded)) {
5153                      $val->setDowngraded(true);
5154                  }
5155              }
5156              break;
5157  
5158          case 'XPROXYREUSE':
5159              // The proxy connection was reused, so no need to do login tasks.
5160              $pipeline->data['proxyreuse'] = true;
5161              break;
5162  
5163          default:
5164              // Unknown response codes SHOULD be ignored - RFC 3501 [7.1]
5165              break;
5166          }
5167      }
5168  
5169  }