Search moodle.org's
Developer Documentation

  • Bug fixes for general core bugs in 3.11.x will end 9 May 2022 (12 months).
  • Bug fixes for security issues in 3.11.x will end 14 November 2022 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.
  • Differences Between: [Versions 310 and 311] [Versions 35 and 311] [Versions 36 and 311] [Versions 37 and 311] [Versions 38 and 311] [Versions 39 and 311]

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