PageRenderTime 65ms CodeModel.GetById 19ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/horde/framework/Horde/Imap/Client/Base.php

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