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 abstracted API interface to IMAP backends supporting the IMAP4rev1
   4:  * protocol (RFC 3501).
   5:  *
   6:  * Copyright 2008-2012 Horde LLC (http://www.horde.org/)
   7:  *
   8:  * See the enclosed file COPYING for license information (LGPL). If you
   9:  * did not receive this file, see http://www.horde.org/licenses/lgpl21.
  10:  *
  11:  * @author   Michael Slusarz <slusarz@horde.org>
  12:  * @category Horde
  13:  * @license  http://www.horde.org/licenses/lgpl21 LGPL 2.1
  14:  * @package  Imap_Client
  15:  *
  16:  * @property Horde_Imap_Client_Utils $utils  A Utils object.
  17:  */
  18: abstract class Horde_Imap_Client_Base implements Serializable
  19: {
  20:     /* Serialized version. */
  21:     const VERSION = 1;
  22: 
  23:     /* Cache names for miscellaneous data. */
  24:     const CACHE_MODSEQ = 'HICmodseq';
  25:     const CACHE_SEARCH = 'HICsearch';
  26: 
  27:     /**
  28:      * The Horde_Imap_Client_Cache object.
  29:      *
  30:      * @var Horde_Imap_Client_Cache
  31:      */
  32:     public $cache = null;
  33: 
  34:     /**
  35:      * The list of fetch fields that can be cached, and their cache names.
  36:      *
  37:      * @var array
  38:      */
  39:     public $cacheFields = array(
  40:         Horde_Imap_Client::FETCH_ENVELOPE => 'HICenv',
  41:         Horde_Imap_Client::FETCH_FLAGS => 'HICflags',
  42:         Horde_Imap_Client::FETCH_HEADERS => 'HIChdrs',
  43:         Horde_Imap_Client::FETCH_IMAPDATE => 'HICdate',
  44:         Horde_Imap_Client::FETCH_SIZE => 'HICsize',
  45:         Horde_Imap_Client::FETCH_STRUCTURE => 'HICstruct'
  46:     );
  47: 
  48:     /**
  49:      * Has the internal configuration changed?
  50:      *
  51:      * @var boolean
  52:      */
  53:     public $changed = false;
  54: 
  55:     /**
  56:      * The debug stream.
  57:      *
  58:      * @var resource
  59:      */
  60:     protected $_debug = null;
  61: 
  62:     /**
  63:      * The fetch data object type to return.
  64:      *
  65:      * @var string
  66:      */
  67:     protected $_fetchDataClass = 'Horde_Imap_Client_Data_Fetch';
  68: 
  69:     /**
  70:      * Cached server data.
  71:      *
  72:      * @var array
  73:      */
  74:     protected $_init;
  75: 
  76:     /**
  77:      * Is there an active authenticated connection to the IMAP Server?
  78:      *
  79:      * @var boolean
  80:      */
  81:     protected $_isAuthenticated = false;
  82: 
  83:     /**
  84:      * Is there a secure connection to the IMAP Server?
  85:      *
  86:      * @var boolean
  87:      */
  88:     protected $_isSecure = false;
  89: 
  90:     /**
  91:      * The current mailbox selection mode.
  92:      *
  93:      * @var integer
  94:      */
  95:     protected $_mode = 0;
  96: 
  97:     /**
  98:      * Hash containing connection parameters.
  99:      * This hash never changes.
 100:      *
 101:      * @var array
 102:      */
 103:     protected $_params = array();
 104: 
 105:     /**
 106:      * The currently selected mailbox.
 107:      *
 108:      * @var Horde_Imap_Client_Mailbox
 109:      */
 110:     protected $_selected = null;
 111: 
 112:     /**
 113:      * Temp array (destroyed at end of process).
 114:      *
 115:      * @var array
 116:      */
 117:     protected $_temp = array();
 118: 
 119:     /**
 120:      * The Horde_Imap_Client_Utils object.
 121:      *
 122:      * @var Horde_Imap_Client_Utils
 123:      */
 124:     protected $_utils;
 125: 
 126:     /**
 127:      * The utils class to use.
 128:      *
 129:      * @var string
 130:      */
 131:     protected $_utilsClass = 'Horde_Imap_Client_Utils';
 132: 
 133:     /**
 134:      * Constructor.
 135:      *
 136:      * @see Horde_Imap_Client::factory()
 137:      *
 138:      * @param array $params  A hash containing configuration parameters.
 139:      *                       See Horde_Imap_Client::factory().
 140:      */
 141:     public function __construct(array $params = array())
 142:     {
 143:         if (!isset($params['username']) || !isset($params['password'])) {
 144:             throw new InvalidArgumentException('Horde_Imap_Client requires a username and password.');
 145:         }
 146: 
 147:         $this->_setInit();
 148: 
 149:         // Default values.
 150:         $params = array_merge(array(
 151:             'encryptKey' => null,
 152:             'hostspec' => 'localhost',
 153:             'log' => null,
 154:             'port' => ((isset($params['secure']) && ($params['secure'] == 'ssl')) ? 993 : 143),
 155:             'secure' => false,
 156:             'timeout' => 30
 157:         ), array_filter($params));
 158: 
 159:         if (empty($params['cache'])) {
 160:             $params['cache'] = array('fields' => array());
 161:         } elseif (empty($params['cache']['fields'])) {
 162:             $params['cache']['fields'] = $this->cacheFields;
 163:         } else {
 164:             $params['cache']['fields'] = array_flip($params['cache']['fields']);
 165:         }
 166: 
 167:         if (empty($params['cache']['fetch_ignore'])) {
 168:             $params['cache']['fetch_ignore'] = array();
 169:         }
 170: 
 171:         $this->_params = $params;
 172:         $this->setParam('password', $this->_params['password']);
 173: 
 174:         $this->changed = true;
 175:         $this->_initOb();
 176:     }
 177: 
 178:     /**
 179:      * Get encryption key.
 180:      *
 181:      * @return string  The encryption key.
 182:      */
 183:     protected function _getEncryptKey()
 184:     {
 185:         if (is_callable($this->_params['encryptKey'])) {
 186:             return call_user_func($this->_params['encryptKey']);
 187:         }
 188: 
 189:         throw new InvalidArgumentException('encryptKey parameter is not a valid callback.');
 190:     }
 191: 
 192:     /**
 193:      * Exception wrapper - logs an error message before (optionally) throwing
 194:      * exception.
 195:      *
 196:      * Server debug information, if present, will be stored in the 'details'
 197:      * property of the exception object.
 198:      *
 199:      * @param mixed $msg            Error message/error object. If an array,
 200:      *                              the first entry is used as the exception
 201:      *                              message and the second entry is taken
 202:      *                              to be server debug information.
 203:      * @param integer|string $code  Error code. If string, will convert from
 204:      *                              the Exception constant of the same name.
 205:      *                              If 'NO_SUPPORT', throws a non-supported
 206:      *                              extension exception.
 207:      * @param boolean $logonly      If true, log only and don't throw
 208:      *                              exception.
 209:      *
 210:      * @throws Horde_Imap_Client_Exception
 211:      * @throws Horde_Imap_Client_Exception_NoSupportExtension
 212:      */
 213:     protected function _exception($msg, $code = 0, $logonly = false)
 214:     {
 215:         if (is_array($msg)) {
 216:             $details = $msg[1];
 217:             $msg = $msg[0];
 218:         } else {
 219:             $details = null;
 220:         }
 221: 
 222:         if (is_integer($code)) {
 223:             $e = new Horde_Imap_Client_Exception($msg, $code);
 224:         } elseif ($code == 'NO_SUPPORT') {
 225:             $e = new Horde_Imap_Client_Exception_NoSupportExtension($msg);
 226:         } else {
 227:             $e = new Horde_Imap_Client_Exception($msg, constant('Horde_Imap_Client_Exception::' . $code));
 228:         }
 229: 
 230:         if (!is_null($details)) {
 231:             $e->details = $details;
 232:         }
 233: 
 234:         if (is_callable($this->_params['log'])) {
 235:             call_user_func($this->_params['log'], $e);
 236:         }
 237: 
 238:         if (!$logonly) {
 239:             throw $e;
 240:         }
 241:     }
 242: 
 243:     /**
 244:      * Do initialization tasks.
 245:      */
 246:     protected function _initOb()
 247:     {
 248:         if (!empty($this->_params['debug'])) {
 249:             if (is_resource($this->_params['debug'])) {
 250:                 $this->_debug = $this->_params['debug'];
 251:             } else {
 252:                 $this->_debug = @fopen($this->_params['debug'], 'a');
 253:             }
 254:         }
 255:     }
 256: 
 257:     /**
 258:      * Destructor.
 259:      */
 260:     public function __destruct()
 261:     {
 262:         $this->logout();
 263: 
 264:         /* Close debugging output. */
 265:         if (is_resource($this->_debug)) {
 266:             fflush($this->_debug);
 267:             fclose($this->_debug);
 268:             $this->_debug = null;
 269:         }
 270:     }
 271: 
 272:     /**
 273:      */
 274:     public function serialize()
 275:     {
 276:         return serialize(array(
 277:             'i' => $this->_init,
 278:             'p' => $this->_params,
 279:             'v' => self::VERSION
 280:         ));
 281:     }
 282: 
 283:     /**
 284:      */
 285:     public function unserialize($data)
 286:     {
 287:         $data = @unserialize($data);
 288:         if (!is_array($data) ||
 289:             !isset($data['v']) ||
 290:             ($data['v'] != self::VERSION)) {
 291:             throw new Exception('Cache version change');
 292:         }
 293: 
 294:         $this->_init = $data['i'];
 295:         $this->_params = $data['p'];
 296: 
 297:         $this->_initOb();
 298:     }
 299: 
 300:     /**
 301:      */
 302:     public function __get($name)
 303:     {
 304:         switch ($name) {
 305:         case 'utils':
 306:             if (!isset($this->_utils)) {
 307:                 $this->_utils = new $this->_utilsClass();
 308:             }
 309:             return $this->_utils;
 310:         }
 311:     }
 312: 
 313:     /**
 314:      * Set an initialization value.
 315:      *
 316:      * @param string $key  The initialization key. If null, resets all keys.
 317:      * @param mixed $val   The cached value. If null, removes the key.
 318:      */
 319:     public function _setInit($key = null, $val = null)
 320:     {
 321:         if (is_null($key)) {
 322:             $this->_init = array(
 323:                 'enabled' => array(),
 324:                 'namespace' => array(),
 325:                 's_charset' => array()
 326:             );
 327:         } elseif (is_null($val)) {
 328:             unset($this->_init[$key]);
 329:         } else {
 330:             switch ($key) {
 331:             case 'capability':
 332:                 if (!empty($this->_params['capability_ignore'])) {
 333:                     if ($this->_debug &&
 334:                         ($ignored = array_intersect_key($val, array_flip($this->_params['capability_ignore'])))) {
 335:                         $this->writeDebug(sprintf("IGNORING these IMAP capabilities: %s\n", implode(', ', array_keys($ignored))), Horde_Imap_Client::DEBUG_INFO);
 336:                     }
 337:                     $val = array_diff_key($val, array_flip($this->_params['capability_ignore']));
 338:                 }
 339:                 break;
 340:             }
 341: 
 342:             $this->_init[$key] = $val;
 343:         }
 344:         $this->changed = true;
 345:     }
 346: 
 347:     /**
 348:      * Initialize the Horde_Imap_Client_Cache object, if necessary.
 349:      *
 350:      * @param boolean $current  If true, we are going to update the currently
 351:      *                          selected mailbox. Add an additional check to
 352:      *                          see if caching is available in current
 353:      *                          mailbox.
 354:      *
 355:      * @return boolean  Returns true if caching is enabled.
 356:      */
 357:     protected function _initCache($current = false)
 358:     {
 359:         if (empty($this->_params['cache']['fields']) ||
 360:             !empty($this->_temp['nocache'])) {
 361:             return false;
 362:         }
 363: 
 364:         if (is_null($this->cache)) {
 365:             try {
 366:                 $this->cache = new Horde_Imap_Client_Cache(array_merge($this->getParam('cache'), array(
 367:                     'baseob' => $this,
 368:                     'debug' => (bool)$this->_debug
 369:                 )));
 370:             } catch (InvalidArgumentException $e) {
 371:                 return false;
 372:             }
 373:         }
 374: 
 375:         if (!$current) {
 376:             return true;
 377:         }
 378: 
 379:         /* If UIDs are labeled as not sticky, don't cache since UIDs will
 380:          * change on every access. */
 381:         $status = $this->status($this->_selected, Horde_Imap_Client::STATUS_UIDNOTSTICKY);
 382:         return !$status['uidnotsticky'];
 383:     }
 384: 
 385:     /**
 386:      * Update the list of ignored mailboxes for caching FETCH data.
 387:      *
 388:      * @param array $mboxes  The list of mailboxes to ignore.
 389:      */
 390:     public function fetchCacheIgnore(array $mboxes)
 391:     {
 392:         $this->_params['cache']['fetch_ignore'] = $mboxes;
 393:         $this->changed = true;
 394:     }
 395: 
 396:     /**
 397:      * Returns a value from the internal params array.
 398:      *
 399:      * @param string $key  The param key.
 400:      *
 401:      * @return mixed  The param value, or null if not found.
 402:      */
 403:     public function getParam($key)
 404:     {
 405:         /* Passwords may be stored encrypted. */
 406:         if (($key == 'password') && !empty($this->_params['_passencrypt'])) {
 407:             try {
 408:                 $secret = new Horde_Secret();
 409:                 return $secret->read($this->_getEncryptKey(), $this->_params['password']);
 410:             } catch (Exception $e) {
 411:                 return null;
 412:             }
 413:         }
 414: 
 415:         return isset($this->_params[$key])
 416:             ? $this->_params[$key]
 417:             : null;
 418:     }
 419: 
 420:     /**
 421:      * Sets a configuration parameter value.
 422:      *
 423:      * @since 1.5.0
 424:      *
 425:      * @param string $key   The param key.
 426:      * @param mixed $value  The param value.
 427:      */
 428:     public function setParam($key, $val)
 429:     {
 430:         switch ($key) {
 431:         case 'password':
 432:             // Encrypt password.
 433:             try {
 434:                 $encrypt_key = $this->_getEncryptKey();
 435:                 if (strlen($encrypt_key)) {
 436:                     $secret = new Horde_Secret();
 437:                     $val = $secret->write($encrypt_key, $val);
 438:                     $this->_params['_passencrypt'] = true;
 439:                 }
 440:             } catch (Exception $e) {
 441:                 $this->_params['_passencrypt'] = false;
 442:             }
 443:             break;
 444:         }
 445: 
 446:         $this->_params[$key] = $val;
 447:         $this->changed = true;
 448:     }
 449: 
 450:     /**
 451:      * Returns the Horde_Imap_Client_Cache object used, if available.
 452:      *
 453:      * @return mixed  Either the object or null.
 454:      */
 455:     public function getCache()
 456:     {
 457:         $this->_initCache();
 458:         return $this->cache;
 459:     }
 460: 
 461:     /**
 462:      * Returns the correct IDs object for use with this driver.
 463:      *
 464:      * @param mixed $ids         See self::add().
 465:      * @param boolean $sequence  Are $ids message sequence numbers?
 466:      *
 467:      * @return Horde_Imap_Client_Ids  The IDs object.
 468:      */
 469:     public function getIdsOb($ids = null, $sequence = false)
 470:     {
 471:         return new Horde_Imap_Client_Ids($ids, $sequence);
 472:     }
 473: 
 474:     /**
 475:      * Returns whether the IMAP server supports the given capability
 476:      * (See RFC 3501 [6.1.1]).
 477:      *
 478:      * @param string $capability  The capability string to query.
 479:      *
 480:      * @param mixed  True if the server supports the queried capability,
 481:      *               false if it doesn't, or an array if the capability can
 482:      *               contain multiple values.
 483:      */
 484:     public function queryCapability($capability)
 485:     {
 486:         if (!isset($this->_init['capability'])) {
 487:             try {
 488:                 $this->capability();
 489:             } catch (Horde_Imap_Client_Exception $e) {
 490:                 return false;
 491:             }
 492:         }
 493: 
 494:         $capability = strtoupper($capability);
 495: 
 496:         if (!isset($this->_init['capability'][$capability])) {
 497:             return false;
 498:         }
 499: 
 500:         /* Check for capability requirements. */
 501:         if (isset(Horde_Imap_Client::$capability_deps[$capability])) {
 502:             foreach (Horde_Imap_Client::$capability_deps[$capability] as $val) {
 503:                 if (!$this->queryCapability($val)) {
 504:                     return false;
 505:                 }
 506:             }
 507:         }
 508: 
 509:         return $this->_init['capability'][$capability];
 510:     }
 511: 
 512:     /**
 513:      * Get CAPABILITY information from the IMAP server.
 514:      *
 515:      * @return array  The capability array.
 516:      *
 517:      * @throws Horde_Imap_Client_Exception
 518:      */
 519:     public function capability()
 520:     {
 521:         if (!isset($this->_init['capability'])) {
 522:             $this->_setInit('capability', $this->_capability());
 523:         }
 524: 
 525:         return $this->_init['capability'];
 526:     }
 527: 
 528:     /**
 529:      * Get CAPABILITY information from the IMAP server.
 530:      *
 531:      * @return array  The capability array.
 532:      *
 533:      * @throws Horde_Imap_Client_Exception
 534:      */
 535:     abstract protected function _capability();
 536: 
 537:     /**
 538:      * Send a NOOP command (RFC 3501 [6.1.2]).
 539:      *
 540:      * @throws Horde_Imap_Client_Exception
 541:      */
 542:     public function noop()
 543:     {
 544:         // NOOP only useful if we are already authenticated.
 545:         if ($this->_isAuthenticated) {
 546:             $this->_noop();
 547:         }
 548:     }
 549: 
 550:     /**
 551:      * Send a NOOP command.
 552:      *
 553:      * @throws Horde_Imap_Client_Exception
 554:      */
 555:     abstract protected function _noop();
 556: 
 557:     /**
 558:      * Get the NAMESPACE information from the IMAP server (RFC 2342).
 559:      *
 560:      * @param array $additional  If the server supports namespaces, any
 561:      *                           additional namespaces to add to the
 562:      *                           namespace list that are not broadcast by
 563:      *                           the server. The namespaces must be UTF-8
 564:      *                           strings.
 565:      *
 566:      * @return array  An array of namespace information with the name as the
 567:      *                key (UTF7-IMAP) and the following values:
 568:      * <ul>
 569:      *  <li>delimiter: (string) The namespace delimiter.</li>
 570:      *  <li>hidden: (boolean) Is this a hidden namespace?</li>
 571:      *  <li>name: (string) The namespace name (UTF7-IMAP).</li>
 572:      *  <li>
 573:      *   translation: (string) Returns the translated name of the namespace
 574:      *   (UTF-8). Requires RFC 5255 and a previous call to setLanguage().
 575:      *  </li>
 576:      *  <li>
 577:      *   type: (integer) The namespace type. Either:
 578:      *   <ul>
 579:      *    <li>Horde_Imap_Client::NS_PERSONAL</li>
 580:      *    <li>Horde_Imap_Client::NS_OTHER</li>
 581:      *    <li>Horde_Imap_Client::NS_SHARED</li>
 582:      *   </ul>
 583:      *  </li>
 584:      * </ul>
 585:      *
 586:      * @throws Horde_Imap_Client_Exception
 587:      */
 588:     public function getNamespaces(array $additional = array())
 589:     {
 590:         $this->login();
 591: 
 592:         $sig = hash('md5', serialize($additional));
 593: 
 594:         if (isset($this->_init['namespace'][$sig])) {
 595:             return $this->_init['namespace'][$sig];
 596:         }
 597: 
 598:         $ns = $this->_getNamespaces();
 599: 
 600:         foreach (array_map(array('Horde_Imap_Client_Utf7imap', 'Utf8ToUtf7Imap'), $additional) as $val) {
 601:             /* Skip namespaces if we have already auto-detected them. Also,
 602:              * hidden namespaces cannot be empty. */
 603:             if (!strlen($val) || isset($ns[$val->utf7imap])) {
 604:                 continue;
 605:             }
 606: 
 607:             $mbox = $this->listMailboxes($val, Horde_Imap_Client::MBOX_ALL, array('delimiter' => true, 'utf8' => true));
 608:             $first = reset($mbox);
 609: 
 610:             if ($first && ($first['mailbox'] == $val)) {
 611:                 $ns[$val->utf7imap] = array(
 612:                     'delimiter' => $first['delimiter'],
 613:                     'hidden' => true,
 614:                     'name' => $val->utf7imap,
 615:                     'translation' => '',
 616:                     'type' => Horde_Imap_Client::NS_SHARED
 617:                 );
 618:             }
 619:         }
 620: 
 621:         if (empty($ns)) {
 622:             /* This accurately determines the namespace information of the
 623:              * base namespace if the NAMESPACE command is not supported.
 624:              * See: RFC 3501 [6.3.8] */
 625:             $mbox = $this->listMailboxes('', Horde_Imap_Client::MBOX_ALL, array('delimiter' => true, 'utf8' => true));
 626:             $first = reset($mbox);
 627:             $ns[''] = array(
 628:                 'delimiter' => $first['delimiter'],
 629:                 'hidden' => false,
 630:                 'name' => '',
 631:                 'translation' => '',
 632:                 'type' => Horde_Imap_Client::NS_PERSONAL
 633:             );
 634:         }
 635: 
 636:         $this->_setInit('namespace', array_merge($this->_init['namespace'], array($sig => $ns)));
 637: 
 638:         return $ns;
 639:     }
 640: 
 641:     /**
 642:      * Get the NAMESPACE information from the IMAP server.
 643:      *
 644:      * @return array  An array of namespace information. See getNamespaces()
 645:      *                for format.
 646:      *
 647:      * @throws Horde_Imap_Client_Exception
 648:      */
 649:     abstract protected function _getNamespaces();
 650: 
 651:     /**
 652:      * Display if connection to the server has been secured via TLS or SSL.
 653:      *
 654:      * @return boolean  True if the IMAP connection is secured.
 655:      */
 656:     public function isSecureConnection()
 657:     {
 658:         return $this->_isSecure;
 659:     }
 660: 
 661:     /**
 662:      * Return a list of alerts that MUST be presented to the user (RFC 3501
 663:      * [7.1]).
 664:      *
 665:      * @return array  An array of alert messages.
 666:      */
 667:     abstract public function alerts();
 668: 
 669:     /**
 670:      * Login to the IMAP server.
 671:      *
 672:      * @throws Horde_Imap_Client_Exception
 673:      */
 674:     public function login()
 675:     {
 676:         if ($this->_isAuthenticated) {
 677:             return;
 678:         }
 679: 
 680:         if ($this->_login()) {
 681:             if (!empty($this->_params['id'])) {
 682:                 try {
 683:                     $this->sendID();
 684:                 } catch (Horde_Imap_Client_Exception_NoSupportExtension $e) {
 685:                     // Ignore if server doesn't support ID extension.
 686:                 }
 687:             }
 688: 
 689:             if (!empty($this->_params['comparator'])) {
 690:                 try {
 691:                     $this->setComparator();
 692:                 } catch (Horde_Imap_Client_Exception_NoSupportExtension $e) {
 693:                     // Ignore if server doesn't support I18NLEVEL=2
 694:                 }
 695:             }
 696: 
 697:             /* Check for ability to cache flags here. */
 698:             if (!isset($this->_init['enabled']['CONDSTORE'])) {
 699:                 unset($this->_params['cache']['fields'][Horde_Imap_Client::FETCH_FLAGS]);
 700:             }
 701:         }
 702: 
 703:         $this->_isAuthenticated = true;
 704:     }
 705: 
 706:     /**
 707:      * Login to the IMAP server.
 708:      *
 709:      * @return boolean  Return true if global login tasks should be run.
 710:      *
 711:      * @throws Horde_Imap_Client_Exception
 712:      */
 713:     abstract protected function _login();
 714: 
 715:     /**
 716:      * Logout from the IMAP server (see RFC 3501 [6.1.3]).
 717:      */
 718:     public function logout()
 719:     {
 720:         if ($this->_isAuthenticated) {
 721:             $this->_logout();
 722:             $this->_isAuthenticated = false;
 723:         }
 724:         $this->_selected = null;
 725:         $this->_mode = 0;
 726:     }
 727: 
 728:     /**
 729:      * Logout from the IMAP server (see RFC 3501 [6.1.3]).
 730:      */
 731:     abstract protected function _logout();
 732: 
 733:     /**
 734:      * Send ID information to the IMAP server (RFC 2971).
 735:      *
 736:      * @param array $info  Overrides the value of the 'id' param and sends
 737:      *                     this information instead.
 738:      *
 739:      * @throws Horde_Imap_Client_Exception
 740:      * @throws Horde_Imap_Client_Exception_NoSupportExtension
 741:      */
 742:     public function sendID($info = null)
 743:     {
 744:         if (!$this->queryCapability('ID')) {
 745:             $this->_exception('The IMAP server does not support the ID extension.', 'NO_SUPPORT');
 746:         }
 747: 
 748:         $this->_sendID(is_null($info) ? (empty($this->_params['id']) ? array() : $this->_params['id']) : $info);
 749:     }
 750: 
 751:     /**
 752:      * Send ID information to the IMAP server (RFC 2971).
 753:      *
 754:      * @param array $info  The information to send to the server.
 755:      *
 756:      * @throws Horde_Imap_Client_Exception
 757:      */
 758:     abstract protected function _sendID($info);
 759: 
 760:     /**
 761:      * Return ID information from the IMAP server (RFC 2971).
 762:      *
 763:      * @return array  An array of information returned, with the keys as the
 764:      *                'field' and the values as the 'value'.
 765:      *
 766:      * @throws Horde_Imap_Client_Exception
 767:      */
 768:     public function getID()
 769:     {
 770:         if (!$this->queryCapability('ID')) {
 771:             $this->_exception('The IMAP server does not support the ID extension.', 'NO_SUPPORT');
 772:         }
 773: 
 774:         return $this->_getID();
 775:     }
 776: 
 777:     /**
 778:      * Return ID information from the IMAP server (RFC 2971).
 779:      *
 780:      * @return array  An array of information returned, with the keys as the
 781:      *                'field' and the values as the 'value'.
 782:      *
 783:      * @throws Horde_Imap_Client_Exception
 784:      */
 785:     abstract protected function _getID();
 786: 
 787:     /**
 788:      * Sets the preferred language for server response messages (RFC 5255).
 789:      *
 790:      * @param array $langs  Overrides the value of the 'lang' param and sends
 791:      *                      this list of preferred languages instead. The
 792:      *                      special string 'i-default' can be used to restore
 793:      *                      the language to the server default.
 794:      *
 795:      * @return string  The language accepted by the server, or null if the
 796:      *                 default language is used.
 797:      *
 798:      * @throws Horde_Imap_Client_Exception
 799:      */
 800:     public function setLanguage($langs = null)
 801:     {
 802:         $lang = null;
 803: 
 804:         if ($this->queryCapability('LANGUAGE')) {
 805:             $lang = is_null($langs)
 806:                 ? (empty($this->_params['lang']) ? null : $this->_params['lang'])
 807:                 : $langs;
 808:         }
 809: 
 810:         return is_null($lang)
 811:             ? null
 812:             : $this->_setLanguage($lang);
 813:     }
 814: 
 815:     /**
 816:      * Sets the preferred language for server response messages (RFC 5255).
 817:      *
 818:      * @param array $langs  The preferred list of languages.
 819:      *
 820:      * @return string  The language accepted by the server, or null if the
 821:      *                 default language is used.
 822:      *
 823:      * @throws Horde_Imap_Client_Exception
 824:      */
 825:     abstract protected function _setLanguage($langs);
 826: 
 827:     /**
 828:      * Gets the preferred language for server response messages (RFC 5255).
 829:      *
 830:      * @param array $list  If true, return the list of available languages.
 831:      *
 832:      * @return mixed  If $list is true, the list of languages available on the
 833:      *                server (may be empty). If false, the language used by
 834:      *                the server, or null if the default language is used.
 835:      *
 836:      * @throws Horde_Imap_Client_Exception
 837:      */
 838:     public function getLanguage($list = false)
 839:     {
 840:         if (!$this->queryCapability('LANGUAGE')) {
 841:             return $list ? array() : null;
 842:         }
 843: 
 844:         return $this->_getLanguage($list);
 845:     }
 846: 
 847:     /**
 848:      * Gets the preferred language for server response messages (RFC 5255).
 849:      *
 850:      * @param array $list  If true, return the list of available languages.
 851:      *
 852:      * @return mixed  If $list is true, the list of languages available on the
 853:      *                server (may be empty). If false, the language used by
 854:      *                the server, or null if the default language is used.
 855:      *
 856:      * @throws Horde_Imap_Client_Exception
 857:      */
 858:     abstract protected function _getLanguage($list);
 859: 
 860:     /**
 861:      * Open a mailbox.
 862:      *
 863:      * @param mixed $mailbox  The mailbox to open. Either a
 864:      *                        Horde_Imap_Client_Mailbox object (as of 1.2.0)
 865:      *                        or a string (UTF-8).
 866:      * @param integer $mode   The access mode. Either
 867:      *   - Horde_Imap_Client::OPEN_READONLY
 868:      *   - Horde_Imap_Client::OPEN_READWRITE
 869:      *   - Horde_Imap_Client::OPEN_AUTO
 870:      *
 871:      * @throws Horde_Imap_Client_Exception
 872:      */
 873:     public function openMailbox($mailbox, $mode = Horde_Imap_Client::OPEN_AUTO)
 874:     {
 875:         $this->login();
 876: 
 877:         $change = false;
 878:         $mailbox = Horde_Imap_Client_Mailbox::get($mailbox, null);
 879: 
 880:         if ($mode == Horde_Imap_Client::OPEN_AUTO) {
 881:             if (is_null($this->_selected) ||
 882:                 !$mailbox->equals($this->_selected)) {
 883:                 $mode = Horde_Imap_Client::OPEN_READONLY;
 884:                 $change = true;
 885:             }
 886:         } elseif (is_null($this->_selected) ||
 887:                   !$mailbox->equals($this->_selected) ||
 888:                   ($mode != $this->_mode)) {
 889:             $change = true;
 890:         }
 891: 
 892:         if ($change) {
 893:             $this->_openMailbox($mailbox, $mode);
 894:             $this->_selected = $mailbox;
 895:             $this->_mode = $mode;
 896:             unset($this->_temp['statuscache'][strval($mailbox)]);
 897:         }
 898:     }
 899: 
 900:     /**
 901:      * Open a mailbox.
 902:      *
 903:      * @param Horde_Imap_Client_Mailbox $mailbox  The mailbox to open.
 904:      * @param integer $mode                       The access mode.
 905:      *
 906:      * @throws Horde_Imap_Client_Exception
 907:      */
 908:     abstract protected function _openMailbox(Horde_Imap_Client_Mailbox $mailbox,
 909:                                              $mode);
 910: 
 911:     /**
 912:      * Return the currently opened mailbox and access mode.
 913:      *
 914:      * @param array $options  Additional options:
 915:      *   - utf8: (boolean) True if 'mailbox' should be in UTF-8 [DEPRECATED].
 916:      *           DEFAULT: 'mailbox' returned in UTF7-IMAP.
 917:      *
 918:      * @return mixed  Null if no mailbox selected, or an array with two
 919:      *                elements:
 920:      *   - mailbox: (mixed) If 'utf8' is true, returns a
 921:      *              Horde_Imap_Client_Mailbox object. Otherwise, returns a
 922:      *              string (UTF7-IMAP; DEPRECATED).
 923:      *   - mode: (integer) Current mode.
 924:      *
 925:      * @throws Horde_Imap_Client_Exception
 926:      */
 927:     public function currentMailbox()
 928:     {
 929:         return is_null($this->_selected)
 930:             ? null
 931:             : array(
 932:                 'mailbox' => (empty($options['utf8']) ? $this->_selected->utf7imap : clone($this->_selected)),
 933:                 'mode' => $this->_mode
 934:             );
 935:     }
 936: 
 937:     /**
 938:      * Create a mailbox.
 939:      *
 940:      * @param mixed $mailbox  The mailbox to create. Either a
 941:      *                        Horde_Imap_Client_Mailbox object (as of 1.2.0)
 942:      *                        or a string (UTF-8).
 943:      * @param array $opts     Additional options:
 944:      *   - special_use: (array) An array of special-use flags to mark the
 945:      *                  mailbox with. The server MUST support RFC 6154.
 946:      *
 947:      * @throws Horde_Imap_Client_Exception
 948:      */
 949:     public function createMailbox($mailbox, array $opts = array())
 950:     {
 951:         $this->login();
 952: 
 953:         if (!$this->queryCapability('CREATE-SPECIAL-USE')) {
 954:             unset($opts['special_use']);
 955:         }
 956: 
 957:         $this->_createMailbox(Horde_Imap_Client_Mailbox::get($mailbox, null), $opts);
 958:     }
 959: 
 960:     /**
 961:      * Create a mailbox.
 962:      *
 963:      * @param Horde_Imap_Client_Mailbox $mailbox  The mailbox to create.
 964:      * @param array $opts                         Additional options. See
 965:      *                                            createMailbox().
 966:      *
 967:      * @throws Horde_Imap_Client_Exception
 968:      */
 969:     abstract protected function _createMailbox(Horde_Imap_Client_Mailbox $mailbox,
 970:                                                $opts);
 971: 
 972:     /**
 973:      * Delete a mailbox.
 974:      *
 975:      * @param mixed $mailbox  The mailbox to delete. Either a
 976:      *                        Horde_Imap_Client_Mailbox object (as of 1.2.0)
 977:      *                        or a string (UTF-8).
 978:      *
 979:      * @throws Horde_Imap_Client_Exception
 980:      */
 981:     public function deleteMailbox($mailbox)
 982:     {
 983:         $this->login();
 984: 
 985:         $mailbox = Horde_Imap_Client_Mailbox::get($mailbox, null);
 986: 
 987:         $this->_deleteMailbox($mailbox);
 988: 
 989:         /* Delete mailbox caches. */
 990:         if ($this->_initCache()) {
 991:             $this->cache->deleteMailbox($mailbox);
 992:         }
 993:         unset($this->_temp['statuscache'][strval($mailbox)]);
 994: 
 995:         /* Unsubscribe from mailbox. */
 996:         try {
 997:             $this->subscribeMailbox($mailbox, false);
 998:         } catch (Horde_Imap_Client_Exception $e) {
 999:             // Ignore failed unsubscribe request
1000:         }
1001:     }
1002: 
1003:     /**
1004:      * Delete a mailbox.
1005:      *
1006:      * @param Horde_Imap_Client_Mailbox $mailbox  The mailbox to delete.
1007:      *
1008:      * @throws Horde_Imap_Client_Exception
1009:      */
1010:     abstract protected function _deleteMailbox(Horde_Imap_Client_Mailbox $mailbox);
1011: 
1012:     /**
1013:      * Rename a mailbox.
1014:      *
1015:      * @param mixed $old  The old mailbox name. Either a
1016:      *                    Horde_Imap_Client_Mailbox object (as of 1.2.0) or a
1017:      *                    string (UTF-8).
1018:      * @param mixed $new  The new mailbox name. Either a
1019:      *                    Horde_Imap_Client_Mailbox object (as of 1.2.0) or a
1020:      *                    string (UTF-8).
1021:      *
1022:      * @throws Horde_Imap_Client_Exception
1023:      */
1024:     public function renameMailbox($old, $new)
1025:     {
1026:         // Login will be handled by first listMailboxes() call.
1027: 
1028:         $old = Horde_Imap_Client_Mailbox::get($old, null);
1029:         $new = Horde_Imap_Client_Mailbox::get($new, null);
1030: 
1031:         /* Check if old mailbox(es) were subscribed to. */
1032:         $base = $this->listMailboxes($old, Horde_Imap_Client::MBOX_SUBSCRIBED, array('delimiter' => true, 'utf8' => true));
1033:         if (empty($base)) {
1034:             $base = $this->listMailboxes($old, Horde_Imap_Client::MBOX_ALL, array('delimiter' => true, 'utf8' => true));
1035:             $base = reset($base);
1036:             $subscribed = array();
1037:         } else {
1038:             $base = reset($base);
1039:             $subscribed = array($base['mailbox']);
1040:         }
1041: 
1042:         $all_mboxes = array($base['mailbox']);
1043:         if (strlen($base['delimiter'])) {
1044:             $all_mboxes = array_merge($all_mboxes, $this->listMailboxes($old . $base['delimiter'] . '*', Horde_Imap_Client::MBOX_ALL, array('flat' => true, 'utf8' => true)));
1045:             $subscribed = array_merge($subscribed, $this->listMailboxes($old . $base['delimiter'] . '*', Horde_Imap_Client::MBOX_SUBSCRIBED, array('flat' => true, 'utf8' => true)));
1046:         }
1047: 
1048:         $this->_renameMailbox($old, $new);
1049: 
1050:         /* Delete mailbox caches. */
1051:         foreach ($all_mboxes as $val) {
1052:             if ($this->_initCache()) {
1053:                 $this->cache->deleteMailbox($val);
1054:             }
1055:             unset($this->_temp['statuscache'][strval($val)]);
1056:         }
1057: 
1058:         foreach ($subscribed as $val) {
1059:             /* Clean up subscription information. */
1060:             try {
1061:                 $this->subscribeMailbox($val, false);
1062:                 $this->subscribeMailbox(new Horde_Imap_Client_Mailbox(substr_replace($val, $new, 0, strlen($old))));
1063:             } catch (Horde_Imap_Client_Exception $e) {
1064:                 // Ignore failed unsubscribe request
1065:             }
1066:         }
1067:     }
1068: 
1069:     /**
1070:      * Rename a mailbox.
1071:      *
1072:      * @param Horde_Imap_Client_Mailbox $old  The old mailbox name.
1073:      * @param Horde_Imap_Client_Mailbox $new  The new mailbox name.
1074:      *
1075:      * @throws Horde_Imap_Client_Exception
1076:      */
1077:     abstract protected function _renameMailbox(Horde_Imap_Client_Mailbox$old,
1078:                                                Horde_Imap_Client_Mailbox $new);
1079: 
1080:     /**
1081:      * Manage subscription status for a mailbox.
1082:      *
1083:      * @param mixed $mailbox      The mailbox to [un]subscribe to. Either a
1084:      *                            Horde_Imap_Client_Mailbox object (as of
1085:      *                            1.2.0) or a string (UTF-8).
1086:      * @param boolean $subscribe  True to subscribe, false to unsubscribe.
1087:      *
1088:      * @throws Horde_Imap_Client_Exception
1089:      */
1090:     public function subscribeMailbox($mailbox, $subscribe = true)
1091:     {
1092:         $this->login();
1093:         $this->_subscribeMailbox(Horde_Imap_Client_Mailbox::get($mailbox, null), (bool)$subscribe);
1094:     }
1095: 
1096:     /**
1097:      * Manage subscription status for a mailbox.
1098:      *
1099:      * @param Horde_Imap_Client_Mailbox $mailbox  The mailbox to [un]subscribe
1100:      *                                            to.
1101:      * @param boolean $subscribe                  True to subscribe, false to
1102:      *                                            unsubscribe.
1103:      *
1104:      * @throws Horde_Imap_Client_Exception
1105:      */
1106:     abstract protected function _subscribeMailbox(Horde_Imap_Client_Mailbox $mailbox,
1107:                                                   $subscribe);
1108: 
1109:     /**
1110:      * Obtain a list of mailboxes matching a pattern.
1111:      *
1112:      * @param mixed $pattern   The mailbox search pattern(s) (see RFC 3501
1113:      *                         [6.3.8] for the format). A UTF-8 string or an
1114:      *                         array of strings.
1115:      * @param integer $mode    Which mailboxes to return.  Either:
1116:      *   - Horde_Imap_Client::MBOX_SUBSCRIBED
1117:      *   - Horde_Imap_Client::MBOX_SUBSCRIBED_EXISTS
1118:      *   - Horde_Imap_Client::MBOX_UNSUBSCRIBED
1119:      *   - Horde_Imap_Client::MBOX_ALL
1120:      * @param array $options   Additional options:
1121:      * <ul>
1122:      *  <li>
1123:      *   attributes: (boolean) If true, return attribute information under
1124:      *   the 'attributes' key.
1125:      *   DEFAULT: Do not return this information.
1126:      *  </li>
1127:      *  <li>
1128:      *   children: (boolean) Tell server to return children attribute
1129:      *   information (\HasChildren, \HasNoChildren). Requires the
1130:      *   LIST-EXTENDED extension to guarantee this information is returned.
1131:      *   Server MAY return this attribute without this option, or if the
1132:      *   CHILDREN extension is available, but it is not guaranteed.
1133:      *   DEFAULT: false
1134:      *  </li>
1135:      *  <li>
1136:      *   delimiter: (boolean) If true, return delimiter information under the
1137:      *   'delimiter' key.
1138:      *   DEFAULT: Do not return this information.
1139:      *  </li>
1140:      *  <li>
1141:      *   flat: (boolean) If true, return a flat list of mailbox names only.
1142:      *   Overrides both the 'attributes' and 'delimiter' options.
1143:      *   DEFAULT: Do not return flat list.
1144:      *  </li>
1145:      *  <li>
1146:      *   recursivematch: (boolean) Force the server to return information
1147:      *   about parent mailboxes that don't match other selection options, but
1148:      *   have some submailboxes that do. Information about children is
1149:      *   returned in the CHILDINFO extended data item ('extended'). Requires
1150:      *   the LIST-EXTENDED extension.
1151:      *   DEFAULT: false
1152:      *  </li>
1153:      *  <li>
1154:      *   remote: (boolean) Tell server to return mailboxes that reside on
1155:      *   another server. Requires the LIST-EXTENDED extension.
1156:      *   DEFAULT: false
1157:      *  </li>
1158:      *  <li>
1159:      *   special_use: (boolean) Tell server to return special-use attribute
1160:      *   information (\Drafts, \Flagged, \Junk, \Sent, \Trash, \All,
1161:      *   \Archive). Server must support the SPECIAL-USE return option for this
1162:      *   setting to have any effect. Server MAY return this attribute without
1163:      *   this option.
1164:      *   DEFAULT: false
1165:      *  <li>
1166:      *   status: (integer) Tell server to return status information. The
1167:      *   value is a bitmask that may contain any of:
1168:      *   <ul>
1169:      *    <li>Horde_Imap_Client::STATUS_MESSAGES</li>
1170:      *    <li>Horde_Imap_Client::STATUS_RECENT</li>
1171:      *    <li>Horde_Imap_Client::STATUS_UIDNEXT</li>
1172:      *    <li>Horde_Imap_Client::STATUS_UIDVALIDITY</li>
1173:      *    <li>Horde_Imap_Client::STATUS_UNSEEN</li>
1174:      *    <li>Horde_Imap_Client::STATUS_HIGHESTMODSEQ</li>
1175:      *   </ul>
1176:      *           Requires the LIST-STATUS extension.
1177:      *           DEFAULT: 0
1178:      *  </li>
1179:      *  <li>
1180:      *   sort: (boolean) If true, return a sorted list of mailboxes?
1181:      *   DEFAULT: Do not sort the list.
1182:      *  </li>
1183:      *  <li>
1184:      *   sort_delimiter: (string) If 'sort' is true, this is the delimiter
1185:      *   used to sort the mailboxes.
1186:      *   DEFAULT: '.'
1187:      *  </li>
1188:      *  <li>
1189:      *   utf8: (boolean) True to return mailbox names in UTF-8.
1190:      *   DEFAULT: Names are returned in UTF7-IMAP.
1191:      *  </li>
1192:      * </ul>
1193:      *
1194:      * @return array  If 'flat' option is true, the array values are a list
1195:      *                of Horde_Imap_Client_Mailbox objects (if the 'utf8'
1196:      *                parameter is true) or a list of UTF7-IMAP strings.
1197:      *                Otherwise, the array values are arrays with these keys:
1198:      *   - attributes: (array) List of lower-cased attributes [only if
1199:      *                 'attributes' option is true].
1200:      *   - delimiter: (string) The delimiter for the mailbox [only if
1201:      *                'delimiter' option is true].
1202:      *   - extended: (TODO) TODO [only if 'recursivematch' option is true and
1203:      *               LIST-EXTENDED extension is supported on the server].
1204:      *   - mailbox: (mixed) The mailbox. A Horde_Imap_Client_Mailbox object if
1205:      *              the 'utf8' parameter is true, or a UTF7-IMAP string.
1206:      *   - status: (array) See status() [only if 'status' option is true and
1207:      *             LIST-STATUS extension is supported on the server].
1208:      *
1209:      * @throws Horde_Imap_Client_Exception
1210:      */
1211:     public function listMailboxes($pattern, $mode = Horde_Imap_Client::MBOX_ALL,
1212:                                   $options = array())
1213:     {
1214:         $this->login();
1215: 
1216:         if (!is_array($pattern)) {
1217:             $pattern = array($pattern);
1218:         }
1219: 
1220:         if (isset($options['special_use']) &&
1221:             !$this->queryCapability('SPECIAL-USE')) {
1222:             unset($options['special_use']);
1223:         }
1224: 
1225:         $ret = $this->_listMailboxes(
1226:             array_map(array('Horde_Imap_Client_Utf7imap', 'Utf8ToUtf7Imap'), $pattern, array_fill(0, count($pattern), null)),
1227:             $mode,
1228:             $options
1229:         );
1230: 
1231:         if (!empty($options['sort'])) {
1232:             Horde_Imap_Client_Sort::sortMailboxes($ret, array('delimiter' => empty($options['sort_delimiter']) ? '.' : $options['sort_delimiter'], 'index' => false, 'keysort' => empty($options['flat'])));
1233:         }
1234: 
1235:         return $ret;
1236:     }
1237: 
1238:     /**
1239:      * Obtain a list of mailboxes matching a pattern.
1240:      *
1241:      * @param array $pattern  The mailbox search patterns (UTF7-IMAP strings).
1242:      * @param integer $mode   Which mailboxes to return.
1243:      * @param array $options  Additional options.
1244:      *
1245:      * @return array  See listMailboxes().
1246:      *
1247:      * @throws Horde_Imap_Client_Exception
1248:      */
1249:     abstract protected function _listMailboxes($pattern, $mode, $options);
1250: 
1251:     /**
1252:      * Obtain status information for a mailbox.
1253:      *
1254:      * @param mixed $mailbox  The mailbox to query. Either a
1255:      *                        Horde_Imap_Client_Mailbox object (as of 1.2.0)
1256:      *                        or a string (UTF-8).
1257:      * @param integer $flags  A bitmask of information requested from the
1258:      *                        server. Allowed flags:
1259:      * <ul>
1260:      *  <li>
1261:      *   Horde_Imap_Client::STATUS_MESSAGES
1262:      *   <ul>
1263:      *    <li>
1264:      *     Return key: messages
1265:      *    </li>
1266:      *    <li>
1267:      *     Return format: (integer) The number of messages in the mailbox.
1268:      *    </li>
1269:      *   </ul>
1270:      *  </li>
1271:      *  <li>
1272:      *   Horde_Imap_Client::STATUS_RECENT
1273:      *   <ul>
1274:      *    <li>
1275:      *     Return key: recent
1276:      *    </li>
1277:      *    <li>
1278:      *     Return format: (integer) The number of messages with the \Recent
1279:      *     flag set
1280:      *    </li>
1281:      *   </ul>
1282:      *  </li>
1283:      *  <li>
1284:      *   Horde_Imap_Client::STATUS_UIDNEXT
1285:      *   <ul>
1286:      *    <li>
1287:      *     Return key: uidnext
1288:      *    </li>
1289:      *    <li>
1290:      *     Return format: (integer) The next UID to be assigned in the
1291:      *     mailbox.
1292:      *    </li>
1293:      *   </ul>
1294:      *  </li>
1295:      *  <li>
1296:      *   Horde_Imap_Client::STATUS_UIDVALIDITY
1297:      *   <ul>
1298:      *    <li>
1299:      *     Return key: uidvalidity
1300:      *    </li>
1301:      *    <li>
1302:      *     Return format: (integer) The unique identifier validity of the
1303:      *     mailbox.
1304:      *    </li>
1305:      *   </ul>
1306:      *  </li>
1307:      *  <li>
1308:      *   Horde_Imap_Client::STATUS_UNSEEN
1309:      *   <ul>
1310:      *    <li>
1311:      *     Return key: unseen
1312:      *    </li>
1313:      *    <li>
1314:      *     Return format: (integer) The number of messages which do not have
1315:      *     the \Seen flag set.
1316:      *    </li>
1317:      *   </ul>
1318:      *  </li>
1319:      *  <li>
1320:      *   Horde_Imap_Client::STATUS_FIRSTUNSEEN
1321:      *   <ul>
1322:      *    <li>
1323:      *     Return key: firstunseen
1324:      *    </li>
1325:      *    <li>
1326:      *     Return format: (integer) The sequence number of the first unseen
1327:      *     message in the mailbox.
1328:      *    </li>
1329:      *   </ul>
1330:      *  </li>
1331:      *  <li>
1332:      *   Horde_Imap_Client::STATUS_FLAGS
1333:      *   <ul>
1334:      *    <li>
1335:      *     Return key: flags
1336:      *    </li>
1337:      *    <li>
1338:      *     Return format: (array) The list of defined flags in the mailbox
1339:      *     (all flags are in lowercase).
1340:      *    </li>
1341:      *   </ul>
1342:      *  </li>
1343:      *  <li>
1344:      *   Horde_Imap_Client::STATUS_PERMFLAGS
1345:      *   <ul>
1346:      *    <li>
1347:      *     Return key: permflags
1348:      *    </li>
1349:      *    <li>
1350:      *     Return format: (array) The list of flags that a client can change
1351:      *     permanently (all flags are in lowercase).
1352:      *    </li>
1353:      *   </ul>
1354:      *  </li>
1355:      *  <li>
1356:      *   Horde_Imap_Client::STATUS_HIGHESTMODSEQ
1357:      *   <ul>
1358:      *    <li>
1359:      *     Return key: highestmodseq
1360:      *    </li>
1361:      *    <li>
1362:      *     Return format: (integer) If the server supports the CONDSTORE
1363:      *     IMAP extension, this will be the highest mod-sequence value of all
1364:      *     messages in the mailbox. Else 0 if CONDSTORE not available or the
1365:      *     mailbox does not support mod-sequences.
1366:      *    </li>
1367:      *   </ul>
1368:      *  </li>
1369:      *  <li>
1370:      *   Horde_Imap_Client::STATUS_LASTMODSEQ
1371:      *   <ul>
1372:      *    <li>
1373:      *     Return key: lastmodseq
1374:      *    </li>
1375:      *    <li>
1376:      *     Return format: (integer) If the server supports the CONDSTORE
1377:      *     IMAP extension, this will be the cached mod-sequence value of the
1378:      *     mailbox when it was first opened if HIGHESTMODSEQ changed. Else 0
1379:      *     if CONDSTORE not available, the mailbox does not support
1380:      *     mod-sequences, or the mod-sequence did not change.
1381:      *    </li>
1382:      *   </ul>
1383:      *  </li>
1384:      *  <li>
1385:      *   Horde_Imap_Client::STATUS_LASTMODSEQUIDS
1386:      *   <ul>
1387:      *    <li>
1388:      *     Return key: lastmodsequids
1389:      *    </li>
1390:      *    <li>
1391:      *     Return format: (array) If the server supports the QRESYNC IMAP
1392:      *     extension, this will be the list of UIDs changed in the mailbox
1393:      *     when it was first opened if HIGHESTMODSEQ changed. Else an empty
1394:      *     array if QRESYNC not available, the mailbox does not support
1395:      *     mod-sequences, or the mod-sequence did not change.
1396:      *    </li>
1397:      *   </ul>
1398:      *  </li>
1399:      *  <li>
1400:      *   Horde_Imap_Client::STATUS_UIDNOTSTICKY
1401:      *   <ul>
1402:      *    <li>
1403:      *     Return key: uidnotsticky
1404:      *    </li>
1405:      *    <li>
1406:      *     Return format: (boolean) If the server supports the UIDPLUS IMAP
1407:      *     extension, and the queried mailbox does not support persistent
1408:      *     UIDs, this value will be true. In all other cases, this value will
1409:      *     be false.
1410:      *    </li>
1411:      *   </ul>
1412:      *  </li>
1413:      *  <li>
1414:      *   Horde_Imap_Client::STATUS_ALL (DEFAULT)
1415:      *   <ul>
1416:      *    <li>
1417:      *     Shortcut to return 'messages', 'recent', 'uidnext', 'uidvalidity',
1418:      *     and 'unseen' values.
1419:      *    </li>
1420:      *   </ul>
1421:      *  </li>
1422:      * </ul>
1423:      *
1424:      * @return array  An array with the requested keys (see above).
1425:      *
1426:      * @throws Horde_Imap_Client_Exception
1427:      */
1428:     public function status($mailbox, $flags = Horde_Imap_Client::STATUS_ALL)
1429:     {
1430:         $this->login();
1431: 
1432:         $unselected_flags = array(
1433:             'messages' => Horde_Imap_Client::STATUS_MESSAGES,
1434:             'recent' => Horde_Imap_Client::STATUS_RECENT,
1435:             'unseen' => Horde_Imap_Client::STATUS_UNSEEN,
1436:             'uidnext' => Horde_Imap_Client::STATUS_UIDNEXT,
1437:             'uidvalidity' => Horde_Imap_Client::STATUS_UIDVALIDITY
1438:         );
1439: 
1440:         if ($flags & Horde_Imap_Client::STATUS_ALL) {
1441:             foreach ($unselected_flags as $val) {
1442:                 $flags |= $val;
1443:             }
1444:         }
1445: 
1446:         $mailbox = Horde_Imap_Client_Mailbox::get($mailbox, null);
1447:         $ret = array();
1448: 
1449:         /* Check for cached information. */
1450:         if ($mailbox->equals($this->_selected) &&
1451:             isset($this->_temp['statuscache'][strval($mailbox)])) {
1452:             $ptr = &$this->_temp['statuscache'][strval($mailbox)];
1453: 
1454:             foreach ($unselected_flags as $key => $val) {
1455:                 if (($flags & $val) && isset($ptr[$key])) {
1456:                     $ret[$key] = $ptr[$key];
1457:                     $flags &= ~$val;
1458:                 }
1459:             }
1460:         }
1461: 
1462:         /* Catch flags that are not supported. */
1463:         if (($flags & Horde_Imap_Client::STATUS_HIGHESTMODSEQ) &&
1464:             !isset($this->_init['enabled']['CONDSTORE'])) {
1465:             $ret['highestmodseq'] = 0;
1466:             $flags &= ~Horde_Imap_Client::STATUS_HIGHESTMODSEQ;
1467:         }
1468: 
1469:         if (($flags & Horde_Imap_Client::STATUS_UIDNOTSTICKY) &&
1470:             !$this->queryCapability('UIDPLUS')) {
1471:             $ret['uidnotsticky'] = false;
1472:             $flags &= ~Horde_Imap_Client::STATUS_UIDNOTSTICKY;
1473:         }
1474: 
1475:         /* Handle LASTMODSEQ related options. */
1476:         if ($flags & Horde_Imap_Client::STATUS_LASTMODSEQ) {
1477:             $ret['lastmodseq'] = 0;
1478:             if (isset($this->_init['enabled']['CONDSTORE']) &&
1479:                 isset($this->_temp['lastmodseq'][strval($mailbox)])) {
1480:                 $ret['lastmodseq'] = $this->_temp['lastmodseq'][strval($mailbox)];
1481:             }
1482:             $flags &= ~Horde_Imap_Client::STATUS_LASTMODSEQ;
1483:         }
1484: 
1485:         if ($flags & Horde_Imap_Client::STATUS_LASTMODSEQUIDS) {
1486:             $ret['lastmodsequids'] = array();
1487:             if (isset($this->_init['enabled']['CONDSTORE']) &&
1488:                 isset($this->_temp['lastmodsequids'][strval($mailbox)])) {
1489:                 $ret['lastmodsequids'] = $this->utils->fromSequenceString($this->_temp['lastmodsequids'][strval($mailbox)]);
1490:             }
1491:             $flags &= ~Horde_Imap_Client::STATUS_LASTMODSEQUIDS;
1492:         }
1493: 
1494:         if (!$flags) {
1495:             return $ret;
1496:         }
1497: 
1498:         /* STATUS_PERMFLAGS requires a read/write mailbox. */
1499:         if ($flags & Horde_Imap_Client::STATUS_PERMFLAGS) {
1500:             $this->openMailbox($mailbox, Horde_Imap_Client::OPEN_READWRITE);
1501:         }
1502: 
1503:         $ret = array_merge($ret, $this->_status($mailbox, $flags));
1504: 
1505:         if (!$mailbox->equals($this->_selected)) {
1506:             if (!isset($this->_temp['statuscache'])) {
1507:                 $this->_temp['statuscache'] = array();
1508:             }
1509:             $ptr = &$this->_temp['statuscache'];
1510: 
1511:             $ptr[strval($mailbox)] = isset($ptr[strval($mailbox)])
1512:                 ? array_merge($ptr[strval($mailbox)], $ret)
1513:                 : $ret;
1514:         }
1515: 
1516:         return $ret;
1517:     }
1518: 
1519:     /**
1520:      * Obtain status information for a mailbox.
1521:      *
1522:      * @param Horde_Imap_Client_Mailbox $mailbox  The mailbox to query.
1523:      * @param integer $flags                      A bitmask of information
1524:      *                                            requested from the server.
1525:      *
1526:      * @return array  See status().
1527:      *
1528:      * @throws Horde_Imap_Client_Exception
1529:      */
1530:     abstract protected function _status(Horde_Imap_Client_Mailbox $mailbox,
1531:                                         $flags);
1532: 
1533:     /**
1534:      * Perform a STATUS call on multiple mailboxes at the same time.
1535:      *
1536:      * This method leverages the LIST-EXTENDED and LIST-STATUS extensions on
1537:      * the IMAP server to improve the efficiency of this operation.
1538:      *
1539:      * @param array $mailboxes  The mailboxes to query. Either
1540:      *                          Horde_Imap_Client_Mailbox objects (as of
1541:      *                          1.2.0), strings (UTF-8), or a combination of
1542:      *                          the two.
1543:      * @param integer $flags    See status().
1544:      * @param array $opts       Additional options:
1545:      *   - sort: (boolean) If true, sort the list of mailboxes?
1546:      *           DEFAULT: Do not sort the list.
1547:      *   - sort_delimiter: (string) If 'sort' is true, this is the delimiter
1548:      *                     used to sort the mailboxes.
1549:      *                     DEFAULT: '.'
1550:      *
1551:      * @return array  An array with the keys as the mailbox names (UTF7-IMAP)
1552:      *                and the values as arrays with the requested keys (from
1553:      *                the mask given in $flags).
1554:      */
1555:     public function statusMultiple($mailboxes,
1556:                                    $flags = Horde_Imap_Client::STATUS_ALL,
1557:                                    array $opts = array())
1558:     {
1559:         if (empty($mailboxes)) {
1560:             return array();
1561:         }
1562: 
1563:         $this->login();
1564: 
1565:         $opts = array_merge(array(
1566:             'sort' => false,
1567:             'sort_delimiter' => '.'
1568:         ), $opts);
1569: 
1570:         $need_status = true;
1571:         $ret = array();
1572: 
1573:         /* Optimization: If there is one mailbox in list, and we are already
1574:          * in that mailbox, we should just do a straight STATUS call. */
1575:         if ($this->queryCapability('LIST-STATUS') &&
1576:             ((count($mailboxes) != 1) ||
1577:             !Horde_Imap_Client_Mailbox::get(reset($mailboxes), null)->equals($this->_selected))) {
1578:             $status = $to_process = array();
1579:             foreach ($mailboxes as $val) {
1580:                 // Force mailboxes containing wildcards to be accessed via
1581:                 // STATUS so that wildcards do not return a bunch of mailboxes
1582:                 // in the LIST-STATUS response.
1583:                 if (strpbrk($val, '*%') === false) {
1584:                     $to_process[] = $val;
1585:                 }
1586:                 $status[strval($val)] = 1;
1587:             }
1588: 
1589:             try {
1590:                 foreach ($this->listMailboxes($to_process, Horde_Imap_Client::MBOX_ALL, array_merge($opts, array('status' => $flags, 'utf8' => true))) as $val) {
1591:                     if (isset($val['status'])) {
1592:                         $ret[$val['mailbox']->utf7imap] = $val['status'];
1593:                     }
1594:                     unset($status[strval($val['mailbox'])]);
1595:                 }
1596:             } catch (Horde_Imap_Client_Exception $e) {}
1597: 
1598:             if (empty($status)) {
1599:                 $need_status = false;
1600:             } else {
1601:                 $mailboxes = array_keys($status);
1602:             }
1603:         }
1604: 
1605:         if ($need_status) {
1606:             foreach ($mailboxes as $val) {
1607:                 $val = Horde_Imap_Client_Mailbox::get($val, null);
1608:                 try {
1609:                     $ret[$val->utf7imap] = $this->status($val, $flags);
1610:                 } catch (Horde_Imap_Client_Exception $e) {}
1611:             }
1612:         }
1613: 
1614:         if ($opts['sort']) {
1615:             Horde_Imap_Client_Sort::sortMailboxes($ret, array(
1616:                 'delimiter' => $opts['sort_delimiter'],
1617:                 'keysort' => true
1618:             ));
1619:         }
1620: 
1621:         return $ret;
1622:     }
1623: 
1624:     /**
1625:      * Append message(s) to a mailbox.
1626:      *
1627:      * @param mixed $mailbox  The mailbox to append the message(s) to. Either
1628:      *                        a Horde_Imap_Client_Mailbox object (as of 1.2.0)
1629:      *                        or a string (UTF-8).
1630:      * @param array $data     The message data to append, along with
1631:      *                        additional options. An array of arrays with
1632:      *                        each embedded array having the following
1633:      *                        entries:
1634:      * <ul>
1635:      *  <li>
1636:      *   data: (mixed) The data to append. If a string or a stream resource,
1637:      *   this will be used as the entire contents of a single message. If an
1638:      *   array, will catenate all given parts into a single message. This
1639:      *   array contains one or more arrays with two keys:
1640:      *   <ul>
1641:      *    <li>
1642:      *     t: (string) Either 'url' or 'text'.
1643:      *    </li>
1644:      *    <li>
1645:      *     v: (mixed) If 't' is 'url', this is the IMAP URL to the message
1646:      *     part to append. If 't' is 'text', this is either a string or
1647:      *     resource representation of the message part data.
1648:      *     DEFAULT: NONE (entry is MANDATORY)
1649:      *    </li>
1650:      *   </ul>
1651:      *  </li>
1652:      *  <li>
1653:      *   flags: (array) An array of flags/keywords to set on the appended
1654:      *   message.
1655:      *   DEFAULT: Only the \Recent flag is set.
1656:      *  </li>
1657:      *  <li>
1658:      *   internaldate: (DateTime) The internaldate to set for the appended
1659:      *   message.
1660:      *   DEFAULT: internaldate will be the same date as when the message was
1661:      *   appended.
1662:      *  </li>
1663:      * </ul>
1664:      * @param array $options  Additonal options:
1665:      *   - create: (boolean) Try to create $mailbox if it does not exist?
1666:      *             DEFAULT: No.
1667:      *
1668:      * @return Horde_Imap_Client_Ids  The UIDs of the appended messages.
1669:      *
1670:      * @throws Horde_Imap_Client_Exception
1671:      */
1672:     public function append($mailbox, $data, $options = array())
1673:     {
1674:         $this->login();
1675: 
1676:         $mailbox = Horde_Imap_Client_Mailbox::get($mailbox, null);
1677: 
1678:         $ret = $this->_append($mailbox, $data, $options);
1679:         unset($this->_temp['statuscache'][strval($mailbox)]);
1680: 
1681:         if ($ret instanceof Horde_Imap_Client_Ids) {
1682:             return $ret;
1683:         }
1684: 
1685:         $uids = $this->getIdsOb();
1686: 
1687:         while (list(,$val) = each($data)) {
1688:             if (is_string($val['data'])) {
1689:                 $text = $val['data'];
1690:             } elseif (is_resource($val['data'])) {
1691:                 $text = '';
1692:                 rewind($val['data']);
1693:                 while (!feof($val['data'])) {
1694:                     $text .= fread($val['data'], 512);
1695:                     if (preg_match("/\n\r{2,}/", $text)) {
1696:                         break;
1697:                     }
1698:                 }
1699:             }
1700: 
1701:             $headers = Horde_Mime_Headers::parseHeaders($text);
1702:             $msgid = $headers->getValue('message-id');
1703: 
1704:             if ($msgid) {
1705:                 $search_query = new Horde_Imap_Client_Search_Query();
1706:                 $search_query->headerText('Message-ID', $msgid);
1707:                 $uidsearch = $this->search($mailbox, $search_query);
1708:                 $uids->add($uidsearch['match']);
1709:             }
1710:         }
1711: 
1712:         return $uids;
1713:     }
1714: 
1715:     /**
1716:      * Append message(s) to a mailbox.
1717:      *
1718:      * @param Horde_Imap_Client_Mailbox $mailbox  The mailbox to append the
1719:      *                                            message(s) to.
1720:      * @param array $data                         The message data.
1721:      * @param array $options                      Additional options.
1722:      *
1723:      * @return mixed  A Horde_Imap_Client_Ids object containing the UIDs of
1724:      *                the appended messages (if server supports UIDPLUS
1725:      *                extension) or true.
1726:      *
1727:      * @throws Horde_Imap_Client_Exception
1728:      */
1729:     abstract protected function _append(Horde_Imap_Client_Mailbox $mailbox,
1730:                                         $data, $options);
1731: 
1732:     /**
1733:      * Request a checkpoint of the currently selected mailbox (RFC 3501
1734:      * [6.4.1]).
1735:      *
1736:      * @throws Horde_Imap_Client_Exception
1737:      */
1738:     public function check()
1739:     {
1740:         // CHECK only useful if we are already authenticated.
1741:         if ($this->_isAuthenticated) {
1742:             $this->_check();
1743:         }
1744:     }
1745: 
1746:     /**
1747:      * Request a checkpoint of the currently selected mailbox.
1748:      *
1749:      * @throws Horde_Imap_Client_Exception
1750:      */
1751:     abstract protected function _check();
1752: 
1753:     /**
1754:      * Close the connection to the currently selected mailbox, optionally
1755:      * expunging all deleted messages (RFC 3501 [6.4.2]).
1756:      *
1757:      * @param array $options  Additional options:
1758:      *   - expunge: (boolean) Expunge all messages flagged as deleted?
1759:      *              DEFAULT: No
1760:      *
1761:      * @throws Horde_Imap_Client_Exception
1762:      */
1763:     public function close($options = array())
1764:     {
1765:         // This check catches the non-logged in case.
1766:         if (is_null($this->_selected)) {
1767:             return;
1768:         }
1769: 
1770:         /* If we are caching, search for deleted messages. */
1771:         if (!empty($options['expunge']) &&
1772:             $this->_initCache(true)) {
1773:             $search_query = new Horde_Imap_Client_Search_Query();
1774:             $search_query->flag(Horde_Imap_Client::FLAG_DELETED, true);
1775:             $search_res = $this->search($this->_selected, $search_query);
1776:             $mbox = $this->_selected;
1777:         } else {
1778:             $search_res = null;
1779:         }
1780: 
1781:         $this->_close($options);
1782:         $this->_selected = null;
1783:         $this->_mode = 0;
1784: 
1785:         if (!is_null($search_res)) {
1786:             $this->_deleteMsgs($mbox, $search_res['match']->ids);
1787:         }
1788:     }
1789: 
1790:     /**
1791:      * Close the connection to the currently selected mailbox, optionally
1792:      * expunging all deleted messages (RFC 3501 [6.4.2]).
1793:      *
1794:      * @param array $options  Additional options.
1795:      *
1796:      * @throws Horde_Imap_Client_Exception
1797:      */
1798:     abstract protected function _close($options);
1799: 
1800:     /**
1801:      * Expunge deleted messages from the given mailbox.
1802:      *
1803:      * @param mixed $mailbox  The mailbox to expunge. Either a
1804:      *                        Horde_Imap_Client_Mailbox object (as of 1.2.0)
1805:      *                        or a string (UTF-8).
1806:      * @param array $options  Additional options:
1807:      *   - ids: (Horde_Imap_Client_Ids) A list of messages to expunge, but
1808:      *          only if they are also flagged as deleted.
1809:      *          DEFAULT: All messages marked as deleted will be expunged.
1810:      *   - list: (boolean) If true, returns the list of expunged messages.
1811:      *           DEFAULT: false
1812:      *
1813:      * @return Horde_Imap_Client_Ids  If 'list' option is true, returns the
1814:      *                                list of expunged messages.
1815:      *
1816:      * @throws Horde_Imap_Client_Exception
1817:      */
1818:     public function expunge($mailbox, $options = array())
1819:     {
1820:         // Open mailbox call will handle the login.
1821:         $this->openMailbox($mailbox, Horde_Imap_Client::OPEN_READWRITE);
1822: 
1823:         if (empty($options['ids'])) {
1824:             $options['ids'] = $this->getIdsOb(Horde_Imap_Client_Ids::ALL);
1825:         } elseif ($options['ids']->isEmpty()) {
1826:             return array();
1827:         }
1828: 
1829:         return $this->_expunge($options);
1830:     }
1831: 
1832:     /**
1833:      * Expunge all deleted messages from the given mailbox.
1834:      *
1835:      * @param array $options  Additional options.
1836:      *
1837:      * @return Horde_Imap_Client_Ids  If 'list' option is true, returns the
1838:      *                                list of expunged messages.
1839:      *
1840:      * @throws Horde_Imap_Client_Exception
1841:      */
1842:     abstract protected function _expunge($options);
1843: 
1844:     /**
1845:      * Search a mailbox.
1846:      *
1847:      * @param mixed $mailbox                         The mailbox to search.
1848:      *                                               Either a
1849:      *                                               Horde_Imap_Client_Mailbox
1850:      *                                               object (as of 1.2.0) or a
1851:      *                                               string (UTF-8).
1852:      * @param Horde_Imap_Client_Search_Query $query  The search query.
1853:      *                                               Defaults to an ALL
1854:      *                                               search.
1855:      * @param array $options                         Additional options:
1856:      * <ul>
1857:      *  <li>
1858:      *   nocache: (boolean) Don't cache the results.
1859:      *   DEFAULT: false (results cached, if possible)
1860:      *  </li>
1861:      *  <li>
1862:      *   partial: (mixed) The range of results to return (message sequence
1863:      *   numbers).
1864:      *   DEFAULT: All messages are returned.
1865:      *  </li>
1866:      *  <li>
1867:      *   results: (array) The data to return. Consists of zero or more of
1868:      *   the following flags:
1869:      *   <ul>
1870:      *    <li>Horde_Imap_Client::SEARCH_RESULTS_COUNT</li>
1871:      *    <li>Horde_Imap_Client::SEARCH_RESULTS_MATCH (DEFAULT)</li>
1872:      *    <li>Horde_Imap_Client::SEARCH_RESULTS_MAX</li>
1873:      *    <li>Horde_Imap_Client::SEARCH_RESULTS_MIN</li>
1874:      *    <li>
1875:      *     Horde_Imap_Client::SEARCH_RESULTS_SAVE (This option is currently
1876:      *     meant for internal use only)
1877:      *    </li>
1878:      *    <li>Horde_Imap_Client::SEARCH_RESULTS_RELEVANCY</li>
1879:      *   </ul>
1880:      *  </li>
1881:      *  <li>
1882:      *   sequence: (boolean) If true, returns an array of sequence numbers.
1883:      *   DEFAULT: Returns an array of UIDs
1884:      *  </li>
1885:      *  <li>
1886:      *   sort: (array) Sort the returned list of messages. Multiple sort
1887:      *   criteria can be specified. Any sort criteria can be sorted in reverse
1888:      *   order (instead of the default ascending order) by adding a
1889:      *   Horde_Imap_Client::SORT_REVERSE element to the array directly before
1890:      *   adding the sort element. The following sort criteria are available:
1891:      *   <ul>
1892:      *    <li>Horde_Imap_Client::SORT_ARRIVAL</li>
1893:      *    <li>Horde_Imap_Client::SORT_CC</li>
1894:      *    <li>Horde_Imap_Client::SORT_DATE</li>
1895:      *    <li>Horde_Imap_Client::SORT_FROM</li>
1896:      *    <li>Horde_Imap_Client::SORT_SEQUENCE</li>
1897:      *    <li>Horde_Imap_Client::SORT_SIZE</li>
1898:      *    <li>Horde_Imap_Client::SORT_SUBJECT</li>
1899:      *    <li>Horde_Imap_Client::SORT_TO</li>
1900:      *    <li>
1901:      *     [On servers that support SORT=DISPLAY, these criteria are also
1902:      *     available:]
1903:      *     <ul>
1904:      *      <li>Horde_Imap_Client::SORT_DISPLAYFROM</li>
1905:      *      <li>Horde_Imap_Client::SORT_DISPLAYTO</li>
1906:      *     </ul>
1907:      *    </li>
1908:      *    <li>
1909:      *     [On servers that support SEARCH=FUZZY, this criteria is also
1910:      *     available:]
1911:      *     <ul>
1912:      *      <li>Horde_Imap_Client::SORT_RELEVANCY</li>
1913:      *     </ul>
1914:      *    </li>
1915:      *   </ul>
1916:      *  </li>
1917:      * </ul>
1918:      *
1919:      * @return array  An array with the following keys:
1920:      *   - count: (integer) The number of messages that match the search
1921:      *            criteria. Always returned.
1922:      *   - match: (Horde_Imap_Client_Ids) The IDs that match $criteria, sorted
1923:      *            if the 'sort' modifier was set. Returned if
1924:      *            Horde_Imap_Client::SEARCH_RESULTS_MATCH is set.
1925:      *   - max: (integer) The UID (default) or message sequence number (if
1926:      *          'sequence' is true) of the highest message that satisifies
1927:      *          $criteria. Returns null if no matches found. Returned if
1928:      *          Horde_Imap_Client::SEARCH_RESULTS_MAX is set.
1929:      *   - min: (integer) The UID (default) or message sequence number (if
1930:      *          'sequence' is true) of the lowest message that satisifies
1931:      *          $criteria. Returns null if no matches found. Returned if
1932:      *          Horde_Imap_Client::SEARCH_RESULTS_MIN is set.
1933:      *   - modseq: (integer) The highest mod-sequence for all messages being
1934:      *            returned. Returned if 'sort' is false, the search query
1935:      *            includes a MODSEQ command, and the server supports the
1936:      *            CONDSTORE IMAP extension.
1937:      *   - relevancy: (array) The list of relevancy scores. Returned if
1938:      *                Horde_Imap_Client::SEARCH_RESULTS_RELEVANCY is set and
1939:      *                the server supports FUZZY search matching.
1940:      *   - save: (boolean) Whether the search results were saved. This value
1941:      *           is meant for internal use only. Returned if 'sort' is false
1942:      *           and Horde_Imap_Client::SEARCH_RESULTS_SAVE is set.
1943:      *
1944:      * @throws Horde_Imap_Client_Exception
1945:      */
1946:     public function search($mailbox, $query = null, $options = array())
1947:     {
1948:         $this->login();
1949: 
1950:         if (empty($options['results'])) {
1951:             $options['results'] = array(
1952:                 Horde_Imap_Client::SEARCH_RESULTS_MATCH,
1953:                 Horde_Imap_Client::SEARCH_RESULTS_COUNT
1954:             );
1955:         }
1956: 
1957:         // Default to an ALL search.
1958:         if (is_null($query)) {
1959:             $query = new Horde_Imap_Client_Search_Query();
1960:         }
1961: 
1962:         $options['_query'] = $query->build($this->capability());
1963: 
1964:         /* RFC 6203: MUST NOT request relevancy results if we are not using
1965:          * FUZZY searching. */
1966:         if (in_array(Horde_Imap_Client::SEARCH_RESULTS_RELEVANCY, $options['results']) &&
1967:             !in_array('SEARCH=FUZZY', $options['_query']['exts_used'])) {
1968:             throw new InvalidArgumentException('Cannot specify RELEVANCY results if not doing a FUZZY search.');
1969:         }
1970: 
1971:         /* Optimization - if query is just for a count of either RECENT or
1972:          * ALL messages, we can send status information instead. Can't
1973:          * optimize with unseen queries because we may cause an infinite loop
1974:          * between here and the status() call. */
1975:         if ((count($options['results']) == 1) &&
1976:             (reset($options['results']) == Horde_Imap_Client::SEARCH_RESULTS_COUNT)) {
1977:             switch ($options['_query']['query']) {
1978:             case 'ALL':
1979:                 $ret = $this->status($this->_selected, Horde_Imap_Client::STATUS_MESSAGES);
1980:                 return array('count' => $ret['messages']);
1981: 
1982:             case 'RECENT':
1983:                 $ret = $this->status($this->_selected, Horde_Imap_Client::STATUS_RECENT);
1984:                 return array('count' => $ret['recent']);
1985:             }
1986:         }
1987: 
1988:         $this->openMailbox($mailbox, Horde_Imap_Client::OPEN_AUTO);
1989: 
1990:         /* Take advantage of search result caching.  If CONDSTORE available,
1991:          * we can cache all queries and invalidate the cache when the MODSEQ
1992:          * changes. If CONDSTORE not available, we can only store queries
1993:          * that don't involve flags. We store results by hashing the options
1994:          * array - the generated query is already added to '_query' key
1995:          * above. */
1996:         $cache = null;
1997:         if (empty($options['nocache']) &&
1998:             $this->_initCache(true) &&
1999:             (isset($this->_init['enabled']['CONDSTORE']) ||
2000:              !$query->flagSearch())) {
2001:             $cache = $this->_getSearchCache('search', $this->_selected, $options);
2002:             if (is_array($cache)) {
2003:                 if (isset($cache['data']['match'])) {
2004:                     $cache['data']['match'] = $this->getIdsOb($cache['data']['match']);
2005:                 }
2006:                 return $cache['data'];
2007:             }
2008:         }
2009: 
2010:         /* Optimization: Catch when there are no messages in a mailbox. */
2011:         $status_res = $this->status($this->_selected, Horde_Imap_Client::STATUS_MESSAGES | Horde_Imap_Client::STATUS_HIGHESTMODSEQ);
2012:         if ($status_res['messages'] ||
2013:             in_array(Horde_Imap_Client::SEARCH_RESULTS_SAVE, $options['results'])) {
2014:             $ret = $this->_search($query, $options);
2015:         } else {
2016:             $ret = array(
2017:                 'count' => 0
2018:             );
2019: 
2020:             foreach ($options['results'] as $val) {
2021:                 switch ($val) {
2022:                 case Horde_Imap_Client::SEARCH_RESULTS_MATCH:
2023:                     $ret['match'] = $this->getIdsOb();
2024:                     break;
2025: 
2026:                 case Horde_Imap_Client::SEARCH_RESULTS_MAX:
2027:                     $ret['max'] = null;
2028:                     break;
2029: 
2030:                 case Horde_Imap_Client::SEARCH_RESULTS_MIN:
2031:                     $ret['min'] = null;
2032:                     break;
2033: 
2034:                 case Horde_Imap_Client::SEARCH_RESULTS_MIN:
2035:                     if (isset($status_res['highestmodseq'])) {
2036:                         $ret['modseq'] = $status_res['highestmodseq'];
2037:                     }
2038:                     break;
2039: 
2040:                 case Horde_Imap_Client::SEARCH_RESULTS_RELEVANCY:
2041:                     $ret['relevancy'] = array();
2042:                     break;
2043:                 }
2044:             }
2045:         }
2046: 
2047:         if ($cache) {
2048:             $save = $ret;
2049:             if (isset($save['match'])) {
2050:                 $save['match'] = $this->utils->toSequenceString($ret['match'], array('nosort' => true));
2051:             }
2052:             $this->_setSearchCache($save, $cache);
2053:         }
2054: 
2055:         return $ret;
2056:     }
2057: 
2058:     /**
2059:      * Search a mailbox.
2060:      *
2061:      * @param object $query   The search query.
2062:      * @param array $options  Additional options. The '_query' key contains
2063:      *                        the value of $query->build().
2064:      *
2065:      * @return Horde_Imap_Client_Ids  An array of IDs.
2066:      *
2067:      * @throws Horde_Imap_Client_Exception
2068:      */
2069:     abstract protected function _search($query, $options);
2070: 
2071:     /**
2072:      * Set the comparator to use for searching/sorting (RFC 5255).
2073:      *
2074:      * @param string $comparator  The comparator string (see RFC 4790 [3.1] -
2075:      *                            "collation-id" - for format). The reserved
2076:      *                            string 'default' can be used to select
2077:      *                            the default comparator.
2078:      *
2079:      * @throws Horde_Imap_Client_Exception
2080:      * @throws Horde_Imap_Client_Exception_NoSupportExtension
2081:      */
2082:     public function setComparator($comparator = null)
2083:     {
2084:         $comp = is_null($comparator)
2085:             ? (empty($this->_params['comparator']) ? null : $this->_params['comparator'])
2086:             : $comparator;
2087:         if (is_null($comp)) {
2088:             return;
2089:         }
2090: 
2091:         $this->login();
2092: 
2093:         $i18n = $this->queryCapability('I18NLEVEL');
2094:         if (empty($i18n) || (max($i18n) < 2)) {
2095:             $this->_exception('The IMAP server does not support changing SEARCH/SORT comparators.', 'NO_SUPPORT');
2096:         }
2097: 
2098:         $this->_setComparator($comp);
2099:     }
2100: 
2101:     /**
2102:      * Set the comparator to use for searching/sorting (RFC 5255).
2103:      *
2104:      * @param string $comparator  The comparator string (see RFC 4790 [3.1] -
2105:      *                            "collation-id" - for format). The reserved
2106:      *                            string 'default' can be used to select
2107:      *                            the default comparator.
2108:      *
2109:      * @throws Horde_Imap_Client_Exception
2110:      */
2111:     abstract protected function _setComparator($comparator);
2112: 
2113:     /**
2114:      * Get the comparator used for searching/sorting (RFC 5255).
2115:      *
2116:      * @return mixed  Null if the default comparator is being used, or an
2117:      *                array of comparator information (see RFC 5255 [4.8]).
2118:      *
2119:      * @throws Horde_Imap_Client_Exception
2120:      */
2121:     public function getComparator()
2122:     {
2123:         $this->login();
2124: 
2125:         $i18n = $this->queryCapability('I18NLEVEL');
2126:         if (empty($i18n) || (max($i18n) < 2)) {
2127:             return null;
2128:         }
2129: 
2130:         return $this->_getComparator();
2131:     }
2132: 
2133:     /**
2134:      * Get the comparator used for searching/sorting (RFC 5255).
2135:      *
2136:      * @return mixed  Null if the default comparator is being used, or an
2137:      *                array of comparator information (see RFC 5255 [4.8]).
2138:      *
2139:      * @throws Horde_Imap_Client_Exception
2140:      */
2141:     abstract protected function _getComparator();
2142: 
2143:     /**
2144:      * Thread sort a given list of messages (RFC 5256).
2145:      *
2146:      * @param mixed $mailbox  The mailbox to query. Either a
2147:      *                        Horde_Imap_Client_Mailbox object (as of 1.2.0)
2148:      *                        or a string (UTF-8).
2149:      * @param array $options  Additional options:
2150:      * <ul>
2151:      *  <li>
2152:      *   criteria: (mixed) The following thread criteria are available:
2153:      *   <ul>
2154:      *    <li>Horde_Imap_Client::THREAD_ORDEREDSUBJECT</li>
2155:      *    <li>Horde_Imap_Client::THREAD_REFERENCES</li>
2156:      *    <li>Horde_Imap_Client::THREAD_REFS</li>
2157:      *    <li>
2158:      *     Other algorithms can be explicitly specified by passing the IMAP
2159:      *     thread algorithm in as a string value.
2160:      *    </li>
2161:      *   </ul>
2162:      *   DEFAULT: Horde_Imap_Client::THREAD_ORDEREDSUBJECT
2163:      *  </li>
2164:      *  <li>
2165:      *   search: (Horde_Imap_Client_Search_Query) The search query.
2166:      *   DEFAULT: All messages in mailbox included in thread sort.
2167:      *  </li>
2168:      *  <li>
2169:      *   sequence: (boolean) If true, each message is stored and referred to
2170:      *   by its message sequence number.
2171:      *   DEFAULT: Stored/referred to by UID.
2172:      *  </li>
2173:      * </ul>
2174:      *
2175:      * @return Horde_Imap_Client_Data_Thread  A thread data object.
2176:      *
2177:      * @throws Horde_Imap_Client_Exception
2178:      */
2179:     public function thread($mailbox, $options = array())
2180:     {
2181:         // Open mailbox call will handle the login.
2182:         $this->openMailbox($mailbox, Horde_Imap_Client::OPEN_AUTO);
2183: 
2184:         /* Take advantage of search result caching.  If CONDSTORE available,
2185:          * we can cache all queries and invalidate the cache when the MODSEQ
2186:          * changes. If CONDSTORE not available, we can only store queries
2187:          * that don't involve flags. See search() for similar caching. */
2188:         $cache = null;
2189:         if ($this->_initCache(true) &&
2190:             (isset($this->_init['enabled']['CONDSTORE']) ||
2191:              empty($options['search']) ||
2192:              !$options['search']->flagSearch())) {
2193:             $cache = $this->_getSearchCache('thread', $this->_selected, $options);
2194:             if (is_array($cache)) {
2195:                 if ($cache['data'] instanceof Horde_Imap_Client_Data_Thread) {
2196:                     return $cache['data'];
2197:                 }
2198:                 $cache = $cache['id'];
2199:             }
2200:         }
2201: 
2202:         $status_res = $this->status($this->_selected, Horde_Imap_Client::STATUS_MESSAGES);
2203: 
2204:         $ob = new Horde_Imap_Client_Data_Thread($status_res['messages'] ? $this->_thread($options) : array(), empty($options['sequence']) ? 'uid' : 'sequence');
2205: 
2206:         if ($cache) {
2207:             $this->_setSearchCache($ob, $cache);
2208:         }
2209: 
2210:         return $ob;
2211:     }
2212: 
2213:     /**
2214:      * Thread sort a given list of messages (RFC 5256).
2215:      *
2216:      * @param array $options  Additional options. See thread().
2217:      *
2218:      * @return array  An array with the following values, one per message,
2219:      *                with the key being either the UID (default) or the
2220:      *                message sequence number (if 'sequence' is true). Values
2221:      *                of each entry:
2222:      *   - b (base): (integer) [OPTIONAL] The ID of the base message. Is not
2223:      *               set, this is the only message in the thread.
2224:      *               DEFAULT: Only message in thread
2225:      *   - l (level): (integer) [OPTIONAL] The thread level of this
2226:      *                message (1 = base).
2227:      *                DEFAULT: 0
2228:      *   - s (subthread): (boolean) [OPTIONAL] Are there more messages in this
2229:      *                    subthread?
2230:      *                    DEFAULT: No
2231:      *
2232:      * @throws Horde_Imap_Client_Exception
2233:      */
2234:     abstract protected function _thread($options);
2235: 
2236:     /**
2237:      * Fetch message data (see RFC 3501 [6.4.5]).
2238:      *
2239:      * @param mixed $mailbox                        The mailbox to search.
2240:      *                                              Either a
2241:      *                                              Horde_Imap_Client_Mailbox
2242:      *                                              object (as of 1.2.0) or a
2243:      *                                              string (UTF-8).
2244:      * @param Horde_Imap_Client_Fetch_Query $query  Fetch query object.
2245:      * @param array $options                        Additional options:
2246:      *   - changedsince: (integer) Only return messages that have a
2247:      *                   mod-sequence larger than this value. This option
2248:      *                   requires the CONDSTORE IMAP extension (if not present,
2249:      *                   this value is ignored). Additionally, the mailbox
2250:      *                   must support mod-sequences or an exception will be
2251:      *                   thrown. If valid, this option implicity adds the
2252:      *                   mod-sequence fetch criteria to the fetch command.
2253:      *                   DEFAULT: Mod-sequence values are ignored.
2254:      *   - fetch_res: (array) A partial results array to have fetch results
2255:      *                added to. [DEPRECATED]
2256:      *   - ids: (Horde_Imap_Client_Ids) A list of messages to fetch data from.
2257:      *          DEFAULT: All messages in $mailbox will be fetched.
2258:      *   - vanished: (boolean) Only return messages from the UID set parameter
2259:      *               that have been expunged and whose associated mod-sequence
2260:      *               is larger than the specified mod-sequence. This option
2261:      *               requires the QRESYNC IMAP extension and requires
2262:      *               'changedsince' to be set, and requires 'ids' to be UIDs.
2263:      *               [DEPRECATED]
2264:      *               DEFAULT: Vanished search ignored.
2265:      *
2266:      * @return array  An array of fetch results. The array consists of
2267:      *                keys that correspond to 'ids', and values that
2268:      *                contain Horde_Imap_Query_Data_Fetch objects.
2269:      *
2270:      * @throws Horde_Imap_Client_Exception
2271:      */
2272:     public function fetch($mailbox, $query, $options = array())
2273:     {
2274:         $this->login();
2275: 
2276:         $query = clone $query;
2277: 
2278:         $cache_array = $header_cache = $new_query = array();
2279:         $qresync = isset($this->_init['enabled']['QRESYNC']);
2280:         $res_seq = null;
2281: 
2282:         if (empty($options['ids'])) {
2283:             $options['ids'] = $this->getIdsOb(empty($options['fetch_res']) ? Horde_Imap_Client_Ids::ALL : array_keys($options['fetch_res']));
2284:             if ($options['ids']->isEmpty()) {
2285:                 return array();
2286:             }
2287:         } elseif ($options['ids']->isEmpty()) {
2288:             return array();
2289:         } elseif ($options['ids']->search_res &&
2290:                   !$this->queryCapability('SEARCHRES')) {
2291:             /* SEARCHRES requires server support. */
2292:             $this->_exception('Server does not support saved searches.', 'NO_SUPPORT');
2293:         }
2294: 
2295:         /* The 'vanished' modifier requires QRESYNC, 'changedsince', and IDs
2296:          * that are not sequence numbers. */
2297:         if (!empty($options['vanished'])) {
2298:             if (!$qresync) {
2299:                 $this->_exception('Server does not support the QRESYNC extension.', 'NO_SUPPORT');
2300:             } elseif ($options['ids']->sequence) {
2301:                 throw new InvalidArgumentException('The vanished FETCH modifier requires UIDs.');
2302:             } elseif (empty($options['changedsince'])) {
2303:                 throw new InvalidArgumentException('The vanished FETCH modifier requires the changedsince parameter.');
2304:             }
2305:         }
2306: 
2307:         $this->openMailbox($mailbox, Horde_Imap_Client::OPEN_AUTO);
2308: 
2309:         $cf = $this->_initCache(true)
2310:             ? $this->_params['cache']['fields']
2311:             : array();
2312: 
2313:         if (!empty($cf)) {
2314:             /* We need the UIDVALIDITY for the current mailbox. */
2315:             $status_res = $this->status($this->_selected, Horde_Imap_Client::STATUS_HIGHESTMODSEQ | Horde_Imap_Client::STATUS_UIDVALIDITY);
2316: 
2317:             /* If using cache, we store by UID so we need to return UIDs. */
2318:             $query->uid();
2319:         }
2320: 
2321:         if ($query->contains(Horde_Imap_Client::FETCH_MODSEQ) &&
2322:             !isset($this->_init['enabled']['CONDSTORE'])) {
2323:             unset($query[$k]);
2324:         }
2325: 
2326:         /* Determine if caching is available and if anything in $query is
2327:          * cacheable.
2328:          * TODO: Re-add base headertext caching. */
2329:         foreach ($cf as $k => $v) {
2330:             if (isset($query[$k])) {
2331:                 switch ($k) {
2332:                 case Horde_Imap_Client::FETCH_ENVELOPE:
2333:                 case Horde_Imap_Client::FETCH_IMAPDATE:
2334:                 case Horde_Imap_Client::FETCH_SIZE:
2335:                 case Horde_Imap_Client::FETCH_STRUCTURE:
2336:                     $cache_array[$k] = $v;
2337:                     break;
2338: 
2339:                 case Horde_Imap_Client::FETCH_FLAGS:
2340:                     /* QRESYNC would have already done syncing on mailbox
2341:                      * open, so no need to do again. Only can cache if MODSEQ
2342:                      * is available in the mailbox. */
2343:                     if (!$qresync && !empty($status_res['highestmodseq'])) {
2344:                         /* Grab all flags updated since the cached modseq
2345:                          * val. */
2346:                         $metadata = $this->cache->getMetaData($this->_selected, $status_res['uidvalidity'], array(self::CACHE_MODSEQ));
2347:                         if (isset($metadata[self::CACHE_MODSEQ]) &&
2348:                             ($metadata[self::CACHE_MODSEQ] != $status_res['highestmodseq'])) {
2349:                             $uids = $this->cache->get($this->_selected, array(), array(), $status_res['uidvalidity']);
2350:                             if (!empty($uids)) {
2351:                                 $flag_query = new Horde_Imap_Client_Fetch_Query();
2352:                                 $flag_query->flags();
2353: 
2354:                                 /* Update flags in cache. */
2355:                                 $this->_fetch($flag_query, array(), array(
2356:                                     'changedsince' => $metadata[self::CACHE_MODSEQ],
2357:                                     'ids' => $this->getIdsOb($uids)
2358:                                 ));
2359:                             }
2360:                             $this->_updateMetaData($this->_selected, array(self::CACHE_MODSEQ => $status_res['highestmodseq']), $status_res['uidvalidity']);
2361:                         }
2362:                     }
2363: 
2364:                     $cache_array[$k] = $v;
2365:                     break;
2366: 
2367:                 case Horde_Imap_Client::FETCH_HEADERS:
2368:                     $this->_temp['headers_caching'] = array();
2369: 
2370:                     foreach ($query[$k] as $key => $val) {
2371:                         /* Only cache if directly requested.  Iterate through
2372:                          * requests to ensure at least one can be cached. */
2373:                         if (!empty($val['cache']) && !empty($val['peek'])) {
2374:                             $cache_array[$k] = $v;
2375:                             ksort($val);
2376:                             $header_cache[$key] = hash('md5', serialize($val));
2377:                         }
2378:                     }
2379:                     break;
2380:                 }
2381:             }
2382:         }
2383: 
2384:         /* Build the default fetch entries. */
2385:         if (empty($options['fetch_res'])) {
2386:             $fetch_ob = new $this->_fetchDataClass();
2387:             $ret = array();
2388: 
2389:             $res_seq = $this->_getSeqUidLookup($options['ids']);
2390:             $ids = $options['ids']->sequence
2391:                 ? array_keys($res_seq['lookup'])
2392:                 : $res_seq['uids'];
2393: 
2394:             foreach ($ids as $val) {
2395:                 $ret[$val] = clone $fetch_ob;
2396:             }
2397:         } else {
2398:             $ret = &$options['fetch_res'];
2399:         }
2400: 
2401:         /* If nothing is cacheable, we can do a straight search. */
2402:         if (empty($cache_array)) {
2403:             $ret = $this->_fetch($query, $ret, $options);
2404:             foreach ($ret as $key => $val) {
2405:                 if ($val->isDefault()) {
2406:                     unset($ret[$key]);
2407:                 }
2408:             }
2409:             return $ret;
2410:         }
2411: 
2412:         /* If doing a changedsince/vanished search, limit the UIDs now. */
2413:         if (!empty($options['changedsince'])) {
2414:             $changed_query = new Horde_Imap_Client_Fetch_Query();
2415:             if (empty($options['vanished']) && $options['ids']->sequence) {
2416:                 $changed_query->seq();
2417:             } else {
2418:                 $changed_query->uid();
2419:             }
2420: 
2421:             $cs_res = $this->_fetch($changed_query, array(), array(
2422:                 'changedsince' => $options['changedsince'],
2423:                 'ids' => $options['ids'],
2424:                 'vanished' => !empty($options['vanished'])
2425:             ));
2426: 
2427:             if (!empty($options['vanished'])) {
2428:                 return $cs_res;
2429:             }
2430: 
2431:             $ret = array_intersect_key($ret, $cs_res);
2432:             if (empty($ret)) {
2433:                 return $ret;
2434:             }
2435: 
2436:             $options['ids'] = $this->getIdsOb(array_keys($ret), $options['ids']->sequence);
2437:         }
2438: 
2439:         /* Need Seq -> UID lookup if we haven't already grabbed it. */
2440:         if (is_null($res_seq)) {
2441:             $res_seq = $this->_getSeqUidLookup($options['ids']);
2442:         }
2443: 
2444:         /* Get the cached values. */
2445:         $data = $this->cache->get($this->_selected, $res_seq['uids']->ids, array_values($cache_array), $status_res['uidvalidity']);
2446: 
2447:         /* Build a list of what we still need. */
2448:         foreach (array_keys($ret) as $val) {
2449:             $crit = clone $query;
2450: 
2451:             if ($options['ids']->sequence) {
2452:                 $uid = $res_seq['lookup'][$val];
2453:                 unset($crit[Horde_Imap_Client::FETCH_SEQ]);
2454:             } else {
2455:                 $uid = $val;
2456:             }
2457: 
2458:             /* UID will be added into the results object below. */
2459:             unset($crit[Horde_Imap_Client::FETCH_UID]);
2460: 
2461:             foreach ($cache_array as $key => $cid) {
2462:                 switch ($key) {
2463:                 case Horde_Imap_Client::FETCH_ENVELOPE:
2464:                     if (isset($data[$uid][$cid]) &&
2465:                         ($data[$uid][$cid] instanceof Horde_Imap_Client_Data_Envelope)) {
2466:                         $ret[$val]->setEnvelope($data[$uid][$cid]);
2467:                         unset($crit[$key]);
2468:                     }
2469:                     break;
2470: 
2471:                 case Horde_Imap_Client::FETCH_FLAGS:
2472:                     if (isset($data[$uid][$cid]) &&
2473:                         is_array($data[$uid][$cid])) {
2474:                         $ret[$val]->setFlags($data[$uid][$cid]);
2475:                         unset($crit[$key]);
2476:                     }
2477:                     break;
2478: 
2479:                 case Horde_Imap_Client::FETCH_HEADERS:
2480:                     /* HEADERS caching. */
2481:                     foreach ($header_cache as $hkey => $hval) {
2482:                         if (isset($data[$uid][$cid][$hval])) {
2483:                             /* We have found a cached entry with the same MD5
2484:                              * sum. */
2485:                             $ret[$val]->setHeaders($hkey, $data[$uid][$cid][$hval]);
2486:                             $crit->remove($key, $hkey);
2487:                         } else {
2488:                             $this->_temp['headers_caching'][$hkey] = $hval;
2489:                         }
2490:                     }
2491:                     break;
2492: 
2493:                 case Horde_Imap_Client::FETCH_IMAPDATE:
2494:                     if (isset($data[$uid][$cid]) &&
2495:                         ($data[$uid][$cid] instanceof Horde_Imap_Client_DateTime)) {
2496:                         $ret[$val]->setImapDate($data[$uid][$cid]);
2497:                         unset($crit[$key]);
2498:                     }
2499:                     break;
2500: 
2501:                 case Horde_Imap_Client::FETCH_SIZE:
2502:                     if (isset($data[$uid][$cid])) {
2503:                         $ret[$val]->setSize($data[$uid][$cid]);
2504:                         unset($crit[$key]);
2505:                     }
2506:                     break;
2507: 
2508:                 case Horde_Imap_Client::FETCH_STRUCTURE:
2509:                     if (isset($data[$uid][$cid]) &&
2510:                         ($data[$uid][$cid] instanceof Horde_Mime_Part)) {
2511:                         $ret[$val]->setStructure($data[$uid][$cid]);
2512:                         unset($crit[$key]);
2513:                     }
2514:                     break;
2515:                 }
2516:             }
2517: 
2518:             if (count($crit)) {
2519:                 $sig = $crit->hash();
2520:                 if (isset($new_query[$sig])) {
2521:                     $new_query[$sig]['i']->add($val);
2522:                 } else {
2523:                     $new_query[$sig] = array(
2524:                         'c' => $crit,
2525:                         'i' => $this->getIdsOb($val, $options['ids']->sequence)
2526:                     );
2527:                 }
2528:             }
2529:         }
2530: 
2531:         foreach ($new_query as $val) {
2532:             $ret = $this->_fetch($val['c'], $ret, array_merge($options, array(
2533:                 'ids' => $val['i']
2534:             )));
2535:         }
2536: 
2537:         foreach ($ret as $key => $val) {
2538:             if ($val->isDefault() && !empty($new_query)) {
2539:                 /* If $new_query is empty, this means that the fetch requested
2540:                  * was for UIDs only. Need to add that info below. */
2541:                 unset($ret[$key]);
2542:             } elseif ($options['ids']->sequence) {
2543:                 $ret[$key]->setSeq($key);
2544:                 $ret[$key]->setUid($res_seq['lookup'][$key]);
2545:             } else {
2546:                 $ret[$key]->setUid($key);
2547:             }
2548:         }
2549: 
2550:         return $ret;
2551:     }
2552: 
2553:     /**
2554:      * Fetch message data.
2555:      *
2556:      * @param Horde_Imap_Client_Fetch_Query $query  Fetch query object.
2557:      * @param array $results                        Partial results.
2558:      * @param array $options                        Additional options.
2559:      *
2560:      * @return array  An array of fetch results. The array consists of
2561:      *                keys that correspond to 'ids', and values that
2562:      *                contain Horde_Imap_Query_Data_Fetch objects.
2563:      *
2564:      * @throws Horde_Imap_Client_Exception
2565:      */
2566:     abstract protected function _fetch($query, $results, $options);
2567: 
2568:     /**
2569:      * Store message flag data (see RFC 3501 [6.4.6]).
2570:      *
2571:      * @param mixed $mailbox  The mailbox containing the messages to modify.
2572:      *                        Either a Horde_Imap_Client_Mailbox object (as of
2573:      *                        1.2.0) or a string (UTF-8).
2574:      * @param array $options  Additional options:
2575:      *   - add: (array) An array of flags to add.
2576:      *          DEFAULT: No flags added.
2577:      *   - ids: (Horde_Imap_Client_Ids) The list of messages to modify.
2578:      *          DEFAULT: All messages in $mailbox will be modified.
2579:      *   - remove: (array) An array of flags to remove.
2580:      *             DEFAULT: No flags removed.
2581:      *   - replace: (array) Replace the current flags with this set
2582:      *              of flags. Overrides both the 'add' and 'remove' options.
2583:      *              DEFAULT: No replace is performed.
2584:      *   - unchangedsince: (integer) Only changes flags if the mod-sequence ID
2585:      *                     of the message is equal or less than this value.
2586:      *                     Requires the CONDSTORE IMAP extension on the server.
2587:      *                     Also requires the mailbox to support mod-sequences.
2588:      *                     Will throw an exception if either condition is not
2589:      *                     met.
2590:      *                     DEFAULT: mod-sequence is ignored when applying
2591:      *                              changes
2592:      *
2593:      * @return Horde_Imap_Client_Ids  A Horde_Imap_Client_Ids object
2594:      *                                containing the list of IDs that failed
2595:      *                                the 'unchangedsince' test.
2596:      *
2597:      * @throws Horde_Imap_Client_Exception
2598:      */
2599:     public function store($mailbox, $options = array())
2600:     {
2601:         // Open mailbox call will handle the login.
2602:         $this->openMailbox($mailbox, Horde_Imap_Client::OPEN_READWRITE);
2603: 
2604:         /* SEARCHRES requires server support. */
2605:         if (empty($options['ids'])) {
2606:             $options['ids'] = $this->getIdsOb(Horde_Imap_Client_Ids::ALL);
2607:         } elseif ($options['ids']->isEmpty()) {
2608:             return $this->getIdsOb();
2609:         } elseif ($options['ids']->search_res &&
2610:                   !$this->queryCapability('SEARCHRES')) {
2611:             $this->_exception('Server does not support saved searches.', 'NO_SUPPORT');
2612:         }
2613: 
2614:         if (!empty($options['unchangedsince']) &&
2615:             !isset($this->_init['enabled']['CONDSTORE'])) {
2616:             $this->_exception('Server does not support the CONDSTORE extension.', 'NO_SUPPORT');
2617:         }
2618: 
2619:         return $this->_store($options);
2620:     }
2621: 
2622:     /**
2623:      * Store message flag data.
2624:      *
2625:      * @param array $options  Additional options.
2626:      *
2627:      * @return Horde_Imap_Client_Ids  A Horde_Imap_Client_Ids object
2628:      *                                containing the list of IDs that failed
2629:      *                                the 'unchangedsince' test.
2630:      *
2631:      * @throws Horde_Imap_Client_Exception
2632:      */
2633:     abstract protected function _store($options);
2634: 
2635:     /**
2636:      * Copy messages to another mailbox.
2637:      *
2638:      * @param mixed $source   The source mailbox. Either a
2639:      *                        Horde_Imap_Client_Mailbox object (as of 1.2.0)
2640:      *                        or a string (UTF-8).
2641:      * @param mixed $dest     The destination mailbox. Either a
2642:      *                        Horde_Imap_Client_Mailbox object (as of 1.2.0)
2643:      *                        or a string (UTF-8).
2644:      * @param array $options  Additional options:
2645:      *   - create: (boolean) Try to create $dest if it does not exist?
2646:      *             DEFAULT: No.
2647:      *   - ids: (Horde_Imap_Client_Ids) The list of messages to copy.
2648:      *          DEFAULT: All messages in $mailbox will be copied.
2649:      *   - move: (boolean) If true, delete the original messages.
2650:      *           DEFAULT: Original messages are not deleted.
2651:      *
2652:      * @return mixed  An array mapping old UIDs (keys) to new UIDs (values) on
2653:      *                success (if the IMAP server and/or driver support the
2654:      *                UIDPLUS extension) or true.
2655:      *
2656:      * @throws Horde_Imap_Client_Exception
2657:      */
2658:     public function copy($source, $dest, $options = array())
2659:     {
2660:         // Open mailbox call will handle the login.
2661:         $this->openMailbox($source, empty($options['move']) ? Horde_Imap_Client::OPEN_AUTO : Horde_Imap_Client::OPEN_READWRITE);
2662: 
2663:         /* SEARCHRES requires server support. */
2664:         if (empty($options['ids'])) {
2665:             $options['ids'] = $this->getIdsOb(Horde_Imap_Client_Ids::ALL);
2666:         } elseif ($options['ids']->isEmpty()) {
2667:             return array();
2668:         } elseif ($options['ids']->search_res &&
2669:                   !$this->queryCapability('SEARCHRES')) {
2670:             $this->_exception('Server does not support saved searches.', 'NO_SUPPORT');
2671:         }
2672: 
2673:         return $this->_copy(Horde_Imap_Client_Mailbox::get($dest, null), $options);
2674:     }
2675: 
2676:     /**
2677:      * Copy messages to another mailbox.
2678:      *
2679:      * @param Horde_Imap_Client_Mailbox $dest  The destination mailbox.
2680:      * @param array $options                   Additional options.
2681:      *
2682:      * @return mixed  An array mapping old UIDs (keys) to new UIDs (values) on
2683:      *                success (if the IMAP server and/or driver support the
2684:      *                UIDPLUS extension) or true.
2685:      *
2686:      * @throws Horde_Imap_Client_Exception
2687:      */
2688:     abstract protected function _copy(Horde_Imap_Client_Mailbox $dest,
2689:                                       $options);
2690: 
2691:     /**
2692:      * Set quota limits. The server must support the IMAP QUOTA extension
2693:      * (RFC 2087).
2694:      *
2695:      * @param mixed $root       The quota root. Either a
2696:      *                          Horde_Imap_Client_Mailbox object (as of 1.2.0)
2697:      *                          or a string (UTF-8).
2698:      * @param array $resources  The resource values to set. Keys are the
2699:      *                          resource atom name; value is the resource
2700:      *                          value.
2701:      *
2702:      * @throws Horde_Imap_Client_Exception
2703:      */
2704:     public function setQuota($root, array $resources = array())
2705:     {
2706:         $this->login();
2707: 
2708:         if (!$this->queryCapability('QUOTA')) {
2709:             $this->_exception('Server does not support the QUOTA extension.', 'NO_SUPPORT');
2710:         }
2711: 
2712:         if (!empty($resources)) {
2713:             $this->_setQuota(Horde_Imap_Client_Mailbox::get($root, null), $resources);
2714:         }
2715:     }
2716: 
2717:     /**
2718:      * Set quota limits.
2719:      *
2720:      * @param Horde_Imap_Client_Mailbox $root  The quota root.
2721:      * @param array $resources                 The resource values to set.
2722:      *
2723:      * @return boolean  True on success.
2724:      *
2725:      * @throws Horde_Imap_Client_Exception
2726:      */
2727:     abstract protected function _setQuota(Horde_Imap_Client_Mailbox $root,
2728:                                           $resources);
2729: 
2730:     /**
2731:      * Get quota limits. The server must support the IMAP QUOTA extension
2732:      * (RFC 2087).
2733:      *
2734:      * @param mixed $root  The quota root. Either a Horde_Imap_Client_Mailbox
2735:      *                     object (as of 1.2.0) or a string (UTF-8).
2736:      *
2737:      * @return mixed  An array with resource keys. Each key holds an array
2738:      *                with 2 values: 'limit' and 'usage'.
2739:      *
2740:      * @throws Horde_Imap_Client_Exception
2741:      */
2742:     public function getQuota($root)
2743:     {
2744:         $this->login();
2745: 
2746:         if (!$this->queryCapability('QUOTA')) {
2747:             $this->_exception('Server does not support the QUOTA extension.', 'NO_SUPPORT');
2748:         }
2749: 
2750:         return $this->_getQuota(Horde_Imap_Client_Mailbox::get($root, null));
2751:     }
2752: 
2753:     /**
2754:      * Get quota limits.
2755:      *
2756:      * @param Horde_Imap_Client_Mailbox $root  The quota root.
2757:      *
2758:      * @return mixed  An array with resource keys. Each key holds an array
2759:      *                with 2 values: 'limit' and 'usage'.
2760:      *
2761:      * @throws Horde_Imap_Client_Exception
2762:      */
2763:     abstract protected function _getQuota(Horde_Imap_Client_Mailbox $root);
2764: 
2765:     /**
2766:      * Get quota limits for a mailbox. The server must support the IMAP QUOTA
2767:      * extension (RFC 2087).
2768:      *
2769:      * @param mixed $mailbox  A mailbox. Either a Horde_Imap_Client_Mailbox
2770:      *                        object (as of 1.2.0) or a string (UTF-8).
2771:      *
2772:      * @return mixed  An array with the keys being the quota roots. Each key
2773:      *                holds an array with resource keys: each of these keys
2774:      *                holds an array with 2 values: 'limit' and 'usage'.
2775:      *
2776:      * @throws Horde_Imap_Client_Exception
2777:      */
2778:     public function getQuotaRoot($mailbox)
2779:     {
2780:         $this->login();
2781: 
2782:         if (!$this->queryCapability('QUOTA')) {
2783:             $this->_exception('Server does not support the QUOTA extension.', 'NO_SUPPORT');
2784:         }
2785: 
2786:         return $this->_getQuotaRoot(Horde_Imap_Client_Mailbox::get($mailbox, null));
2787:     }
2788: 
2789:     /**
2790:      * Get quota limits for a mailbox.
2791:      *
2792:      * @param Horde_Imap_Client_Mailbox $mailbox  A mailbox.
2793:      *
2794:      * @return mixed  An array with the keys being the quota roots. Each key
2795:      *                holds an array with resource keys: each of these keys
2796:      *                holds an array with 2 values: 'limit' and 'usage'.
2797:      *
2798:      * @throws Horde_Imap_Client_Exception
2799:      */
2800:     abstract protected function _getQuotaRoot(Horde_Imap_Client_Mailbox $mailbox);
2801: 
2802:     /**
2803:      * Get the ACL rights for a given mailbox. The server must support the
2804:      * IMAP ACL extension (RFC 2086/4314).
2805:      *
2806:      * @param mixed $mailbox  A mailbox. Either a Horde_Imap_Client_Mailbox
2807:      *                        object (as of 1.2.0) or a string (UTF-8).
2808:      *
2809:      * @return array  An array with identifiers as the keys and
2810:      *                Horde_Imap_Client_Data_Acl objects as the values.
2811:      *
2812:      * @throws Horde_Imap_Client_Exception
2813:      */
2814:     public function getACL($mailbox)
2815:     {
2816:         $this->login();
2817:         return $this->_getACL(Horde_Imap_Client_Mailbox::get($mailbox, null));
2818:     }
2819: 
2820:     /**
2821:      * Get ACL rights for a given mailbox.
2822:      *
2823:      * @param Horde_Imap_Client_Mailbox $mailbox  A mailbox.
2824:      *
2825:      * @return array  An array with identifiers as the keys and
2826:      *                Horde_Imap_Client_Data_Acl objects as the values.
2827:      *
2828:      * @throws Horde_Imap_Client_Exception
2829:      */
2830:     abstract protected function _getACL(Horde_Imap_Client_Mailbox $mailbox);
2831: 
2832:     /**
2833:      * Set ACL rights for a given mailbox/identifier.
2834:      *
2835:      * @param mixed $mailbox      A mailbox. Either a Horde_Imap_Client_Mailbox
2836:      *                            object (as of 1.2.0) or a string (UTF-8).
2837:      * @param string $identifier  The identifier to alter (UTF-8).
2838:      * @param array $options      Additional options:
2839:      *   - remove: (boolean) If true, removes rights for $identifier.
2840:      *             DEFAULT: false
2841:      *   - rights: (string) The rights to alter.
2842:      *             DEFAULT: If 'remove' is true, removes all rights. If
2843:      *                      'remove' is false, no rights are altered.
2844:      *
2845:      * @throws Horde_Imap_Client_Exception
2846:      */
2847:     public function setACL($mailbox, $identifier, $options)
2848:     {
2849:         $this->login();
2850: 
2851:         if (!$this->queryCapability('ACL')) {
2852:             $this->_exception('Server does not support the ACL extension.', 'NO_SUPPORT');
2853:         }
2854: 
2855:         if (!empty($options['rights'])) {
2856:             $acl = ($options['rights'] instanceof Horde_Imap_Client_Data_Acl)
2857:                 ? $options['rights']
2858:                 : new Horde_Imap_Client_Data_Acl(strval($options['rights']));
2859: 
2860:             $options['rights'] =
2861:                 (empty($options['remove']) ? '+' : '-') .
2862:                 $acl->getString($this->queryCapability('RIGHTS') ? Horde_Imap_Client_Data_AclCommon::RFC_4314 : Horde_Imap_Client_Data_AclCommon::RFC_2086);
2863:         }
2864: 
2865:         if (empty($options['rights']) && empty($options['remove'])) {
2866:             return;
2867:         }
2868: 
2869:         return $this->_setACL(
2870:             Horde_Imap_Client_Mailbox::get($mailbox, null),
2871:             Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($identifier, true),
2872:             $options
2873:         );
2874:     }
2875: 
2876:     /**
2877:      * Set ACL rights for a given mailbox/identifier.
2878:      *
2879:      * @param Horde_Imap_Client_Mailbox $mailbox  A mailbox.
2880:      * @param string $identifier                  The identifier to alter
2881:      *                                            (UTF7-IMAP).
2882:      * @param array $options                      Additional options. 'rights'
2883:      *                                            contains the string of
2884:      *                                            rights to set on the server.
2885:      *
2886:      * @throws Horde_Imap_Client_Exception
2887:      */
2888:     abstract protected function _setACL(Horde_Imap_Client_Mailbox $mailbox,
2889:                                         $identifier, $options);
2890: 
2891:     /**
2892:      * List the ACL rights for a given mailbox/identifier. The server must
2893:      * support the IMAP ACL extension (RFC 2086/4314).
2894:      *
2895:      * @param mixed $mailbox      A mailbox. Either a Horde_Imap_Client_Mailbox
2896:      *                            object (as of 1.2.0) or a string (UTF-8).
2897:      * @param string $identifier  The identifier to query (UTF-8).
2898:      *
2899:      * @return Horde_Imap_Client_Data_AclRights  An ACL data rights object.
2900:      *
2901:      * @throws Horde_Imap_Client_Exception
2902:      */
2903:     public function listACLRights($mailbox, $identifier)
2904:     {
2905:         $this->login();
2906: 
2907:         if (!$this->queryCapability('ACL')) {
2908:             $this->_exception('Server does not support the ACL extension.', 'NO_SUPPORT');
2909:         }
2910: 
2911:         return $this->_listACLRights(
2912:             Horde_Imap_Client_Mailbox::get($mailbox, null),
2913:             Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($identifier, true)
2914:         );
2915:     }
2916: 
2917:     /**
2918:      * Get ACL rights for a given mailbox/identifier.
2919:      *
2920:      * @param Horde_Imap_Client_Mailbox $mailbox  A mailbox.
2921:      * @param string $identifier                  The identifier to query
2922:      *                                            (UTF7-IMAP).
2923:      *
2924:      * @return Horde_Imap_Client_Data_AclRights  An ACL data rights object.
2925:      *
2926:      * @throws Horde_Imap_Client_Exception
2927:      */
2928:     abstract protected function _listACLRights(Horde_Imap_Client_Mailbox $mailbox,
2929:                                                $identifier);
2930: 
2931:     /**
2932:      * Get the ACL rights for the current user for a given mailbox. The
2933:      * server must support the IMAP ACL extension (RFC 2086/4314).
2934:      *
2935:      * @param mixed $mailbox  A mailbox. Either a Horde_Imap_Client_Mailbox
2936:      *                        object (as of 1.2.0) or a string (UTF-8).
2937:      *
2938:      * @return Horde_Imap_Client_Data_Acl  An ACL data object.
2939:      *
2940:      * @throws Horde_Imap_Client_Exception
2941:      */
2942:     public function getMyACLRights($mailbox)
2943:     {
2944:         $this->login();
2945: 
2946:         if (!$this->queryCapability('ACL')) {
2947:             $this->_exception('Server does not support the ACL extension.', 'NO_SUPPORT');
2948:         }
2949: 
2950:         return $this->_getMyACLRights(Horde_Imap_Client_Mailbox::get($mailbox, null));
2951:     }
2952: 
2953:     /**
2954:      * Get the ACL rights for the current user for a given mailbox.
2955:      *
2956:      * @param Horde_Imap_Client_Mailbox $mailbox  A mailbox.
2957:      *
2958:      * @return Horde_Imap_Client_Data_Acl  An ACL data object.
2959:      *
2960:      * @throws Horde_Imap_Client_Exception
2961:      */
2962:     abstract protected function _getMyACLRights(Horde_Imap_Client_Mailbox $mailbox);
2963: 
2964:     /**
2965:      * Return master list of ACL rights available on the server.
2966:      *
2967:      * @return array  A list of ACL rights.
2968:      */
2969:     public function allAclRights()
2970:     {
2971:         $this->login();
2972: 
2973:         $rights = array(
2974:             Horde_Imap_Client::ACL_LOOKUP,
2975:             Horde_Imap_Client::ACL_READ,
2976:             Horde_Imap_Client::ACL_SEEN,
2977:             Horde_Imap_Client::ACL_WRITE,
2978:             Horde_Imap_Client::ACL_INSERT,
2979:             Horde_Imap_Client::ACL_POST,
2980:             Horde_Imap_Client::ACL_ADMINISTER
2981:         );
2982: 
2983:         if ($capability = $this->queryCapability('RIGHTS')) {
2984:             // Add rights defined in CAPABILITY string (RFC 4314).
2985:             return array_merge($rights, str_split(reset($capability)));
2986:         }
2987: 
2988:         // Add RFC 2086 rights (DEPRECATED)
2989:         return array_merge($rights, array(
2990:             Horde_Imap_Client::ACL_CREATE,
2991:             Horde_Imap_Client::ACL_DELETE
2992:         ));
2993:     }
2994: 
2995:     /**
2996:      * Get metadata for a given mailbox. The server must support either the
2997:      * IMAP METADATA extension (RFC 5464) or the ANNOTATEMORE extension
2998:      * (http://ietfreport.isoc.org/idref/draft-daboo-imap-annotatemore/).
2999:      *
3000:      * @param mixed $mailbox  A mailbox. Either a Horde_Imap_Client_Mailbox
3001:      *                        object (as of 1.2.0) or a string (UTF-8).
3002:      * @param array $entries  The entries to fetch (UTF-8 strings).
3003:      * @param array $options  Additional options:
3004:      *   - depth: (string) Either "0", "1" or "infinity". Returns only the
3005:      *            given value (0), only values one level below the specified
3006:      *            value (1) or all entries below the specified value
3007:      *            (infinity).
3008:      *   - maxsize: (integer) The maximal size the returned values may have.
3009:      *              DEFAULT: No maximal size.
3010:      *
3011:      * @return array  An array with metadata names as the keys and metadata
3012:      *                values as the values. If 'maxsize' is set, and entries
3013:      *                exist on the server larger than this size, the size will
3014:      *                be returned in the key '*longentries'.
3015:      *
3016:      * @throws Horde_Imap_Client_Exception
3017:      */
3018:     public function getMetadata($mailbox, $entries, $options = array())
3019:     {
3020:         $this->login();
3021: 
3022:         if (!is_array($entries)) {
3023:             $entries = array($entries);
3024:         }
3025: 
3026:         return $this->_getMetadata(Horde_Imap_Client_Mailbox::get($mailbox, null), array_map(array('Horde_Imap_Client_Utf7imap', 'Utf8ToUtf7Imap'), $entries, array_fill(0, count($entries), null)), $options);
3027:     }
3028: 
3029:     /**
3030:      * Get metadata for a given mailbox.
3031:      *
3032:      * @param Horde_Imap_Client_Mailbox $mailbox  A mailbox.
3033:      * @param array $entries                      The entries to fetch
3034:      *                                            (UTF7-IMAP strings).
3035:      * @param array $options                      Additional options.
3036:      *
3037:      * @return array  An array with metadata names as the keys and metadata
3038:      *                values as the values.
3039:      *
3040:      * @throws Horde_Imap_Client_Exception
3041:      */
3042:     abstract protected function _getMetadata(Horde_Imap_Client_Mailbox $mailbox,
3043:                                              $entries, $options);
3044: 
3045:     /**
3046:      * Set metadata for a given mailbox/identifier.
3047:      *
3048:      * @param mixed $mailbox  A mailbox. Either a Horde_Imap_Client_Mailbox
3049:      *                        object (as of 1.2.0) or a string (UTF-8). If
3050:      *                        empty, sets a server annotation.
3051:      * @param array $data     A set of data values. The metadata values
3052:      *                        corresponding to the keys of the array will
3053:      *                        be set to the values in the array.
3054:      *
3055:      * @throws Horde_Imap_Client_Exception
3056:      */
3057:     public function setMetadata($mailbox, $data)
3058:     {
3059:         $this->login();
3060:         $this->_setMetadata(Horde_Imap_Client_Mailbox::get($mailbox, null), $data);
3061:     }
3062: 
3063:     /**
3064:      * Set metadata for a given mailbox/identifier.
3065:      *
3066:      * @param Horde_Imap_Client_Mailbox $mailbox  A mailbox.
3067:      * @param array $data                         A set of data values. See
3068:      *                                            setMetaData() for format.
3069:      *
3070:      * @throws Horde_Imap_Client_Exception
3071:      */
3072:     abstract protected function _setMetadata(Horde_Imap_Client_Mailbox $mailbox,
3073:                                              $data);
3074: 
3075:     /* Public utility functions. */
3076: 
3077:     /**
3078:      * Returns a unique identifier for the current mailbox status.
3079:      *
3080:      * @param mixed $mailbox  A mailbox. Either a Horde_Imap_Client_Mailbox
3081:      *                        object (as of 1.2.0) or a string (UTF-8).
3082:      * @param array $addl     Additional cache info to add to the cache ID
3083:      *                        string.
3084:      *
3085:      * @return string  The cache ID string, which will change when the
3086:      *                 composition of the mailbox changes. The uidvalidity
3087:      *                 will always be the first element, and will be delimited
3088:      *                 by the '|' character.
3089:      *
3090:      * @throws Horde_Imap_Client_Exception
3091:      */
3092:     public function getCacheId($mailbox, $addl = array())
3093:     {
3094:         $query = Horde_Imap_Client::STATUS_UIDVALIDITY | Horde_Imap_Client::STATUS_MESSAGES | Horde_Imap_Client::STATUS_UIDNEXT;
3095: 
3096:         /* Use MODSEQ as cache ID if CONDSTORE extension is available. */
3097:         if (isset($this->_init['enabled']['CONDSTORE'])) {
3098:             $query |= Horde_Imap_Client::STATUS_HIGHESTMODSEQ;
3099:         }
3100: 
3101:         $status = $this->status($mailbox, $query);
3102: 
3103:         if (!empty($status['highestmodseq'])) {
3104:             $parts = array(
3105:                 'V' . $status['uidvalidity'],
3106:                 'H' . $status['highestmodseq']
3107:             );
3108:         } else {
3109:             if (empty($status['uidnext'])) {
3110:                 /* UIDNEXT is not strictly required on mailbox open. If it is
3111:                  * not available, use the last UID + 1 in the mailbox instead
3112:                  * (or 0 if mailbox is empty). */
3113:                 if (empty($status['messages'])) {
3114:                     $status['uidnext'] = 0;
3115:                 } else {
3116:                     $this->_temp['nocache'] = true;
3117:                     $search_res = $this->_getSeqUidLookup($this->getIdsOb($status['messages'], true));
3118:                     unset($this->_temp['nocache']);
3119:                     $uids = $search_res['uids']->ids;
3120:                     $status['uidnext'] = intval(end($uids)) + 1;
3121:                 }
3122:             }
3123: 
3124:             $parts = array(
3125:                 'V' . $status['uidvalidity'],
3126:                 'U' . $status['uidnext'],
3127:                 'M' . $status['messages']
3128:             );
3129:         }
3130: 
3131:         return implode('|', array_merge($parts, $addl));
3132:     }
3133: 
3134:     /**
3135:      * Parses a cacheID created by getCacheId().
3136:      *
3137:      * @param string $id  The cache ID.
3138:      *
3139:      * @return array  An array with the following information:
3140:      *   - highestmodseq: (integer)
3141:      *   - messages: (integer)
3142:      *   - uidnext: (integer)
3143:      *   - uidvalidity: (integer) Always present
3144:      */
3145:     public function parseCacheId($id)
3146:     {
3147:         $data = array(
3148:             'H' => 'highestmodseq',
3149:             'M' => 'messages',
3150:             'U' => 'uidnext',
3151:             'V' => 'uidvalidity'
3152:         );
3153:         $info = array();
3154: 
3155:         foreach (explode('|', $id) as $part) {
3156:             if (isset($data[$part[0]])) {
3157:                 $info[$data[$part[0]]] = intval(substr($part, 1));
3158:             }
3159:         }
3160: 
3161:         return $info;
3162:     }
3163: 
3164:     /**
3165:      * Parses a client command array to create a server command string.
3166:      *
3167:      * @deprecated
3168:      * @see Horde_Imap_Client_Utils#parseCommandArray()
3169:      */
3170:     public function parseCommandArray($query, $callback = null, $out = '')
3171:     {
3172:         return $this->parseCommandArray($query, $callback, $out);
3173:     }
3174: 
3175:     /**
3176:      * Given an IMAP body section string, fetches the corresponding part.
3177:      *
3178:      * @param mixed $mailbox   A mailbox. Either a Horde_Imap_Client_Mailbox
3179:      *                         object (as of 1.2.0) or a string (UTF-8).
3180:      * @param integer $uid     The IMAP UID.
3181:      * @param string $section  The IMAP section string.
3182:      *
3183:      * @return resource  The section contents in a stream. Returns null if
3184:      *                   the part could not be found.
3185:      *
3186:      * @throws Horde_Imap_Client_Exception
3187:      */
3188:     public function fetchFromSectionString($mailbox, $uid, $section = null)
3189:     {
3190:         $ids_ob = $this->getIdsOb($uid);
3191:         $section = trim($section);
3192: 
3193:         // BODY[]
3194:         if (!strlen($section)) {
3195:             $query = new Horde_Imap_Client_Fetch_Query();
3196:             $query->fullText(array(
3197:                 'peek' => true
3198:             ));
3199: 
3200:             $fetch = $this->fetch($mailbox, $query, array('ids' => $ids_ob));
3201:             return $fetch[$uid]->getFullMsg(true);
3202:         }
3203: 
3204:         // BODY[<#.>HEADER.FIELDS<.NOT>()]
3205:         if (($pos = stripos($section, 'HEADER.FIELDS')) !== false) {
3206:             $hdr_pos = strpos($section, '(');
3207:             $cmd = substr($section, 0, $hdr_pos);
3208: 
3209:             $query = new Horde_Imap_Client_Fetch_Query();
3210:             $query->headers(
3211:                 'section',
3212:                 explode(' ', substr($section, $hdr_pos + 1, strrpos($section, ')') - $hdr_pos)),
3213:                 array(
3214:                     'id' => ($pos ? substr($section, 0, $pos - 1) : 0),
3215:                     'notsearch' => (stripos($cmd, '.NOT') !== false),
3216:                     'peek' => true
3217:                 )
3218:             );
3219: 
3220:             $fetch = $this->fetch($mailbox, $query, array('ids' => $ids_ob));
3221:             return $fetch[$uid]->getHeaders('section', Horde_Imap_Client_Data_Fetch::HEADER_STREAM);
3222:         }
3223: 
3224:         // BODY[#]
3225:         if (is_numeric(substr($section, -1))) {
3226:             $query = new Horde_Imap_Client_Fetch_Query();
3227:             $query->bodyPart($section, array(
3228:                 'peek' => true
3229:             ));
3230: 
3231:             $fetch = $this->fetch($mailbox, $query, array('ids' => $ids_ob));
3232:             return $fetch[$uid]->getBodyPart($section, true);
3233:         }
3234: 
3235:         // BODY[<#.>HEADER]
3236:         if (($pos = stripos($section, 'HEADER')) !== false) {
3237:             $id = $pos
3238:                 ? substr($section, 0, $pos - 1)
3239:                 : 0;
3240: 
3241:             $query = new Horde_Imap_Client_Fetch_Query();
3242:             $query->headerText(array(
3243:                 'id' => $id,
3244:                 'peek' => true
3245:             ));
3246: 
3247:             $fetch = $this->fetch($mailbox, $query, array('ids' => $ids_ob));
3248:             return $fetch[$uid]->getHeaderText($id, Horde_Imap_Client_Data_Fetch::HEADER_STREAM);
3249:         }
3250: 
3251:         // BODY[<#.>TEXT]
3252:         if (($pos = stripos($section, 'TEXT')) !== false) {
3253:             $id = $pos
3254:                 ? substr($section, 0, $pos - 1)
3255:                 : 0;
3256: 
3257:             $query = new Horde_Imap_Client_Fetch_Query();
3258:             $query->bodyText(array(
3259:                 'id' => $id,
3260:                 'peek' => true
3261:             ));
3262: 
3263:             $fetch = $this->fetch($mailbox, $query, array('ids' => $ids_ob));
3264:             return $fetch[$uid]->getBodyText($id, true);
3265:         }
3266: 
3267:         // BODY[<#.>MIMEHEADER]
3268:         if (($pos = stripos($section, 'MIME')) !== false) {
3269:             $id = $pos
3270:                 ? substr($section, 0, $pos - 1)
3271:                 : 0;
3272: 
3273:             $query = new Horde_Imap_Client_Fetch_Query();
3274:             $query->mimeHeader($id, array(
3275:                 'peek' => true
3276:             ));
3277: 
3278:             $fetch = $this->fetch($mailbox, $query, array('ids' => $ids_ob));
3279:             return $fetch[$uid]->getMimeHeader($id, Horde_Imap_Client_Data_Fetch::HEADER_STREAM);
3280:         }
3281: 
3282:         return null;
3283:     }
3284: 
3285:     /**
3286:      * Determines if the given charset is valid for search-related queries.
3287:      * This check pertains just to the basic IMAP SEARCH command.
3288:      *
3289:      * @param string $charset  The query charset.
3290:      *
3291:      * @return boolean  True if server supports this charset.
3292:      */
3293:     public function validSearchCharset($charset)
3294:     {
3295:         $charset = strtoupper($charset);
3296: 
3297:         if (!isset($this->_init['s_charset'][$charset])) {
3298:             $support = null;
3299: 
3300:             switch ($charset) {
3301:             case 'US-ASCII';
3302:                 /* US-ASCII is always supported (RFC 3501 [6.4.4]). */
3303:                 $support = true;
3304:                 break;
3305:             }
3306: 
3307:             /* Use a dummy search query and search for BADCHARSET
3308:              * response. */
3309:             if (is_null($support)) {
3310:                 $query = new Horde_Imap_Client_Search_Query();
3311:                 $query->charset($charset);
3312:                 $query->ids($this->getIdsOb(1, true));
3313:                 $query->text('a');
3314:                 try {
3315:                     $this->search('INBOX', $query, array(
3316:                         'sequence' => true
3317:                     ));
3318:                     $support = true;
3319:                 } catch (Horde_Imap_Client_Exception $e) {
3320:                     /* BADCHARSET is only a MAY return - but there is no
3321:                      * other way of determining charset support. */
3322:                     $support = ($e->getCode() != Horde_Imap_Client_Exception::BADCHARSET);
3323:                 }
3324:             }
3325: 
3326:             $s_charset = $this->_init['s_charset'];
3327:             $s_charset[$charset] = $support;
3328:             $this->_setInit('s_charset', $s_charset);
3329:         }
3330: 
3331:         return $this->_init['s_charset'][$charset];
3332:     }
3333: 
3334:     /**
3335:      * Output debug information.
3336:      *
3337:      * @param string $msg    Debug line.
3338:      * @param string $type   The message type. One of the following:
3339:      *   - Horde_Imap_Client::DEBUG_RAW: None (output raw message)
3340:      *   - Horde_Imap_Client::DEBUG_CLIENT: Client command
3341:      *   - Horde_Imap_Client::DEBUG_INFO: Informational message
3342:      *   - Horde_Imap_Client::DEBUG_SERVER: Server command
3343:      */
3344:     public function writeDebug($msg, $type = Horde_Imap_Client::DEBUG_RAW)
3345:     {
3346:         if (!$this->_debug) {
3347:             return;
3348:         }
3349: 
3350:         $pre = '';
3351: 
3352:         if ($type) {
3353:             $new_time = microtime(true);
3354:             if (isset($this->_temp['debug_time'])) {
3355:                 if (($diff = ($new_time - $this->_temp['debug_time'])) > Horde_Imap_Client::SLOW_COMMAND) {
3356:                     fwrite($this->_debug, '>> Slow IMAP Command: ' . round($diff, 3) . " seconds\n");
3357:                 }
3358:             } else {
3359:                 fwrite($this->_debug,
3360:                     str_repeat('-', 30) . "\n" .
3361:                     '>> Timestamp: ' . date('r') . "\n"
3362:                 );
3363:             }
3364: 
3365:             $this->_temp['debug_time'] = $new_time;
3366: 
3367:             switch ($type) {
3368:             case Horde_Imap_Client::DEBUG_CLIENT:
3369:                 $pre .= 'C: ';
3370:                 break;
3371: 
3372:             case Horde_Imap_Client::DEBUG_INFO:
3373:                 $pre .= '>> ';
3374:                 break;
3375: 
3376:             case Horde_Imap_Client::DEBUG_SERVER:
3377:                 $pre .= 'S: ';
3378:                 break;
3379:             }
3380:         }
3381: 
3382:         fwrite($this->_debug, $pre . $msg);
3383:     }
3384: 
3385:     /* Private utility functions. */
3386: 
3387:     /**
3388:      * Returns UIDs for an ALL search, or for a sequence number -> UID lookup.
3389:      *
3390:      * @param Horde_Imap_Client_Ids $ids  The IDs to lookup.
3391:      * @param boolean $reverse            Perform reverse lookup (UID ->
3392:      *                                    Sequence number) if needed.
3393:      *
3394:      * @return array  An array with 2 possible entries:
3395:      *   - lookup: (array) The mapping of sequence numbers [keys] to UIDs
3396:      *             [values]. Calculated if $reverse is true or $ids are
3397:      *             sequence numbers.
3398:      *   - uids: (Horde_Imap_Client_Ids) The list of UIDs.
3399:      */
3400:     protected function _getSeqUidLookup(Horde_Imap_Client_Ids $ids,
3401:                                         $reverse = false)
3402:     {
3403:         $ret = array('lookup' => array());
3404: 
3405:         if (count($ids) && !$ids->sequence && !$reverse) {
3406:             $ret['uids'] = $ids;
3407:             return $ret;
3408:         }
3409: 
3410:         if ($ids->all || $ids->search_res) {
3411:             $search = null;
3412:         } else {
3413:             $search = new Horde_Imap_Client_Search_Query();
3414:             $search->ids($ids);
3415:         }
3416: 
3417:         $res = $this->search($this->_selected, $search, array(
3418:             'sequence' => !$ids->all && !$ids->sequence,
3419:             'sort' => array(Horde_Imap_Client::SORT_SEQUENCE)
3420:         ));
3421: 
3422:         if ($res['count']) {
3423:             $ret['uids'] = ($ids->all || $ids->sequence)
3424:                 ? $res['match']
3425:                 : $ids;
3426: 
3427:             if ($ids->all) {
3428:                 $seq = range(1, $res['count']);
3429:             } else {
3430:                 $seq = $ids->sequence
3431:                     ? $ids->ids
3432:                     : $res['match']->ids;
3433:                 sort($seq, SORT_NUMERIC);
3434:             }
3435:             $ret['lookup'] = array_combine($seq, $ret['uids']->ids);
3436:         }
3437: 
3438:         return $ret;
3439:     }
3440: 
3441:     /**
3442:      * Store FETCH data in cache.
3443:      *
3444:      * @param array $data     The data array.
3445:      * @param array $options  Additional options:
3446:      *   - fields: (array) Only update these cache fields.
3447:      *             DEFAULT: Update all cache fields.
3448:      *   - mailbox: (Horde_Imap_Client_Mailbox) The mailbox to update.
3449:      *              DEFAULT: The selected mailbox.
3450:      *   - seq: (boolean) Is data stored with sequence numbers?
3451:      *          DEFAULT: Data stored with UIDs.
3452:      *   - uidvalid: (integer) The UID Validity number.
3453:      *               DEFAULT: UIDVALIDITY discovered via a status() call.
3454:      *
3455:      * @throws Horde_Imap_Client_Exception
3456:      */
3457:     protected function _updateCache($data, array $options = array())
3458:     {
3459:         $mailbox = empty($options['mailbox'])
3460:             ? $this->_selected
3461:             : $options['mailbox'];
3462: 
3463:         if (!$this->_initCache(empty($options['mailbox']))) {
3464:             return;
3465:         }
3466: 
3467:         if (in_array(strval($mailbox), $this->_params['cache']['fetch_ignore'])) {
3468:             $this->writeDebug(sprintf("IGNORING cached FETCH data (mailbox: %s)\n", $mailbox), Horde_Imap_Client::DEBUG_INFO);
3469:             return;
3470:         }
3471: 
3472:         $seq_res = empty($options['seq'])
3473:             ? null
3474:             : $this->_getSeqUidLookup($this->getIdsOb(array_keys($data), true));
3475: 
3476:         $cf = empty($options['fields'])
3477:             ? $this->_params['cache']['fields']
3478:             : array_intersect_key($this->_params['cache']['fields'], array_flip($options['fields']));
3479:         $tocache = array();
3480: 
3481:         $status_flags = 0;
3482:         if (isset($this->_init['enabled']['CONDSTORE'])) {
3483:             $status_flags |= Horde_Imap_Client::STATUS_HIGHESTMODSEQ;
3484:         }
3485:         if (empty($options['uidvalid'])) {
3486:             $status_flags |= Horde_Imap_Client::STATUS_UIDVALIDITY;
3487:         }
3488: 
3489:         $status_res = $this->status($mailbox, $status_flags);
3490: 
3491:         $highestmodseq = empty($status_res['highestmodseq'])
3492:             ? null
3493:             : array($status_res['highestmodseq']);
3494:         $uidvalid = isset($status_res['uidvalidity'])
3495:             ? $status_res['uidvalidity']
3496:             : $options['uidvalid'];
3497: 
3498:         reset($data);
3499:         while (list($k, $v) = each($data)) {
3500:             $tmp = array();
3501: 
3502:             foreach ($cf as $key => $val) {
3503:                 if ($v->exists($key)) {
3504:                     switch ($key) {
3505:                     case Horde_Imap_Client::FETCH_ENVELOPE:
3506:                         $tmp[$val] = $v->getEnvelope();
3507:                         break;
3508: 
3509:                     case Horde_Imap_Client::FETCH_FLAGS:
3510:                         /* A FLAGS FETCH can only occur if we are in the
3511:                          * mailbox. So either HIGHESTMODSEQ has already been
3512:                          * updated or the flag FETCHs will provide the new
3513:                          * HIGHESTMODSEQ value.  In either case, we are
3514:                          * guaranteed that all cache information is correctly
3515:                          * updated (in the former case, we reached here via
3516:                          * a 'changedsince' FETCH and in the latter case, we
3517:                          * are in EXAMINE/SELECT mode and will catch all flag
3518:                          * changes).
3519:                          * Ignore flag caching if MODSEQs not available. */
3520:                         if ($highestmodseq) {
3521:                             if ($modseq = $v->getModSeq()) {
3522:                                 $highestmodseq[] = $modseq;
3523:                             }
3524:                             $tmp[$val] = $v->getFlags();
3525:                         }
3526:                         break;
3527: 
3528:                     case Horde_Imap_Client::FETCH_HEADERS:
3529:                         foreach ($this->_temp['headers_caching'] as $label => $hash) {
3530:                             if ($hdr = $v->getHeaders($label)) {
3531:                                 $tmp[$val][$hash] = $hdr;
3532:                             }
3533:                         }
3534:                         break;
3535: 
3536:                     case Horde_Imap_Client::FETCH_IMAPDATE:
3537:                         $tmp[$val] = $v->getImapDate();
3538:                         break;
3539: 
3540:                     case Horde_Imap_Client::FETCH_SIZE:
3541:                         $tmp[$val] = $v->getSize();
3542:                         break;
3543: 
3544:                     case Horde_Imap_Client::FETCH_STRUCTURE:
3545:                         $tmp[$val] = clone $v->getStructure();
3546:                         break;
3547:                     }
3548:                 }
3549:             }
3550: 
3551:             if (!empty($tmp)) {
3552:                 $tocache[is_null($seq_res) ? $k : $seq_res['lookup'][$k]] = $tmp;
3553:             }
3554:         }
3555: 
3556:         $this->cache->set($mailbox, $tocache, $uidvalid);
3557: 
3558:         if (!empty($highestmodseq)) {
3559:             $modseq = max($highestmodseq);
3560:             $metadata = $this->cache->getMetaData($mailbox, $uidvalid, array(self::CACHE_MODSEQ));
3561:             if (!isset($metadata[self::CACHE_MODSEQ]) ||
3562:                 ($metadata[self::CACHE_MODSEQ] != $modseq)) {
3563:                     $this->_temp['lastmodseq'][strval($mailbox)] = isset($metadata[self::CACHE_MODSEQ])
3564:                         ? $metadata[self::CACHE_MODSEQ]
3565:                         : 0;
3566:                 if (count($tocache)) {
3567:                     $this->_temp['lastmodsequids'][strval($mailbox)] = $this->utils->toSequenceString(array_keys($tocache), array('nosort' => true));
3568:                 }
3569:                 $this->_updateMetaData($mailbox, array(self::CACHE_MODSEQ => $modseq), $uidvalid);
3570:             }
3571:         }
3572:     }
3573: 
3574:     /**
3575:      * Moves cache entries from one mailbox to another.
3576:      *
3577:      * @param string $from      The source mailbox (UTF7-IMAP).
3578:      * @param string $to        The destination mailbox (UTF7-IMAP).
3579:      * @param array $map        Mapping of source UIDs (keys) to destination
3580:      *                          UIDs (values).
3581:      * @param string $uidvalid  UIDVALIDITY of destination mailbox.
3582:      *
3583:      * @throws Horde_Imap_Client_Exception
3584:      */
3585:     protected function _moveCache($from, $to, $map, $uidvalid = null)
3586:     {
3587:         if (!$this->_initCache()) {
3588:             return;
3589:         }
3590: 
3591:         if (in_array($to, $this->_params['cache']['fetch_ignore'])) {
3592:             $this->writeDebug(sprintf("IGNORING moving cached FETCH data (%s => %s)\n", $from, $to), Horde_Imap_Client::DEBUG_INFO);
3593:             return;
3594:         }
3595: 
3596:         if (is_null($uidvalid)) {
3597:             $status_res = $this->status($to, Horde_Imap_Client::STATUS_UIDVALIDITY);
3598:             $uidvalid = $status_res['uidvalidity'];
3599:         }
3600: 
3601:         $old_data = $this->cache->get($from, array_keys($map), null);
3602:         $new_data = array();
3603: 
3604:         foreach ($map as $key => $val) {
3605:             if (!empty($old_data[$key])) {
3606:                 $new_data[$val] = $old_data[$key];
3607:             }
3608:         }
3609: 
3610:         if (!empty($new_data)) {
3611:             $this->cache->set($to, $new_data, $uidvalid);
3612:         }
3613:     }
3614: 
3615:     /**
3616:      * Delete messages in the cache.
3617:      *
3618:      * @param string $mailbox  An IMAP mailbox string.
3619:      * @param array $uids      The list of message UIDs to delete.
3620:      *
3621:      * @throws Horde_Imap_Client_Exception
3622:      */
3623:     protected function _deleteMsgs($mailbox, $uids)
3624:     {
3625:         $this->cache->deleteMsgs($mailbox, $uids);
3626:     }
3627: 
3628:     /**
3629:      * Retrieve data from the search cache.
3630:      *
3631:      * @param string $type     The cache type ('search' or 'thread').
3632:      * @param string $mailbox  The mailbox to update.
3633:      * @param array $options   The options array of the calling function.
3634:      *
3635:      * @return mixed  If retrieved, array is returned with data in key 'data'
3636:      *                and the search ID in key 'id'.
3637:      *                If caching is available, returns cacheid string.
3638:      *                Returns null if caching is not available.
3639:      */
3640:     protected function _getSearchCache($type, $mailbox, $options)
3641:     {
3642:         ksort($options);
3643:         $cache = hash('md5', $type . serialize($options));
3644: 
3645:         $search_id = $mailbox . $cache;
3646:         $status = $this->status($mailbox, Horde_Imap_Client::STATUS_UIDVALIDITY);
3647:         $metadata = $this->cache->getMetaData($mailbox, $status['uidvalidity'], array(self::CACHE_SEARCH));
3648: 
3649:         $cacheid = $this->getCacheId($mailbox);
3650:         if (isset($metadata[self::CACHE_SEARCH]['cacheid']) &&
3651:             ($metadata[self::CACHE_SEARCH]['cacheid'] != $cacheid)) {
3652:             $metadata[self::CACHE_SEARCH] = array();
3653:             if ($this->_debug &&
3654:                 !isset($this->_temp['searchcacheexpire'][strval($mailbox)])) {
3655:                 $this->writeDebug(sprintf("Expired search results from cache (mailbox: %s)\n", $mailbox), Horde_Imap_Client::DEBUG_INFO);
3656:                 $this->_temp['searchcacheexpire'][strval($mailbox)] = true;
3657:             }
3658:         } elseif (isset($metadata[self::CACHE_SEARCH][$cache])) {
3659:             $this->writeDebug(sprintf("Retrieved %s results from cache (mailbox: %s; id: %s)\n", $type, $mailbox, $cache), Horde_Imap_Client::DEBUG_INFO);
3660: 
3661:             return array(
3662:                 'data' => unserialize($metadata[self::CACHE_SEARCH][$cache]),
3663:                 'id' => $search_id
3664:             );
3665:         }
3666: 
3667:         $metadata[self::CACHE_SEARCH]['cacheid'] = $cacheid;
3668: 
3669:         $this->_temp['searchcache'][$search_id] = array(
3670:             'id' => $cache,
3671:             'mailbox' => $mailbox,
3672:             'metadata' => $metadata,
3673:             'type' => $type
3674:         );
3675: 
3676:         return $search_id;
3677:     }
3678: 
3679:     /**
3680:      * Set data in the search cache.
3681:      *
3682:      * @param mixed $data  The cache data to store.
3683:      * @param string $sid  The search ID returned from _getSearchCache().
3684:      */
3685:     protected function _setSearchCache($data, $sid)
3686:     {
3687:         $cache = &$this->_temp['searchcache'][$sid];
3688:         $cache['metadata'][self::CACHE_SEARCH][$cache['id']] = serialize($data);
3689: 
3690:         $this->_updateMetaData($cache['mailbox'], $cache['metadata']);
3691: 
3692:         if ($this->_debug) {
3693:             $this->writeDebug(sprintf("Saved %s results to cache (mailbox: %s; id: %s)\n", $cache['type'], $cache['mailbox'], $cache['id']), Horde_Imap_Client::DEBUG_INFO);
3694:             unset($this->_temp['searchcacheexpire'][strval($cache['mailbox'])]);
3695:         }
3696:     }
3697: 
3698:     /**
3699:      * Updates metadata for a mailbox.
3700:      *
3701:      * @param string $mailbox    Mailbox to update.
3702:      * @param string $data       The data to update.
3703:      * @param integer $uidvalid  The uidvalidity of the mailbox.  If not set,
3704:      *                           do a status call to grab it.
3705:      */
3706:     protected function _updateMetaData($mailbox, $data, $uidvalid = null)
3707:     {
3708:         if (is_null($uidvalid)) {
3709:             $status = $this->status($mailbox, Horde_Imap_Client::STATUS_UIDVALIDITY);
3710:             $uidvalid = $status['uidvalidity'];
3711:         }
3712:         $this->cache->setMetaData($mailbox, $uidvalid, $data);
3713:     }
3714: 
3715:     /**
3716:      * Prepares append message data for insertion into the IMAP command
3717:      * string.
3718:      *
3719:      * @param mixed $data       Either a resource or a string.
3720:      * @param resource $stream  The stream to append to. If not given, will
3721:      *                          append to new stream.
3722:      *
3723:      * @param resource  A stream containing the message data.
3724:      */
3725:     protected function _prepareAppendData($data = null, $stream = null)
3726:     {
3727:         if (is_null($stream)) {
3728:             $stream = fopen('php://temp', 'w+');
3729:             stream_filter_register('horde_eol', 'Horde_Stream_Filter_Eol');
3730:             stream_filter_append($stream, 'horde_eol', STREAM_FILTER_WRITE);
3731:         }
3732: 
3733:         if (!is_null($data)) {
3734:             if (is_resource($data)) {
3735:                 rewind($data);
3736:                 stream_copy_to_stream($data, $stream);
3737:             } else {
3738:                 fwrite($stream, $data);
3739:             }
3740:         }
3741: 
3742:         return $stream;
3743:     }
3744: 
3745:     /**
3746:      * Builds a stream from CATENATE input to append().
3747:      *
3748:      * @param array $data  See append() - array input for the 'data' key to
3749:      *                     the $data parameter.
3750:      *
3751:      * @return resource  The data combined into a single stream.
3752:      * @throws Horde_Imap_Client_Exception
3753:      */
3754:     protected function _buildCatenateData($data)
3755:     {
3756:         $stream = $this->_prepareAppendData();
3757: 
3758:         foreach (array_keys($data) as $key2) {
3759:             switch ($data[$key2]['t']) {
3760:             case 'text':
3761:                 $this->_prepareAppendData($data[$key2]['v'], $stream);
3762:                 break;
3763: 
3764:             case 'url':
3765:                 $part = $exception = null;
3766:                 $url = $this->utils->parseUrl($data[$key2]['v']);
3767: 
3768:                 if (isset($url['mailbox']) &&
3769:                     isset($url['uid'])) {
3770:                     try {
3771:                         $status_res = isset($url['uidvalidity'])
3772:                             ? $this->status($url['mailbox'], Horde_Imap_Client::STATUS_UIDVALIDITY)
3773:                             : null;
3774: 
3775:                         if (is_null($status_res) ||
3776:                             ($status_res['uidvalidity'] == $url['uidvalidity'])) {
3777:                             $part = $this->fetchFromSectionString($url['mailbox'], $url['uid'], isset($url['section']) ? $url['section'] : null);
3778:                         }
3779:                     } catch (Horde_Imap_Client_Exception $exception) {
3780:                     }
3781:                 }
3782: 
3783:                 if (is_null($part)) {
3784:                     $message = 'Bad IMAP URL given in CATENATE data: ' . json_encode($url);
3785:                     if ($exception) {
3786:                         $message .= ' ' . $exception->getMessage();
3787:                     }
3788:                     throw new InvalidArgumentException($message);
3789:                 } else {
3790:                     $this->_prepareAppendData($part, $stream);
3791:                 }
3792:                 break;
3793:             }
3794:         }
3795: 
3796:         return $stream;
3797:     }
3798: 
3799:     /**
3800:      * Parses human-readable response text for response codes.
3801:      *
3802:      * @param string $text  The response text.
3803:      *
3804:      * @return object  An object with the following properties:
3805:      *   - code: (string) The response code, if it exists.
3806:      *   - data: (string) The response code data, if it exists.
3807:      *   - text: (string) The human-readable response text.
3808:      */
3809:     protected function _parseResponseText($text)
3810:     {
3811:         $ret = new stdClass;
3812: 
3813:         $text = trim($text);
3814:         if ($text[0] == '[') {
3815:             $pos = strpos($text, ' ', 2);
3816:             $end_pos = strpos($text, ']', 2);
3817:             if ($pos > $end_pos) {
3818:                 $ret->code = strtoupper(substr($text, 1, $end_pos - 1));
3819:             } else {
3820:                 $ret->code = strtoupper(substr($text, 1, $pos - 1));
3821:                 $ret->data = substr($text, $pos + 1, $end_pos - $pos - 1);
3822:             }
3823:             $ret->text = trim(substr($text, $end_pos + 1));
3824:         } else {
3825:             $ret->text = $text;
3826:         }
3827: 
3828:         return $ret;
3829:     }
3830: 
3831: }
3832: 
API documentation generated by ApiGen