Overview

Packages

  • Imap
    • Client

Classes

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