PageRenderTime 145ms CodeModel.GetById 25ms RepoModel.GetById 0ms app.codeStats 1ms

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

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