PageRenderTime 74ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 1ms

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

https://bitbucket.org/moodle/moodle
PHP | 5166 lines | 3389 code | 640 blank | 1137 comment | 470 complexity | 640586b3c0b881ef95067a6db12bd997 MD5 | raw file
Possible License(s): Apache-2.0, LGPL-2.1, BSD-3-Clause, MIT, GPL-3.0
  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($lr as $val_utf8 => $tmp) {
  1372. $lr[$val_utf8]['status'] = $this->_prepareStatusResponse($status_opts, $val_utf8);
  1373. }
  1374. }
  1375. return $lr;
  1376. }
  1377. /**
  1378. * Parse a LIST/LSUB response (RFC 3501 [7.2.2 & 7.2.3]).
  1379. *
  1380. * @param Horde_Imap_Client_Interaction_Pipeline $pipeline Pipeline
  1381. * object.
  1382. * @param Horde_Imap_Client_Tokenize $data The server response (includes
  1383. * type as first token).
  1384. *
  1385. * @throws Horde_Imap_Client_Exception
  1386. */
  1387. protected function _parseList(
  1388. Horde_Imap_Client_Interaction_Pipeline $pipeline,
  1389. Horde_Imap_Client_Tokenize $data
  1390. )
  1391. {
  1392. $data->next();
  1393. $attr = null;
  1394. $attr_raw = $data->flushIterator();
  1395. $delimiter = $data->next();
  1396. $mbox = Horde_Imap_Client_Mailbox::get(
  1397. $data->next(),
  1398. !$this->_capability()->isEnabled('UTF8=ACCEPT')
  1399. );
  1400. $ml = $pipeline->data['mailboxlist'];
  1401. switch ($ml['mode']) {
  1402. case Horde_Imap_Client::MBOX_ALL_SUBSCRIBED:
  1403. case Horde_Imap_Client::MBOX_SUBSCRIBED_EXISTS:
  1404. case Horde_Imap_Client::MBOX_UNSUBSCRIBED:
  1405. $attr = array_flip(array_map('Horde_String::lower', $attr_raw));
  1406. /* Subscribed list is in UTF-8. */
  1407. if (is_null($ml['sub']) &&
  1408. !isset($attr['\\subscribed']) &&
  1409. (strcasecmp($mbox, 'INBOX') === 0)) {
  1410. $attr['\\subscribed'] = 1;
  1411. } elseif (isset($ml['sub'][strval($mbox)])) {
  1412. $attr['\\subscribed'] = 1;
  1413. }
  1414. break;
  1415. }
  1416. switch ($ml['mode']) {
  1417. case Horde_Imap_Client::MBOX_SUBSCRIBED_EXISTS:
  1418. if (isset($attr['\\nonexistent']) ||
  1419. !isset($attr['\\subscribed'])) {
  1420. return;
  1421. }
  1422. break;
  1423. case Horde_Imap_Client::MBOX_UNSUBSCRIBED:
  1424. if (isset($attr['\\subscribed'])) {
  1425. return;
  1426. }
  1427. break;
  1428. }
  1429. if (!empty($ml['opts']['flat'])) {
  1430. $pipeline->data['listresponse'][] = $mbox;
  1431. return;
  1432. }
  1433. $tmp = array(
  1434. 'delimiter' => $delimiter,
  1435. 'mailbox' => $mbox
  1436. );
  1437. if ($attr || !empty($ml['opts']['attributes'])) {
  1438. if (is_null($attr)) {
  1439. $attr = array_flip(array_map('Horde_String::lower', $attr_raw));
  1440. }
  1441. /* RFC 5258 [3.4]: inferred attributes. */
  1442. if ($ml['ext']) {
  1443. if (isset($attr['\\noinferiors'])) {
  1444. $attr['\\hasnochildren'] = 1;
  1445. }
  1446. if (isset($attr['\\nonexistent'])) {
  1447. $attr['\\noselect'] = 1;
  1448. }
  1449. }
  1450. $tmp['attributes'] = array_keys($attr);
  1451. }
  1452. if ($data->next() !== false) {
  1453. $tmp['extended'] = $data->flushIterator();
  1454. }
  1455. $pipeline->data['listresponse'][strval($mbox)] = $tmp;
  1456. }
  1457. /**
  1458. */
  1459. protected function _status($mboxes, $flags)
  1460. {
  1461. $on_error = null;
  1462. $out = $to_process = array();
  1463. $pipeline = $this->_pipeline();
  1464. $unseen_flags = array(
  1465. Horde_Imap_Client::STATUS_FIRSTUNSEEN,
  1466. Horde_Imap_Client::STATUS_UNSEEN
  1467. );
  1468. foreach ($mboxes as $mailbox) {
  1469. /* If FLAGS/PERMFLAGS/UIDNOTSTICKY/FIRSTUNSEEN are needed, we must
  1470. * do a SELECT/EXAMINE to get this information (data will be
  1471. * caught in the code below). */
  1472. if (($flags & Horde_Imap_Client::STATUS_FIRSTUNSEEN) ||
  1473. ($flags & Horde_Imap_Client::STATUS_FLAGS) ||
  1474. ($flags & Horde_Imap_Client::STATUS_PERMFLAGS) ||
  1475. ($flags & Horde_Imap_Client::STATUS_UIDNOTSTICKY)) {
  1476. $this->openMailbox($mailbox);
  1477. }
  1478. $mbox_ob = $this->_mailboxOb($mailbox);
  1479. $data = $query = array();
  1480. foreach ($this->_statusFields as $key => $val) {
  1481. if (!($val & $flags)) {
  1482. continue;
  1483. }
  1484. if ($val == Horde_Imap_Client::STATUS_HIGHESTMODSEQ) {
  1485. $c = $this->_capability();
  1486. /* Don't include modseq returns if server does not support
  1487. * it. */
  1488. if (!$c->query('CONDSTORE')) {
  1489. continue;
  1490. }
  1491. /* Even though CONDSTORE is available, it may not yet have
  1492. * been enabled. */
  1493. $c->enable('CONDSTORE');
  1494. $on_error = function() use ($c) {
  1495. $c->enable('CONDSTORE', false);
  1496. };
  1497. }
  1498. if ($mailbox->equals($this->_selected)) {
  1499. if (!is_null($tmp = $mbox_ob->getStatus($val))) {
  1500. $data[$key] = $tmp;
  1501. } elseif (($val == Horde_Imap_Client::STATUS_UIDNEXT) &&
  1502. ($flags & Horde_Imap_Client::STATUS_UIDNEXT_FORCE)) {
  1503. /* UIDNEXT is not mandatory. */
  1504. if ($mbox_ob->getStatus(Horde_Imap_Client::STATUS_MESSAGES) == 0) {
  1505. $data[$key] = 0;
  1506. } else {
  1507. $fquery = new Horde_Imap_Client_Fetch_Query();
  1508. $fquery->uid();
  1509. $fetch_res = $this->fetch($this->_selected, $fquery, array(
  1510. 'ids' => $this->getIdsOb(Horde_Imap_Client_Ids::LARGEST)
  1511. ));
  1512. $data[$key] = $fetch_res->first()->getUid() + 1;
  1513. }
  1514. } elseif (in_array($val, $unseen_flags)) {
  1515. /* RFC 3501 [6.3.1] - FIRSTUNSEEN information is not
  1516. * mandatory. If missing in EXAMINE/SELECT results, we
  1517. * need to do a search. An UNSEEN count also requires
  1518. * a search. */
  1519. $squery = new Horde_Imap_Client_Search_Query();
  1520. $squery->flag(Horde_Imap_Client::FLAG_SEEN, false);
  1521. $search = $this->search($mailbox, $squery, array(
  1522. 'results' => array(
  1523. Horde_Imap_Client::SEARCH_RESULTS_MIN,
  1524. Horde_Imap_Client::SEARCH_RESULTS_COUNT
  1525. ),
  1526. 'sequence' => true
  1527. ));
  1528. $mbox_ob->setStatus(Horde_Imap_Client::STATUS_FIRSTUNSEEN, $search['min']);
  1529. $mbox_ob->setStatus(Horde_Imap_Client::STATUS_UNSEEN, $search['count']);
  1530. $data[$key] = $mbox_ob->getStatus($val);
  1531. }
  1532. } else {
  1533. $query[] = $key;
  1534. }
  1535. }
  1536. $out[strval($mailbox)] = $data;
  1537. if (count($query)) {
  1538. $cmd = $this->_command('STATUS')->add(array(
  1539. $this->_getMboxFormatOb($mailbox),
  1540. new Horde_Imap_Client_Data_Format_List(
  1541. array_map('Horde_String::upper', $query)
  1542. )
  1543. ));
  1544. $cmd->on_error = $on_error;
  1545. $pipeline->add($cmd);
  1546. $to_process[] = array($query, $mailbox);
  1547. }
  1548. }
  1549. if (count($pipeline)) {
  1550. $this->_sendCmd($pipeline);
  1551. foreach ($to_process as $val) {
  1552. $out[strval($val[1])] += $this->_prepareStatusResponse($val[0], $val[1]);
  1553. }
  1554. }
  1555. return $out;
  1556. }
  1557. /**
  1558. * Parse a STATUS response (RFC 3501 [7.2.4]).
  1559. *
  1560. * @param Horde_Imap_Client_Tokenize $data Token data
  1561. */
  1562. protected function _parseStatus(Horde_Imap_Client_Tokenize $data)
  1563. {
  1564. // Mailbox name is in UTF7-IMAP (unless UTF8 has been enabled).
  1565. $mbox_ob = $this->_mailboxOb(
  1566. Horde_Imap_Client_Mailbox::get(
  1567. $data->next(),
  1568. !$this->_capability()->isEnabled('UTF8=ACCEPT')
  1569. )
  1570. );
  1571. $data->next();
  1572. while (($k = $data->next()) !== false) {
  1573. $mbox_ob->setStatus(
  1574. $this->_statusFields[Horde_String::lower($k)],
  1575. $data->next()
  1576. );
  1577. }
  1578. }
  1579. /**
  1580. * Prepares a status response for a mailbox.
  1581. *
  1582. * @param array $request The status keys to return.
  1583. * @param string $mailbox The mailbox to query.
  1584. */
  1585. protected function _prepareStatusResponse($request, $mailbox)
  1586. {
  1587. $mbox_ob = $this->_mailboxOb($mailbox);
  1588. $out = array();
  1589. foreach ($request as $val) {
  1590. $out[$val] = $mbox_ob->getStatus($this->_statusFields[$val]);
  1591. }
  1592. return $out;
  1593. }
  1594. /**
  1595. */
  1596. protected function _append(Horde_Imap_Client_Mailbox $mailbox, $data,
  1597. $options)
  1598. {
  1599. $c = $this->_capability();
  1600. // Check for MULTIAPPEND extension (RFC 3502)
  1601. if ((count($data) > 1) && !$c->query('MULTIAPPEND')) {
  1602. $result = $this->getIdsOb();
  1603. foreach (array_keys($data) as $key) {
  1604. $res = $this->_append($mailbox, array($data[$key]), $options);
  1605. if (($res === true) || ($result === true)) {
  1606. $result = true;
  1607. } else {
  1608. $result->add($res);
  1609. }
  1610. }
  1611. return $result;
  1612. }
  1613. // Check for extensions.
  1614. $binary = $c->query('BINARY');
  1615. $catenate = $c->query('CATENATE');
  1616. $utf8 = $c->isEnabled('UTF8=ACCEPT');
  1617. $asize = 0;
  1618. $cmd = $this->_command('APPEND')->add(
  1619. $this->_getMboxFormatOb($mailbox)
  1620. );
  1621. $cmd->literal8 = true;
  1622. foreach (array_keys($data) as $key) {
  1623. if (!empty($data[$key]['flags'])) {
  1624. $tmp = new Horde_Imap_Client_Data_Format_List();
  1625. foreach ($data[$key]['flags'] as $val) {
  1626. /* Ignore recent flag. RFC 3501 [9]: flag definition */
  1627. if (strcasecmp($val, Horde_Imap_Client::FLAG_RECENT) !== 0) {
  1628. $tmp->add($val);
  1629. }
  1630. }
  1631. $cmd->add($tmp);
  1632. }
  1633. if (!empty($data[$key]['internaldate'])) {
  1634. $cmd->add(new Horde_Imap_Client_Data_Format_DateTime($data[$key]['internaldate']));
  1635. }
  1636. $adata = null;
  1637. if (is_array($data[$key]['data'])) {
  1638. if ($catenate) {
  1639. $cmd->add('CATENATE');
  1640. $tmp = new Horde_Imap_Client_Data_Format_List();
  1641. } else {
  1642. $data_stream = new Horde_Stream_Temp();
  1643. }
  1644. foreach ($data[$key]['data'] as $v) {
  1645. switch ($v['t']) {
  1646. case 'text':
  1647. if ($catenate) {
  1648. $tdata = $this->_appendData($v['v'], $asize);
  1649. if ($utf8) {
  1650. /* RFC 6855 [4]: CATENATE UTF8 extension. */
  1651. $tdata->forceBinary();
  1652. $tmp->add(array(
  1653. 'UTF8',
  1654. new Horde_Imap_Client_Data_Format_List($tdata)
  1655. ));
  1656. } else {
  1657. $tmp->add(array(
  1658. 'TEXT',
  1659. $tdata
  1660. ));
  1661. }
  1662. } else {
  1663. if (is_resource($v['v'])) {
  1664. rewind($v['v']);
  1665. }
  1666. $data_stream->add($v['v']);
  1667. }
  1668. break;
  1669. case 'url':
  1670. if ($catenate) {
  1671. $tmp->add(array(
  1672. 'URL',
  1673. new Horde_Imap_Client_Data_Format_Astring($v['v'])
  1674. ));
  1675. } else {
  1676. $data_stream->add($this->_convertCatenateUrl($v['v']));
  1677. }
  1678. break;
  1679. }
  1680. }
  1681. if ($catenate) {
  1682. $cmd->add($tmp);
  1683. } else {
  1684. $adata = $this->_appendData($data_stream->stream, $asize);
  1685. }
  1686. } else {
  1687. $adata = $this->_appendData($data[$key]['data'], $asize);
  1688. }
  1689. if (!is_null($adata)) {
  1690. if ($utf8) {
  1691. /* RFC 6855 [4]: APPEND UTF8 extension. */
  1692. $adata->forceBinary();
  1693. $cmd->add(array(
  1694. 'UTF8',
  1695. new Horde_Imap_Client_Data_Format_List($adata)
  1696. ));
  1697. } else {
  1698. $cmd->add($adata);
  1699. }
  1700. }
  1701. }
  1702. /* Although it is normally more efficient to use LITERAL+, disable if
  1703. * payload is over 50 KB because it allows the server to throw error
  1704. * before we potentially push a lot of data to server that would
  1705. * otherwise be ignored (see RFC 4549 [4.2.2.3]).
  1706. * Additionally, since so many IMAP servers have issues with APPEND
  1707. * + BINARY, don't use LITERAL+ since servers may send BAD
  1708. * (incorrectly) after initial command. */
  1709. $cmd->literalplus = (($asize < (1024 * 50)) && !$binary);
  1710. // If the mailbox is currently selected read-only, we need to close
  1711. // because some IMAP implementations won't allow an append. And some
  1712. // implementations don't support append on ANY open mailbox. Be safe
  1713. // and always make sure we are in a non-selected state.
  1714. $this->close();
  1715. try {
  1716. $resp = $this->_sendCmd($cmd);
  1717. } catch (Horde_Imap_Client_Exception $e) {
  1718. switch ($e->getCode()) {
  1719. case $e::CATENATE_BADURL:
  1720. case $e::CATENATE_TOOBIG:
  1721. /* Cyrus 2.4 (at least as of .14) has a broken CATENATE (see
  1722. * Bug #11111). Regardless, if CATENATE is broken, we can try
  1723. * to fallback to APPEND. */
  1724. $c->remove('CATENATE');
  1725. return $this->_append($mailbox, $data, $options);
  1726. case $e::DISCONNECT:
  1727. /* Workaround broken literal8 on Cyrus. */
  1728. if ($binary) {
  1729. // Need to re-login first before removing capability.
  1730. $this->login();
  1731. $c->remove('BINARY');
  1732. return $this->_append($mailbox, $data, $options);
  1733. }
  1734. break;
  1735. }
  1736. if (!empty($options['create']) &&
  1737. !empty($e->resp_data['trycreate'])) {
  1738. $this->createMailbox($mailbox);
  1739. unset($options['create']);
  1740. return $this->_append($mailbox, $data, $options);
  1741. }
  1742. /* RFC 3516/4466 says we should be able to append binary data
  1743. * using literal8 "~{#} format", but it doesn't seem to work on
  1744. * all servers tried (UW-IMAP/Cyrus). Do a last-ditch check for
  1745. * broken BINARY and attempt to fix here. */
  1746. if ($c->query('BINARY') &&
  1747. ($e instanceof Horde_Imap_Client_Exception_ServerResponse)) {
  1748. switch ($e->status) {
  1749. case Horde_Imap_Client_Interaction_Server::BAD:
  1750. case Horde_Imap_Client_Interaction_Server::NO:
  1751. $c->remove('BINARY');
  1752. return $this->_append($mailbox, $data, $options);
  1753. }
  1754. }
  1755. throw $e;
  1756. }
  1757. /* If we reach this point and have data in 'appenduid', UIDPLUS (RFC
  1758. * 4315) has done the dirty work for us. */
  1759. return isset($resp->data['appenduid'])
  1760. ? $resp->data['appenduid']
  1761. : true;
  1762. }
  1763. /**
  1764. * Prepares append message data for insertion into the IMAP command
  1765. * string.
  1766. *
  1767. * @param mixed $data Either a resource or a string.
  1768. * @param integer &$asize Total append size.
  1769. *
  1770. * @return Horde_Imap_Client_Data_Format_String_Nonascii The data object.
  1771. */
  1772. protected function _appendData($data, &$asize)
  1773. {
  1774. if (is_resource($data)) {
  1775. rewind($data);
  1776. }
  1777. /* Since this is body text, with possible embedded charset
  1778. * information, non-ASCII characters are supported. */
  1779. $ob = new Horde_Imap_Client_Data_Format_String_Nonascii($data, array(
  1780. 'eol' => true,
  1781. 'skipscan' => true
  1782. ));
  1783. // APPEND data MUST be sent in a literal (RFC 3501 [6.3.11]).
  1784. $ob->forceLiteral();
  1785. $asize += $ob->length();
  1786. return $ob;
  1787. }
  1788. /**
  1789. * Converts a CATENATE URL to stream data.
  1790. *
  1791. * @param string $url The CATENATE URL.
  1792. *
  1793. * @return resource A stream containing the data.
  1794. */
  1795. protected function _convertCatenateUrl($url)
  1796. {
  1797. $e = $part = null;
  1798. $url = new Horde_Imap_Client_Url_Imap($url);
  1799. if (!is_null($url->mailbox) && !is_null($url->uid)) {
  1800. try {
  1801. $status_res = is_null($url->uidvalidity)
  1802. ? null
  1803. : $this->status($url->mailbox, Horde_Imap_Client::STATUS_UIDVALIDITY);
  1804. if (is_null($status_res) ||
  1805. ($status_res['uidvalidity'] == $url->uidvalidity)) {
  1806. if (!isset($this->_temp['catenate_ob'])) {
  1807. $this->_temp['catenate_ob'] = new Horde_Imap_Client_Socket_Catenate($this);
  1808. }
  1809. $part = $this->_temp['catenate_ob']->fetchFromUrl($url);
  1810. }
  1811. } catch (Horde_Imap_Client_Exception $e) {}
  1812. }
  1813. if (is_null($part)) {
  1814. $message = 'Bad IMAP URL given in CATENATE data: ' . strval($url);
  1815. if ($e) {
  1816. $message .= ' ' . $e->getMessage();
  1817. }
  1818. throw new InvalidArgumentException($message);
  1819. }
  1820. return $part;
  1821. }
  1822. /**
  1823. */
  1824. protected function _check()
  1825. {
  1826. // CHECK returns no untagged information (RFC 3501 [6.4.1])
  1827. $this->_sendCmd($this->_command('CHECK'));
  1828. }
  1829. /**
  1830. */
  1831. protected function _close($options)
  1832. {
  1833. if (empty($options['expunge'])) {
  1834. if ($this->_capability('UNSELECT')) {
  1835. // RFC 3691 defines 'UNSELECT' for precisely this purpose
  1836. $this->_sendCmd($this->_command('UNSELECT'));
  1837. } else {
  1838. /* RFC 3501 [6.4.2]: to close a mailbox without expunge,
  1839. * select a non-existent mailbox. */
  1840. try {
  1841. $this->_sendCmd($this->_command('EXAMINE')->add(
  1842. $this->_getMboxFormatOb("\24nonexist\24")
  1843. ));
  1844. /* Not pipelining, since the odds that this CLOSE is even
  1845. * needed is tiny; and it returns BAD, which should be
  1846. * avoided, if possible. */
  1847. $this->_sendCmd($this->_command('CLOSE'));
  1848. } catch (Horde_Imap_Client_Exception_ServerResponse $e) {
  1849. // Ignore error; it is expected.
  1850. }
  1851. }
  1852. } else {
  1853. // If caching, we need to know the UIDs being deleted, so call
  1854. // expunge() before calling close().
  1855. if ($this->_initCache(true)) {
  1856. $this->expunge($this->_selected);
  1857. }
  1858. // CLOSE returns no untagged information (RFC 3501 [6.4.2])
  1859. $this->_sendCmd($this->_command('CLOSE'));
  1860. }
  1861. }
  1862. /**
  1863. */
  1864. protected function _expunge($options)
  1865. {
  1866. $expunged_ob = $modseq = null;
  1867. $ids = $options['ids'];
  1868. $list_msgs = !empty($options['list']);
  1869. $mailbox = $this->_selected;
  1870. $uidplus = $this->_capability('UIDPLUS');
  1871. $unflag = array();
  1872. $use_cache = $this->_initCache(true);
  1873. if ($ids->all) {
  1874. if (!$uidplus || $list_msgs || $use_cache) {
  1875. $ids = $this->resolveIds($mailbox, $ids, 2);
  1876. }
  1877. } elseif ($uidplus) {
  1878. /* If QRESYNC is not available, and we are returning the list of
  1879. * expunged messages (or we are caching), we have to make sure we
  1880. * have a mapping of Sequence -> UIDs. If we have QRESYNC, the
  1881. * server SHOULD return a VANISHED response with UIDs. However,
  1882. * even if the server returns EXPUNGEs instead, we can use
  1883. * vanished() to grab the list. */
  1884. unset($this->_temp['search_save']);
  1885. if ($this->_capability()->isEnabled('QRESYNC')) {
  1886. $ids = $this->resolveIds($mailbox, $ids, 1);
  1887. if ($list_msgs) {
  1888. $modseq = $this->_mailboxOb()->getStatus(Horde_Imap_Client::STATUS_HIGHESTMODSEQ);
  1889. }
  1890. } else {
  1891. $ids = $this->resolveIds($mailbox, $ids, ($list_msgs || $use_cache) ? 2 : 1);
  1892. }
  1893. if (!empty($this->_temp['search_save'])) {
  1894. $ids = $this->getIdsOb(Horde_Imap_Client_Ids::SEARCH_RES);
  1895. }
  1896. } else {
  1897. /* Without UIDPLUS, need to temporarily unflag all messages marked
  1898. * as deleted but not a part of requested IDs to delete. Use NOT
  1899. * searches to accomplish this goal. */
  1900. $squery = new Horde_Imap_Client_Search_Query();
  1901. $squery->flag(Horde_Imap_Client::FLAG_DELETED, true);
  1902. $squery->ids($ids, true);
  1903. $s_res = $this->search($mailbox, $squery, array(
  1904. 'results' => array(
  1905. Horde_Imap_Client::SEARCH_RESULTS_MATCH,
  1906. Horde_Imap_Client::SEARCH_RESULTS_SAVE
  1907. )
  1908. ));
  1909. $this->store($mailbox, array(
  1910. 'ids' => empty($s_res['save']) ? $s_res['match'] : $this->getIdsOb(Horde_Imap_Client_Ids::SEARCH_RES),
  1911. 'remove' => array(Horde_Imap_Client::FLAG_DELETED)
  1912. ));
  1913. $unflag = $s_res['match'];
  1914. }
  1915. if ($list_msgs) {
  1916. $expunged_ob = $this->getIdsOb();
  1917. $this->_temp['expunged'] = $expunged_ob;
  1918. }
  1919. /* Always use UID EXPUNGE if available. */
  1920. if ($uidplus) {
  1921. /* We can only pipeline STORE w/ EXPUNGE if using UIDs and UIDPLUS
  1922. * is available. */
  1923. if (empty($options['delete'])) {
  1924. $pipeline = $this->_pipeline();
  1925. } else {
  1926. $pipeline = $this->_storeCmd(array(
  1927. 'add' => array(
  1928. Horde_Imap_Client::FLAG_DELETED
  1929. ),
  1930. 'ids' => $ids
  1931. ));
  1932. }
  1933. foreach ($ids->split(2000) as $val) {
  1934. $pipeline->add(
  1935. $this->_command('UID EXPUNGE')->add($val)
  1936. );
  1937. }
  1938. $resp = $this->_sendCmd($pipeline);
  1939. } else {
  1940. if (!empty($options['delete'])) {
  1941. $this->store($mailbox, array(
  1942. 'add' => array(Horde_Imap_Client::FLAG_DELETED),
  1943. 'ids' => $ids
  1944. ));
  1945. }
  1946. if ($use_cache || $list_msgs) {
  1947. $this->_sendCmd($this->_command('EXPUNGE'));
  1948. } else {
  1949. /* This is faster than an EXPUNGE because the server will not
  1950. * return untagged EXPUNGE responses. We can only do this if
  1951. * we are not updating cache information. */
  1952. $this->close(array('expunge' => true));
  1953. }
  1954. }
  1955. unset($this->_temp['expunged']);
  1956. if (!empty($unflag)) {
  1957. $this->store($mailbox, array(
  1958. 'add' => array(Horde_Imap_Client::FLAG_DELETED),
  1959. 'ids' => $unflag
  1960. ));
  1961. }
  1962. if (!is_null($modseq) && !empty($resp->data['expunge_seen'])) {
  1963. /* There's a chance we actually did a full map of sequence -> UID,
  1964. * but this code should never be reached in the first place so
  1965. * be ultra-safe and just do a full VANISHED search. */
  1966. $expunged_ob = $this->vanished($mailbox, $modseq, array(
  1967. 'ids' => $ids
  1968. ));
  1969. $this->_deleteMsgs($mailbox, $expunged_ob, array(
  1970. 'pipeline' => $resp
  1971. ));
  1972. }
  1973. return $expunged_ob;
  1974. }
  1975. /**
  1976. * Parse a VANISHED response (RFC 7162 [3.2.10]).
  1977. *
  1978. * @param Horde_Imap_Client_Interaction_Pipeline $pipeline Pipeline
  1979. * object.
  1980. * @param Horde_Imap_Client_Tokenize $data The response data.
  1981. */
  1982. protected function _parseVanished(
  1983. Horde_Imap_Client_Interaction_Pipeline $pipeline,
  1984. Horde_Imap_Client_Tokenize $data
  1985. )
  1986. {
  1987. /* There are two forms of VANISHED. VANISHED (EARLIER) will be sent
  1988. * in a FETCH (VANISHED) or SELECT/EXAMINE (QRESYNC) call.
  1989. * If this is the case, we can go ahead and update the cache
  1990. * immediately (we know we are caching or else QRESYNC would not be
  1991. * enabled). HIGHESTMODSEQ information will be updated via the tagged
  1992. * response. */
  1993. if (($curr = $data->next()) === true) {
  1994. if (Horde_String::upper($data->next()) === 'EARLIER') {
  1995. /* Caching is guaranteed to be active if we are using
  1996. * QRESYNC. */
  1997. $data->next();
  1998. $vanished = $this->getIdsOb($data->next());
  1999. if (isset($pipeline->data['vanished'])) {
  2000. $pipeline->data['vanished']->add($vanished);
  2001. } else {
  2002. $this->_deleteMsgs($this->_selected, $vanished, array(
  2003. 'pipeline' => $pipeline
  2004. ));
  2005. }
  2006. }
  2007. } else {
  2008. /* The second form is just VANISHED. This is analogous to EXPUNGE
  2009. * and requires the message count to decrement. */
  2010. $this->_deleteMsgs($this->_selected, $this->getIdsOb($curr), array(
  2011. 'decrement' => true,
  2012. 'pipeline' => $pipeline
  2013. ));
  2014. }
  2015. }
  2016. /**
  2017. * Search a mailbox. This driver supports all IMAP4rev1 search criteria
  2018. * as defined in RFC 3501.
  2019. */
  2020. protected function _search($query, $options)
  2021. {
  2022. $sort_criteria = array(
  2023. Horde_Imap_Client::SORT_ARRIVAL => 'ARRIVAL',
  2024. Horde_Imap_Client::SORT_CC => 'CC',
  2025. Horde_Imap_Client::SORT_DATE => 'DATE',
  2026. Horde_Imap_Client::SORT_DISPLAYFROM => 'DISPLAYFROM',
  2027. Horde_Imap_Client::SORT_DISPLAYTO => 'DISPLAYTO',
  2028. Horde_Imap_Client::SORT_FROM => 'FROM',
  2029. Horde_Imap_Client::SORT_REVERSE => 'REVERSE',
  2030. Horde_Imap_Client::SORT_RELEVANCY => 'RELEVANCY',
  2031. // This is a bogus entry to allow the sort options check to
  2032. // correctly work below.
  2033. Horde_Imap_Client::SORT_SEQUENCE => 'SEQUENCE',
  2034. Horde_Imap_Client::SORT_SIZE => 'SIZE',
  2035. Horde_Imap_Client::SORT_SUBJECT => 'SUBJECT',
  2036. Horde_Imap_Client::SORT_TO => 'TO'
  2037. );
  2038. $results_criteria = array(
  2039. Horde_Imap_Client::SEARCH_RESULTS_COUNT => 'COUNT',
  2040. Horde_Imap_Client::SEARCH_RESULTS_MATCH => 'ALL',
  2041. Horde_Imap_Client::SEARCH_RESULTS_MAX => 'MAX',
  2042. Horde_Imap_Client::SEARCH_RESULTS_MIN => 'MIN',
  2043. Horde_Imap_Client::SEARCH_RESULTS_RELEVANCY => 'RELEVANCY',
  2044. Horde_Imap_Client::SEARCH_RESULTS_SAVE => 'SAVE'
  2045. );
  2046. // Check if the server supports sorting (RFC 5256).
  2047. $esearch = $return_sort = $server_seq_sort = $server_sort = false;
  2048. if (!empty($options['sort'])) {
  2049. /* Make sure sort options are correct. If not, default to no
  2050. * sort. */
  2051. if (count(array_intersect($options['sort'], array_keys($sort_criteria))) === 0) {
  2052. unset($options['sort']);
  2053. } else {
  2054. $return_sort = true;
  2055. if ($this->_capability('SORT')) {
  2056. /* Make sure server supports DISPLAYFROM & DISPLAYTO. */
  2057. $server_sort =
  2058. !array_intersect($options['sort'], array(Horde_Imap_Client::SORT_DISPLAYFROM, Horde_Imap_Client::SORT_DISPLAYTO)) ||
  2059. $this->_capability('SORT', 'DISPLAY');
  2060. }
  2061. /* If doing a sequence sort, need to do this on the client
  2062. * side. */
  2063. if ($server_sort &&
  2064. in_array(Horde_Imap_Client::SORT_SEQUENCE, $options['sort'])) {
  2065. $server_sort = false;
  2066. /* Optimization: If doing only a sequence sort, just do a
  2067. * simple search and sort UIDs/sequences on client side. */
  2068. switch (count($options['sort'])) {
  2069. case 1:
  2070. $server_seq_sort = true;
  2071. break;
  2072. case 2:
  2073. $server_seq_sort = (reset($options['sort']) == Horde_Imap_Client::SORT_REVERSE);
  2074. break;
  2075. }
  2076. }
  2077. }
  2078. }
  2079. $charset = is_null($options['_query']['charset'])
  2080. ? 'US-ASCII'
  2081. : $options['_query']['charset'];
  2082. $partial = false;
  2083. if ($server_sort) {
  2084. $cmd = $this->_command(
  2085. empty($options['sequence']) ? 'UID SORT' : 'SORT'
  2086. );
  2087. $results = array();
  2088. // Use ESEARCH (RFC 4466) response if server supports.
  2089. $esearch = false;
  2090. // Check for ESORT capability (RFC 5267)
  2091. if ($this->_capability('ESORT')) {
  2092. foreach ($options['results'] as $val) {
  2093. if (isset($results_criteria[$val]) &&
  2094. ($val != Horde_Imap_Client::SEARCH_RESULTS_SAVE)) {
  2095. $results[] = $results_criteria[$val];
  2096. }
  2097. }
  2098. $esearch = true;
  2099. }
  2100. // Add PARTIAL limiting (RFC 5267 [4.4])
  2101. if ((!$esearch || !empty($options['partial'])) &&
  2102. $this->_capability('CONTEXT', 'SORT')) {
  2103. /* RFC 5267 indicates RFC 4466 ESEARCH-like support,
  2104. * notwithstanding "real" RFC 4731 support. */
  2105. $esearch = true;
  2106. if (!empty($options['partial'])) {
  2107. /* Can't have both ALL and PARTIAL returns. */
  2108. $results = array_diff($results, array('ALL'));
  2109. $results[] = 'PARTIAL';
  2110. $results[] = $options['partial'];
  2111. $partial = true;
  2112. }
  2113. }
  2114. if ($esearch && empty($this->_init['noesearch'])) {
  2115. $cmd->add(array(
  2116. 'RETURN',
  2117. new Horde_Imap_Client_Data_Format_List($results)
  2118. ));
  2119. }
  2120. $tmp = new Horde_Imap_Client_Data_Format_List();
  2121. foreach ($options['sort'] as $val) {
  2122. if (isset($sort_criteria[$val])) {
  2123. $tmp->add($sort_criteria[$val]);
  2124. }
  2125. }
  2126. $cmd->add($tmp);
  2127. /* Charset is mandatory for SORT (RFC 5256 [3]).
  2128. * If UTF-8 support is activated, a client MUST ONLY
  2129. * send the 'UTF-8' specification (RFC 6855 [3]; Errata 4029). */
  2130. if (!$this->_capability()->isEnabled('UTF8=ACCEPT')) {
  2131. $cmd->add($charset);
  2132. } else {
  2133. $cmd->add('UTF-8');
  2134. }
  2135. } else {
  2136. $cmd = $this->_command(
  2137. empty($options['sequence']) ? 'UID SEARCH' : 'SEARCH'
  2138. );
  2139. $esearch = false;
  2140. $results = array();
  2141. // Check if the server supports ESEARCH (RFC 4731).
  2142. if ($this->_capability('ESEARCH')) {
  2143. foreach ($options['results'] as $val) {
  2144. if (isset($results_criteria[$val])) {
  2145. $results[] = $results_criteria[$val];
  2146. }
  2147. }
  2148. $esearch = true;
  2149. }
  2150. // Add PARTIAL limiting (RFC 5267 [4.4]).
  2151. if ((!$esearch || !empty($options['partial'])) &&
  2152. $this->_capability('CONTEXT', 'SEARCH')) {
  2153. /* RFC 5267 indicates RFC 4466 ESEARCH-like support,
  2154. * notwithstanding "real" RFC 4731 support. */
  2155. $esearch = true;
  2156. if (!empty($options['partial'])) {
  2157. // Can't have both ALL and PARTIAL returns.
  2158. $results = array_diff($results, array('ALL'));
  2159. $results[] = 'PARTIAL';
  2160. $results[] = $options['partial'];
  2161. $partial = true;
  2162. }
  2163. }
  2164. if ($esearch && empty($this->_init['noesearch'])) {
  2165. // Always use ESEARCH if available because it returns results
  2166. // in a more compact sequence-set list
  2167. $cmd->add(array(
  2168. 'RETURN',
  2169. new Horde_Imap_Client_Data_Format_List($results)
  2170. ));
  2171. }
  2172. /* Charset is optional for SEARCH (RFC 3501 [6.4.4]).
  2173. * If UTF-8 support is activated, a client MUST NOT
  2174. * send the charset specification (RFC 6855 [3]; Errata 4029). */
  2175. if (($charset != 'US-ASCII') &&
  2176. !$this->_capability()->isEnabled('UTF8=ACCEPT')) {
  2177. $cmd->add(array(
  2178. 'CHARSET',
  2179. $options['_query']['charset']
  2180. ));
  2181. }
  2182. }
  2183. $cmd->add($options['_query']['query'], true);
  2184. $pipeline = $this->_pipeline($cmd);
  2185. $pipeline->data['esearchresp'] = array();
  2186. $er = &$pipeline->data['esearchresp'];
  2187. $pipeline->data['searchresp'] = $this->getIdsOb(array(), !empty($options['sequence']));
  2188. $sr = &$pipeline->data['searchresp'];
  2189. try {
  2190. $resp = $this->_sendCmd($pipeline);
  2191. } catch (Horde_Imap_Client_Exception $e) {
  2192. if (($e instanceof Horde_Imap_Client_Exception_ServerResponse) &&
  2193. ($e->status === Horde_Imap_Client_Interaction_Server::NO) &&
  2194. ($charset != 'US-ASCII')) {
  2195. /* RFC 3501 [6.4.4]: BADCHARSET response code is only a
  2196. * SHOULD return. If it doesn't exist, need to check for
  2197. * command status of 'NO'. List of supported charsets in
  2198. * the BADCHARSET response has already been parsed and stored
  2199. * at this point. */
  2200. $this->search_charset->setValid($charset, false);
  2201. $e->setCode(Horde_Imap_Client_Exception::BADCHARSET);
  2202. }
  2203. if (empty($this->_temp['search_retry'])) {
  2204. $this->_temp['search_retry'] = true;
  2205. /* Bug #9842: Workaround broken Cyrus servers (as of
  2206. * 2.4.7). */
  2207. if ($esearch && ($charset != 'US-ASCII')) {
  2208. $this->_capability()->remove('ESEARCH');
  2209. $this->_setInit('noesearch', true);
  2210. try {
  2211. return $this->_search($query, $options);
  2212. } catch (Horde_Imap_Client_Exception $e) {}
  2213. }
  2214. /* Try to convert charset. */
  2215. if (($e->getCode() === Horde_Imap_Client_Exception::BADCHARSET) &&
  2216. ($charset != 'US-ASCII')) {
  2217. foreach ($this->search_charset->charsets as $val) {
  2218. $this->_temp['search_retry'] = 1;
  2219. $new_query = clone($query);
  2220. try {
  2221. $new_query->charset($val);
  2222. $options['_query'] = $new_query->build($this);
  2223. return $this->_search($new_query, $options);
  2224. } catch (Horde_Imap_Client_Exception $e) {}
  2225. }
  2226. }
  2227. unset($this->_temp['search_retry']);
  2228. }
  2229. throw $e;
  2230. }
  2231. if ($return_sort && !$server_sort) {
  2232. if ($server_seq_sort) {
  2233. $sr->sort();
  2234. if (reset($options['sort']) == Horde_Imap_Client::SORT_REVERSE) {
  2235. $sr->reverse();
  2236. }
  2237. } else {
  2238. if (!isset($this->_temp['clientsort'])) {
  2239. $this->_temp['clientsort'] = new Horde_Imap_Client_Socket_ClientSort($this);
  2240. }
  2241. $sr = $this->getIdsOb($this->_temp['clientsort']->clientSort($sr, $options), !empty($options['sequence']));
  2242. }
  2243. }
  2244. if (!$partial && !empty($options['partial'])) {
  2245. $partial = $this->getIdsOb($options['partial'], true);
  2246. $min = $partial->min - 1;
  2247. $sr = $this->getIdsOb(
  2248. array_slice($sr->ids, $min, $partial->max - $min),
  2249. !empty($options['sequence'])
  2250. );
  2251. }
  2252. $ret = array();
  2253. foreach ($options['results'] as $val) {
  2254. switch ($val) {
  2255. case Horde_Imap_Client::SEARCH_RESULTS_COUNT:
  2256. $ret['count'] = ($esearch && !$partial)
  2257. ? $er['count']
  2258. : count($sr);
  2259. break;
  2260. case Horde_Imap_Client::SEARCH_RESULTS_MATCH:
  2261. $ret['match'] = $sr;
  2262. break;
  2263. case Horde_Imap_Client::SEARCH_RESULTS_MAX:
  2264. $ret['max'] = $esearch
  2265. ? (!$partial && isset($er['max']) ? $er['max'] : null)
  2266. : (count($sr) ? max($sr->ids) : null);
  2267. break;
  2268. case Horde_Imap_Client::SEARCH_RESULTS_MIN:
  2269. $ret['min'] = $esearch
  2270. ? (!$partial && isset($er['min']) ? $er['min'] : null)
  2271. : (count($sr) ? min($sr->ids) : null);
  2272. break;
  2273. case Horde_Imap_Client::SEARCH_RESULTS_RELEVANCY:
  2274. $ret['relevancy'] = ($esearch && isset($er['relevancy'])) ? $er['relevancy'] : array();
  2275. break;
  2276. case Horde_Imap_Client::SEARCH_RESULTS_SAVE:
  2277. $this->_temp['search_save'] = $ret['save'] = $esearch ? empty($resp->data['searchnotsaved']) : false;
  2278. break;
  2279. }
  2280. }
  2281. // Add modseq data, if needed.
  2282. if (!empty($er['modseq'])) {
  2283. $ret['modseq'] = $er['modseq'];
  2284. }
  2285. unset($this->_temp['search_retry']);
  2286. /* Check for EXPUNGEISSUED (RFC 2180 [4.3]/RFC 5530 [3]). */
  2287. if (!empty($resp->data['expungeissued'])) {
  2288. $this->noop();
  2289. }
  2290. return $ret;
  2291. }
  2292. /**
  2293. * Parse a SEARCH/SORT response (RFC 3501 [7.2.5]; RFC 4466 [3];
  2294. * RFC 5256 [4]; RFC 5267 [3]).
  2295. *
  2296. * @param Horde_Imap_Client_Interaction_Pipeline $pipeline Pipeline
  2297. * object.
  2298. * @param array $data A list of IDs (message sequence numbers or UIDs).
  2299. */
  2300. protected function _parseSearch(
  2301. Horde_Imap_Client_Interaction_Pipeline $pipeline,
  2302. $data
  2303. )
  2304. {
  2305. /* More than one search response may be sent. */
  2306. $pipeline->data['searchresp']->add($data);
  2307. }
  2308. /**
  2309. * Parse an ESEARCH response (RFC 4466 [2.6.2])
  2310. * Format: (TAG "a567") UID COUNT 5 ALL 4:19,21,28
  2311. *
  2312. * @param Horde_Imap_Client_Interaction_Pipeline $pipeline Pipeline
  2313. * object.
  2314. * @param Horde_Imap_Client_Tokenize $data The server response.
  2315. */
  2316. protected function _parseEsearch(
  2317. Horde_Imap_Client_Interaction_Pipeline $pipeline,
  2318. Horde_Imap_Client_Tokenize $data
  2319. )
  2320. {
  2321. // Ignore search correlator information
  2322. if ($data->next() === true) {
  2323. $data->flushIterator(false);
  2324. }
  2325. // Ignore UID tag
  2326. $current = $data->next();
  2327. if (Horde_String::upper($current) === 'UID') {
  2328. $current = $data->next();
  2329. }
  2330. do {
  2331. $val = $data->next();
  2332. $tag = Horde_String::upper($current);
  2333. switch ($tag) {
  2334. case 'ALL':
  2335. $this->_parseSearch($pipeline, $val);
  2336. break;
  2337. case 'COUNT':
  2338. case 'MAX':
  2339. case 'MIN':
  2340. case 'MODSEQ':
  2341. case 'RELEVANCY':
  2342. $pipeline->data['esearchresp'][Horde_String::lower($tag)] = $val;
  2343. break;
  2344. case 'PARTIAL':
  2345. // RFC 5267 [4.4]
  2346. $partial = $val->flushIterator();
  2347. $this->_parseSearch($pipeline, end($partial));
  2348. break;
  2349. }
  2350. } while (($current = $data->next()) !== false);
  2351. }
  2352. /**
  2353. */
  2354. protected function _setComparator($comparator)
  2355. {
  2356. $cmd = $this->_command('COMPARATOR');
  2357. foreach ($comparator as $val) {
  2358. $cmd->add(new Horde_Imap_Client_Data_Format_Astring($val));
  2359. }
  2360. $this->_sendCmd($cmd);
  2361. }
  2362. /**
  2363. */
  2364. protected function _getComparator()
  2365. {
  2366. $resp = $this->_sendCmd($this->_command('COMPARATOR'));
  2367. return isset($resp->data['comparator'])
  2368. ? $resp->data['comparator']
  2369. : null;
  2370. }
  2371. /**
  2372. * Parse a COMPARATOR response (RFC 5255 [4.8])
  2373. *
  2374. * @param Horde_Imap_Client_Interaction_Pipeline $pipeline Pipeline
  2375. * object.
  2376. * @param Horde_Imap_Client_Tokenize $data The server response.
  2377. */
  2378. protected function _parseComparator(
  2379. Horde_Imap_Client_Interaction_Pipeline $pipeline,
  2380. $data
  2381. )
  2382. {
  2383. $pipeline->data['comparator'] = $data->next();
  2384. // Ignore optional matching comparator list
  2385. }
  2386. /**
  2387. * @throws Horde_Imap_Client_Exception_NoSupportExtension
  2388. */
  2389. protected function _thread($options)
  2390. {
  2391. $thread_criteria = array(
  2392. Horde_Imap_Client::THREAD_ORDEREDSUBJECT => 'ORDEREDSUBJECT',
  2393. Horde_Imap_Client::THREAD_REFERENCES => 'REFERENCES',
  2394. Horde_Imap_Client::THREAD_REFS => 'REFS'
  2395. );
  2396. $tsort = (isset($options['criteria']))
  2397. ? (is_string($options['criteria']) ? Horde_String::upper($options['criteria']) : $thread_criteria[$options['criteria']])
  2398. : 'ORDEREDSUBJECT';
  2399. if (!$this->_capability('THREAD', $tsort)) {
  2400. switch ($tsort) {
  2401. case 'ORDEREDSUBJECT':
  2402. if (empty($options['search'])) {
  2403. $ids = $this->getIdsOb(Horde_Imap_Client_Ids::ALL, !empty($options['sequence']));
  2404. } else {
  2405. $search_res = $this->search($this->_selected, $options['search'], array('sequence' => !empty($options['sequence'])));
  2406. $ids = $search_res['match'];
  2407. }
  2408. /* Do client-side ORDEREDSUBJECT threading. */
  2409. $query = new Horde_Imap_Client_Fetch_Query();
  2410. $query->envelope();
  2411. $query->imapDate();
  2412. $fetch_res = $this->fetch($this->_selected, $query, array(
  2413. 'ids' => $ids
  2414. ));
  2415. if (!isset($this->_temp['clientsort'])) {
  2416. $this->_temp['clientsort'] = new Horde_Imap_Client_Socket_ClientSort($this);
  2417. }
  2418. return $this->_temp['clientsort']->threadOrderedSubject($fetch_res, empty($options['sequence']));
  2419. case 'REFERENCES':
  2420. case 'REFS':
  2421. throw new Horde_Imap_Client_Exception_NoSupportExtension(
  2422. 'THREAD',
  2423. sprintf('Server does not support "%s" thread sort.', $tsort)
  2424. );
  2425. }
  2426. }
  2427. $cmd = $this->_command(
  2428. empty($options['sequence']) ? 'UID THREAD' : 'THREAD'
  2429. )->add($tsort);
  2430. /* If UTF-8 support is activated, a client MUST send the UTF-8
  2431. * charset specification since charset is mandatory for this
  2432. * command (RFC 6855 [3]; Errata 4029). */
  2433. if (empty($options['search'])) {
  2434. if (!$this->_capability()->isEnabled('UTF8=ACCEPT')) {
  2435. $cmd->add('US-ASCII');
  2436. } else {
  2437. $cmd->add('UTF-8');
  2438. }
  2439. $cmd->add('ALL');
  2440. } else {
  2441. $search_query = $options['search']->build();
  2442. if (!$this->_capability()->isEnabled('UTF8=ACCEPT')) {
  2443. $cmd->add(is_null($search_query['charset']) ? 'US-ASCII' : $search_query['charset']);
  2444. }
  2445. $cmd->add($search_query['query'], true);
  2446. }
  2447. return new Horde_Imap_Client_Data_Thread(
  2448. $this->_sendCmd($cmd)->data['threadparse'],
  2449. empty($options['sequence']) ? 'uid' : 'sequence'
  2450. );
  2451. }
  2452. /**
  2453. * Parse a THREAD response (RFC 5256 [4]).
  2454. *
  2455. * @param Horde_Imap_Client_Interaction_Pipeline $pipeline Pipeline
  2456. * object.
  2457. * @param Horde_Imap_Client_Tokenize $data Thread data.
  2458. */
  2459. protected function _parseThread(
  2460. Horde_Imap_Client_Interaction_Pipeline $pipeline,
  2461. Horde_Imap_Client_Tokenize $data
  2462. )
  2463. {
  2464. $out = array();
  2465. while ($data->next() !== false) {
  2466. $thread = array();
  2467. $this->_parseThreadLevel($thread, $data);
  2468. $out[] = $thread;
  2469. }
  2470. $pipeline->data['threadparse'] = $out;
  2471. }
  2472. /**
  2473. * Parse a level of a THREAD response (RFC 5256 [4]).
  2474. *
  2475. * @param array $thread Results.
  2476. * @param Horde_Imap_Client_Tokenize $data Thread data.
  2477. * @param integer $level The current tree level.
  2478. */
  2479. protected function _parseThreadLevel(&$thread,
  2480. Horde_Imap_Client_Tokenize $data,
  2481. $level = 0)
  2482. {
  2483. while (($curr = $data->next()) !== false) {
  2484. if ($curr === true) {
  2485. $this->_parseThreadLevel($thread, $data, $level);
  2486. } elseif (!is_bool($curr)) {
  2487. $thread[$curr] = $level++;
  2488. }
  2489. }
  2490. }
  2491. /**
  2492. */
  2493. protected function _fetch(Horde_Imap_Client_Fetch_Results $results,
  2494. $queries)
  2495. {
  2496. $pipeline = $this->_pipeline();
  2497. $pipeline->data['fetch_lookup'] = array();
  2498. $pipeline->data['fetch_followup'] = array();
  2499. foreach ($queries as $options) {
  2500. $this->_fetchCmd($pipeline, $options);
  2501. $sequence = $options['ids']->sequence;
  2502. }
  2503. try {
  2504. $resp = $this->_sendCmd($pipeline);
  2505. /* Check for EXPUNGEISSUED (RFC 2180 [4.1]/RFC 5530 [3]). */
  2506. if (!empty($resp->data['expungeissued'])) {
  2507. $this->noop();
  2508. }
  2509. foreach ($resp->fetch as $k => $v) {
  2510. $results->get($sequence ? $k : $v->getUid())->merge($v);
  2511. }
  2512. } catch (Horde_Imap_Client_Exception_ServerResponse $e) {
  2513. if ($e->status === Horde_Imap_Client_Interaction_Server::NO) {
  2514. if ($e->getCode() === $e::UNKNOWNCTE ||
  2515. $e->getCode() === $e::PARSEERROR) {
  2516. /* UNKNOWN-CTE error. Redo the query without the BINARY
  2517. * elements. Also include PARSEERROR in this as
  2518. * Dovecot >= 2.2 binary fetch treats broken email as PARSE
  2519. * error and no longer UNKNOWN-CTE
  2520. */
  2521. if (!empty($pipeline->data['binaryquery'])) {
  2522. foreach ($queries as $val) {
  2523. foreach ($pipeline->data['binaryquery'] as $key2 => $val2) {
  2524. unset($val2['decode']);
  2525. $val['_query']->bodyPart($key2, $val2);
  2526. $val['_query']->remove(Horde_Imap_Client::FETCH_BODYPARTSIZE, $key2);
  2527. }
  2528. $pipeline->data['fetch_followup'][] = $val;
  2529. }
  2530. } else {
  2531. $this->noop();
  2532. }
  2533. } elseif ($sequence) {
  2534. /* A NO response, when coupled with a sequence FETCH, most
  2535. * likely means that messages were expunged. (RFC 2180
  2536. * [4.1]) */
  2537. $this->noop();
  2538. }
  2539. }
  2540. } catch (Exception $e) {
  2541. // For any other error, ignore the Exception - fetch() is nice in
  2542. // that the return value explicitly handles missing data for any
  2543. // given message.
  2544. }
  2545. if (!empty($pipeline->data['fetch_followup'])) {
  2546. $this->_fetch($results, $pipeline->data['fetch_followup']);
  2547. }
  2548. }
  2549. /**
  2550. * Add a FETCH command to the given pipeline.
  2551. *
  2552. * @param Horde_Imap_Client_Interaction_Pipeline $pipeline Pipeline
  2553. * object.
  2554. * @param array $options Fetch query
  2555. * options
  2556. */
  2557. protected function _fetchCmd(
  2558. Horde_Imap_Client_Interaction_Pipeline $pipeline,
  2559. $options
  2560. )
  2561. {
  2562. $fetch = new Horde_Imap_Client_Data_Format_List();
  2563. $sequence = $options['ids']->sequence;
  2564. /* Build an IMAP4rev1 compliant FETCH query. We handle the following
  2565. * criteria:
  2566. * BINARY[.PEEK][<section #>]<<partial>> (RFC 3516)
  2567. * see BODY[] response
  2568. * BINARY.SIZE[<section #>] (RFC 3516)
  2569. * BODY[.PEEK][<section>]<<partial>>
  2570. * <section> = HEADER, HEADER.FIELDS, HEADER.FIELDS.NOT, MIME,
  2571. * TEXT, empty
  2572. * <<partial>> = 0.# (# of bytes)
  2573. * BODYSTRUCTURE
  2574. * ENVELOPE
  2575. * FLAGS
  2576. * INTERNALDATE
  2577. * MODSEQ (RFC 7162)
  2578. * RFC822.SIZE
  2579. * UID
  2580. *
  2581. * No need to support these (can be built from other queries):
  2582. * ===========================================================
  2583. * ALL macro => (FLAGS INTERNALDATE RFC822.SIZE ENVELOPE)
  2584. * BODY => Use BODYSTRUCTURE instead
  2585. * FAST macro => (FLAGS INTERNALDATE RFC822.SIZE)
  2586. * FULL macro => (FLAGS INTERNALDATE RFC822.SIZE ENVELOPE BODY)
  2587. * RFC822 => BODY[]
  2588. * RFC822.HEADER => BODY[HEADER]
  2589. * RFC822.TEXT => BODY[TEXT]
  2590. */
  2591. foreach ($options['_query'] as $type => $c_val) {
  2592. switch ($type) {
  2593. case Horde_Imap_Client::FETCH_STRUCTURE:
  2594. $fetch->add('BODYSTRUCTURE');
  2595. break;
  2596. case Horde_Imap_Client::FETCH_FULLMSG:
  2597. if (empty($c_val['peek'])) {
  2598. $this->openMailbox($this->_selected, Horde_Imap_Client::OPEN_READWRITE);
  2599. }
  2600. $fetch->add(
  2601. 'BODY' .
  2602. (!empty($c_val['peek']) ? '.PEEK' : '') .
  2603. '[]' .
  2604. $this->_partialAtom($c_val)
  2605. );
  2606. break;
  2607. case Horde_Imap_Client::FETCH_HEADERTEXT:
  2608. case Horde_Imap_Client::FETCH_BODYTEXT:
  2609. case Horde_Imap_Client::FETCH_MIMEHEADER:
  2610. case Horde_Imap_Client::FETCH_BODYPART:
  2611. case Horde_Imap_Client::FETCH_HEADERS:
  2612. foreach ($c_val as $key => $val) {
  2613. $cmd = ($key == 0)
  2614. ? ''
  2615. : $key . '.';
  2616. $main_cmd = 'BODY';
  2617. switch ($type) {
  2618. case Horde_Imap_Client::FETCH_HEADERTEXT:
  2619. $cmd .= 'HEADER';
  2620. break;
  2621. case Horde_Imap_Client::FETCH_BODYTEXT:
  2622. $cmd .= 'TEXT';
  2623. break;
  2624. case Horde_Imap_Client::FETCH_MIMEHEADER:
  2625. $cmd .= 'MIME';
  2626. break;
  2627. case Horde_Imap_Client::FETCH_BODYPART:
  2628. // Remove the last dot from the string.
  2629. $cmd = substr($cmd, 0, -1);
  2630. if (!empty($val['decode']) &&
  2631. $this->_capability('BINARY')) {
  2632. $main_cmd = 'BINARY';
  2633. $pipeline->data['binaryquery'][$key] = $val;
  2634. }
  2635. break;
  2636. case Horde_Imap_Client::FETCH_HEADERS:
  2637. $cmd .= 'HEADER.FIELDS';
  2638. if (!empty($val['notsearch'])) {
  2639. $cmd .= '.NOT';
  2640. }
  2641. $cmd .= ' (' . implode(' ', array_map('Horde_String::upper', $val['headers'])) . ')';
  2642. // Maintain a command -> label lookup so we can put
  2643. // the results in the proper location.
  2644. $pipeline->data['fetch_lookup'][$cmd] = $key;
  2645. }
  2646. if (empty($val['peek'])) {
  2647. $this->openMailbox($this->_selected, Horde_Imap_Client::OPEN_READWRITE);
  2648. }
  2649. $fetch->add(
  2650. $main_cmd .
  2651. (!empty($val['peek']) ? '.PEEK' : '') .
  2652. '[' . $cmd . ']' .
  2653. $this->_partialAtom($val)
  2654. );
  2655. }
  2656. break;
  2657. case Horde_Imap_Client::FETCH_BODYPARTSIZE:
  2658. if ($this->_capability('BINARY')) {
  2659. foreach ($c_val as $val) {
  2660. $fetch->add('BINARY.SIZE[' . $val . ']');
  2661. }
  2662. }
  2663. break;
  2664. case Horde_Imap_Client::FETCH_ENVELOPE:
  2665. $fetch->add('ENVELOPE');
  2666. break;
  2667. case Horde_Imap_Client::FETCH_FLAGS:
  2668. $fetch->add('FLAGS');
  2669. break;
  2670. case Horde_Imap_Client::FETCH_IMAPDATE:
  2671. $fetch->add('INTERNALDATE');
  2672. break;
  2673. case Horde_Imap_Client::FETCH_SIZE:
  2674. $fetch->add('RFC822.SIZE');
  2675. break;
  2676. case Horde_Imap_Client::FETCH_UID:
  2677. /* A UID FETCH will always return UID information (RFC 3501
  2678. * [6.4.8]). Don't add to query as it just creates a longer
  2679. * FETCH command. */
  2680. if ($sequence) {
  2681. $fetch->add('UID');
  2682. }
  2683. break;
  2684. case Horde_Imap_Client::FETCH_SEQ:
  2685. /* Nothing we need to add to fetch request unless sequence is
  2686. * the only criteria (see below). */
  2687. break;
  2688. case Horde_Imap_Client::FETCH_MODSEQ:
  2689. /* The 'changedsince' modifier implicitly adds the MODSEQ
  2690. * FETCH item (RFC 7162 [3.1.4.1]). Don't add to query as it
  2691. * just creates a longer FETCH command. */
  2692. if (empty($options['changedsince'])) {
  2693. $fetch->add('MODSEQ');
  2694. }
  2695. break;
  2696. }
  2697. }
  2698. /* If empty fetch, add UID to make command valid. */
  2699. if (!count($fetch)) {
  2700. $fetch->add('UID');
  2701. }
  2702. /* Add changedsince parameters. */
  2703. if (empty($options['changedsince'])) {
  2704. $fetch_cmd = $fetch;
  2705. } else {
  2706. /* We might just want the list of UIDs changed since a given
  2707. * modseq. In that case, we don't have any other FETCH attributes,
  2708. * but RFC 3501 requires at least one specified attribute. */
  2709. $fetch_cmd = array(
  2710. $fetch,
  2711. new Horde_Imap_Client_Data_Format_List(array(
  2712. 'CHANGEDSINCE',
  2713. new Horde_Imap_Client_Data_Format_Number($options['changedsince'])
  2714. ))
  2715. );
  2716. }
  2717. /* The FETCH command should be the only command issued by this library
  2718. * that should ever approach the command length limit.
  2719. * @todo Move this check to a more centralized location (_command()?).
  2720. * For simplification, assume that the UID list is the limiting factor
  2721. * and split this list at a sequence comma delimiter if it exceeds
  2722. * the character limit. */
  2723. foreach ($options['ids']->split($this->_capability()->cmdlength) as $val) {
  2724. $cmd = $this->_command(
  2725. $sequence ? 'FETCH' : 'UID FETCH'
  2726. )->add(array(
  2727. $val,
  2728. $fetch_cmd
  2729. ));
  2730. $pipeline->add($cmd);
  2731. }
  2732. }
  2733. /**
  2734. * Add a partial atom to an IMAP command based on the criteria options.
  2735. *
  2736. * @param array $opts Criteria options.
  2737. *
  2738. * @return string The partial atom.
  2739. */
  2740. protected function _partialAtom($opts)
  2741. {
  2742. if (!empty($opts['length'])) {
  2743. return '<' . (empty($opts['start']) ? 0 : intval($opts['start'])) . '.' . intval($opts['length']) . '>';
  2744. }
  2745. return empty($opts['start'])
  2746. ? ''
  2747. : ('<' . intval($opts['start']) . '>');
  2748. }
  2749. /**
  2750. * Parse a FETCH response (RFC 3501 [7.4.2]). A FETCH response may occur
  2751. * due to a FETCH command, or due to a change in a message's state (i.e.
  2752. * the flags change).
  2753. *
  2754. * @param Horde_Imap_Client_Interaction_Pipeline $pipeline Pipeline
  2755. * object.
  2756. * @param integer $id The message sequence number.
  2757. * @param Horde_Imap_Client_Tokenize $data The server response.
  2758. */
  2759. protected function _parseFetch(
  2760. Horde_Imap_Client_Interaction_Pipeline $pipeline,
  2761. $id,
  2762. Horde_Imap_Client_Tokenize $data
  2763. )
  2764. {
  2765. if ($data->next() !== true) {
  2766. return;
  2767. }
  2768. $ob = $pipeline->fetch->get($id);
  2769. $ob->setSeq($id);
  2770. $flags = $modseq = $uid = false;
  2771. while (($tag = $data->next()) !== false) {
  2772. $tag = Horde_String::upper($tag);
  2773. /* Catch equivalent RFC822 tags, in case server returns them
  2774. * (in error, since we only use BODY in FETCH requests). */
  2775. switch ($tag) {
  2776. case 'RFC822':
  2777. $tag = 'BODY[]';
  2778. break;
  2779. case 'RFC822.HEADER':
  2780. $tag = 'BODY[HEADER]';
  2781. break;
  2782. case 'RFC822.TEXT':
  2783. $tag = 'BODY[TEXT]';
  2784. break;
  2785. }
  2786. switch ($tag) {
  2787. case 'BODYSTRUCTURE':
  2788. $data->next();
  2789. $structure = $this->_parseBodystructure($data);
  2790. $structure->buildMimeIds();
  2791. $ob->setStructure($structure);
  2792. break;
  2793. case 'ENVELOPE':
  2794. $data->next();
  2795. $ob->setEnvelope($this->_parseEnvelope($data));
  2796. break;
  2797. case 'FLAGS':
  2798. $data->next();
  2799. $ob->setFlags($data->flushIterator());
  2800. $flags = true;
  2801. break;
  2802. case 'INTERNALDATE':
  2803. $ob->setImapDate($data->next());
  2804. break;
  2805. case 'RFC822.SIZE':
  2806. $ob->setSize($data->next());
  2807. break;
  2808. case 'UID':
  2809. $ob->setUid($data->next());
  2810. $uid = true;
  2811. break;
  2812. case 'MODSEQ':
  2813. $data->next();
  2814. $modseq = $data->next();
  2815. $data->next();
  2816. /* MODSEQ must be greater than 0, so do sanity checking. */
  2817. if ($modseq > 0) {
  2818. $ob->setModSeq($modseq);
  2819. /* Store MODSEQ value. It may be used as the highestmodseq
  2820. * once a tagged response is received (RFC 7162 [6]). */
  2821. $pipeline->data['modseqs'][] = $modseq;
  2822. }
  2823. break;
  2824. default:
  2825. // Catch BODY[*]<#> responses
  2826. if (strpos($tag, 'BODY[') === 0) {
  2827. // Remove the beginning 'BODY['
  2828. $tag = substr($tag, 5);
  2829. // BODY[HEADER.FIELDS] request
  2830. if (!empty($pipeline->data['fetch_lookup']) &&
  2831. (strpos($tag, 'HEADER.FIELDS') !== false)) {
  2832. $data->next();
  2833. $sig = $tag . ' (' . implode(' ', array_map('Horde_String::upper', $data->flushIterator())) . ')';
  2834. // Ignore the trailing bracket
  2835. $data->next();
  2836. $ob->setHeaders($pipeline->data['fetch_lookup'][$sig], $data->next());
  2837. } else {
  2838. // Remove trailing bracket and octet start info
  2839. $tag = substr($tag, 0, strrpos($tag, ']'));
  2840. if (!strlen($tag)) {
  2841. // BODY[] request
  2842. if (!is_null($tmp = $data->nextStream())) {
  2843. $ob->setFullMsg($tmp);
  2844. }
  2845. } elseif (is_numeric(substr($tag, -1))) {
  2846. // BODY[MIMEID] request
  2847. if (!is_null($tmp = $data->nextStream())) {
  2848. $ob->setBodyPart($tag, $tmp);
  2849. }
  2850. } else {
  2851. // BODY[HEADER|TEXT|MIME] request
  2852. if (($last_dot = strrpos($tag, '.')) === false) {
  2853. $mime_id = 0;
  2854. } else {
  2855. $mime_id = substr($tag, 0, $last_dot);
  2856. $tag = substr($tag, $last_dot + 1);
  2857. }
  2858. if (!is_null($tmp = $data->nextStream())) {
  2859. switch ($tag) {
  2860. case 'HEADER':
  2861. $ob->setHeaderText($mime_id, $tmp);
  2862. break;
  2863. case 'TEXT':
  2864. $ob->setBodyText($mime_id, $tmp);
  2865. break;
  2866. case 'MIME':
  2867. $ob->setMimeHeader($mime_id, $tmp);
  2868. break;
  2869. }
  2870. }
  2871. }
  2872. }
  2873. } elseif (strpos($tag, 'BINARY[') === 0) {
  2874. // Catch BINARY[*]<#> responses
  2875. // Remove the beginning 'BINARY[' and the trailing bracket
  2876. // and octet start info
  2877. $tag = substr($tag, 7, strrpos($tag, ']') - 7);
  2878. $body = $data->nextStream();
  2879. if (is_null($body)) {
  2880. /* Dovecot bug (as of 2.2.12): binary fetch of body
  2881. * part may fail with NIL return if decoding failed on
  2882. * server. Try again with non-decoded body. */
  2883. $bq = $pipeline->data['binaryquery'][$tag];
  2884. unset($bq['decode']);
  2885. $query = new Horde_Imap_Client_Fetch_Query();
  2886. $query->bodyPart($tag, $bq);
  2887. $qids = ($quid = $ob->getUid())
  2888. ? new Horde_Imap_Client_Ids($quid)
  2889. : new Horde_Imap_Client_Ids($id, true);
  2890. $pipeline->data['fetch_followup'][] = array(
  2891. '_query' => $query,
  2892. 'ids' => $qids
  2893. );
  2894. } else {
  2895. $ob->setBodyPart(
  2896. $tag,
  2897. $body,
  2898. empty($this->_temp['literal8']) ? '8bit' : 'binary'
  2899. );
  2900. }
  2901. } elseif (strpos($tag, 'BINARY.SIZE[') === 0) {
  2902. // Catch BINARY.SIZE[*] responses
  2903. // Remove the beginning 'BINARY.SIZE[' and the trailing
  2904. // bracket and octet start info
  2905. $tag = substr($tag, 12, strrpos($tag, ']') - 12);
  2906. $ob->setBodyPartSize($tag, $data->next());
  2907. }
  2908. break;
  2909. }
  2910. }
  2911. /* MODSEQ issue: Oh joy. Per RFC 5162 (see Errata #1807), FETCH FLAGS
  2912. * responses are NOT required to provide UID information, even if
  2913. * QRESYNC is explicitly enabled. Caveat: the FLAGS information
  2914. * returned during a SELECT/EXAMINE MUST contain UIDs so we are OK
  2915. * there.
  2916. * The good news: all decent IMAP servers (Cyrus, Dovecot) will always
  2917. * provide UID information, so this is not normally an issue.
  2918. * The bad news: spec-wise, this behavior cannot be 100% guaranteed.
  2919. * Compromise: We will watch for a FLAGS response with a MODSEQ and
  2920. * check if a UID exists also. If not, put the sequence number in a
  2921. * queue - it is possible the UID information may appear later in an
  2922. * untagged response. When the command is over, double check to make
  2923. * sure there are none of these MODSEQ/FLAGS that are still UID-less.
  2924. * In the (rare) event that there is, don't cache anything and
  2925. * immediately close the mailbox: flags will be correctly sync'd next
  2926. * mailbox open so we only lose a bit of caching efficiency.
  2927. * Otherwise, we could end up with an inconsistent cached state.
  2928. * This Errata has been fixed in 7162 [3.2.4]. */
  2929. if ($flags && $modseq && !$uid) {
  2930. $pipeline->data['modseqs_nouid'][] = $id;
  2931. }
  2932. }
  2933. /**
  2934. * Recursively parse BODYSTRUCTURE data from a FETCH return (see
  2935. * RFC 3501 [7.4.2]).
  2936. *
  2937. * @param Horde_Imap_Client_Tokenize $data Data returned from the server.
  2938. *
  2939. * @return Horde_Mime_Part Mime part object.
  2940. */
  2941. protected function _parseBodystructure(Horde_Imap_Client_Tokenize $data)
  2942. {
  2943. $ob = new Horde_Mime_Part();
  2944. // If index 0 is an array, this is a multipart part.
  2945. if (($entry = $data->next()) === true) {
  2946. do {
  2947. $ob->addPart($this->_parseBodystructure($data));
  2948. } while (($entry = $data->next()) === true);
  2949. // The subpart type.
  2950. $ob->setType('multipart/' . $entry);
  2951. // After the subtype is further extension information. This
  2952. // information MAY appear for BODYSTRUCTURE requests.
  2953. // This is parameter information.
  2954. if (($tmp = $data->next()) === false) {
  2955. return $ob;
  2956. } elseif ($tmp === true) {
  2957. foreach ($this->_parseStructureParams($data) as $key => $val) {
  2958. $ob->setContentTypeParameter($key, $val);
  2959. }
  2960. }
  2961. } else {
  2962. $ob->setType($entry . '/' . $data->next());
  2963. if ($data->next() === true) {
  2964. foreach ($this->_parseStructureParams($data) as $key => $val) {
  2965. $ob->setContentTypeParameter($key, $val);
  2966. }
  2967. }
  2968. if (!is_null($tmp = $data->next())) {
  2969. $ob->setContentId($tmp);
  2970. }
  2971. if (!is_null($tmp = $data->next())) {
  2972. $ob->setDescription(Horde_Mime::decode($tmp));
  2973. }
  2974. $te = $data->next();
  2975. $bytes = $data->next();
  2976. if (!is_null($te)) {
  2977. $ob->setTransferEncoding($te);
  2978. /* Base64 transfer encoding is approx. 33% larger than
  2979. * original data size (RFC 2045 [6.8]). Return from
  2980. * BODYSTRUCTURE is the size of the ENCODED data (RFC 3501
  2981. * [7.4.2]). */
  2982. if (strcasecmp($te, 'base64') === 0) {
  2983. $bytes *= 0.75;
  2984. }
  2985. }
  2986. $ob->setBytes($bytes);
  2987. // If the type is 'message/rfc822' or 'text/*', several extra
  2988. // fields are included
  2989. switch ($ob->getPrimaryType()) {
  2990. case 'message':
  2991. if ($ob->getSubType() == 'rfc822') {
  2992. if ($data->next() === true) {
  2993. // Ignore: envelope
  2994. $data->flushIterator(false);
  2995. }
  2996. if ($data->next() === true) {
  2997. $ob->addPart($this->_parseBodystructure($data));
  2998. }
  2999. $data->next(); // Ignore: lines
  3000. }
  3001. break;
  3002. case 'text':
  3003. $data->next(); // Ignore: lines
  3004. break;
  3005. }
  3006. // After the subtype is further extension information. This
  3007. // information MAY appear for BODYSTRUCTURE requests.
  3008. // Ignore: MD5
  3009. if ($data->next() === false) {
  3010. return $ob;
  3011. }
  3012. }
  3013. // This is disposition information
  3014. if (($tmp = $data->next()) === false) {
  3015. return $ob;
  3016. } elseif ($tmp === true) {
  3017. $ob->setDisposition($data->next());
  3018. if ($data->next() === true) {
  3019. foreach ($this->_parseStructureParams($data) as $key => $val) {
  3020. $ob->setDispositionParameter($key, $val);
  3021. }
  3022. }
  3023. $data->next();
  3024. }
  3025. // This is language information. It is either a single value or a list
  3026. // of values.
  3027. if (($tmp = $data->next()) === false) {
  3028. return $ob;
  3029. } elseif (!is_null($tmp)) {
  3030. $ob->setLanguage(($tmp === true) ? $data->flushIterator() : $tmp);
  3031. }
  3032. // Ignore location (RFC 2557) and consume closing paren.
  3033. $data->flushIterator(false);
  3034. return $ob;
  3035. }
  3036. /**
  3037. * Helper function to parse a parameters-like tokenized array.
  3038. *
  3039. * @param mixed $data Message data. Either a Horde_Imap_Client_Tokenize
  3040. * object or null.
  3041. *
  3042. * @return array The parameter array.
  3043. */
  3044. protected function _parseStructureParams($data)
  3045. {
  3046. $params = array();
  3047. if (is_null($data)) {
  3048. return $params;
  3049. }
  3050. while (($name = $data->next()) !== false) {
  3051. $params[Horde_String::lower($name)] = $data->next();
  3052. }
  3053. $cp = new Horde_Mime_Headers_ContentParam('Unused', $params);
  3054. return $cp->params;
  3055. }
  3056. /**
  3057. * Parse ENVELOPE data from a FETCH return (see RFC 3501 [7.4.2]).
  3058. *
  3059. * @param Horde_Imap_Client_Tokenize $data Data returned from the server.
  3060. *
  3061. * @return Horde_Imap_Client_Data_Envelope An envelope object.
  3062. */
  3063. protected function _parseEnvelope(Horde_Imap_Client_Tokenize $data)
  3064. {
  3065. // 'route', the 2nd element, is deprecated by RFC 2822.
  3066. $addr_structure = array(
  3067. 0 => 'personal',
  3068. 2 => 'mailbox',
  3069. 3 => 'host'
  3070. );
  3071. $env_data = array(
  3072. 0 => 'date',
  3073. 1 => 'subject',
  3074. 2 => 'from',
  3075. 3 => 'sender',
  3076. 4 => 'reply_to',
  3077. 5 => 'to',
  3078. 6 => 'cc',
  3079. 7 => 'bcc',
  3080. 8 => 'in_reply_to',
  3081. 9 => 'message_id'
  3082. );
  3083. $addr_ob = new Horde_Mail_Rfc822_Address();
  3084. $env_addrs = $this->getParam('envelope_addrs');
  3085. $env_str = $this->getParam('envelope_string');
  3086. $key = 0;
  3087. $ret = new Horde_Imap_Client_Data_Envelope();
  3088. while (($val = $data->next()) !== false) {
  3089. if (!isset($env_data[$key]) || is_null($val)) {
  3090. ++$key;
  3091. continue;
  3092. }
  3093. if (is_string($val)) {
  3094. // These entries are text fields.
  3095. $ret->{$env_data[$key]} = substr($val, 0, $env_str);
  3096. } else {
  3097. // These entries are address structures.
  3098. $group = null;
  3099. $key2 = 0;
  3100. $tmp = new Horde_Mail_Rfc822_List();
  3101. while ($data->next() !== false) {
  3102. $a_val = $data->flushIterator();
  3103. // RFC 3501 [7.4.2]: Group entry when host is NIL.
  3104. // Group end when mailbox is NIL; otherwise, this is
  3105. // mailbox name.
  3106. if (is_null($a_val[3])) {
  3107. if (is_null($a_val[2])) {
  3108. $group = null;
  3109. } else {
  3110. $group = new Horde_Mail_Rfc822_Group($a_val[2]);
  3111. $tmp->add($group);
  3112. }
  3113. } else {
  3114. $addr = clone $addr_ob;
  3115. foreach ($addr_structure as $add_key => $add_val) {
  3116. if (!is_null($a_val[$add_key])) {
  3117. $addr->$add_val = $a_val[$add_key];
  3118. }
  3119. }
  3120. if ($group) {
  3121. $group->addresses->add($addr);
  3122. } else {
  3123. $tmp->add($addr);
  3124. }
  3125. }
  3126. if (++$key2 >= $env_addrs) {
  3127. $data->flushIterator(false);
  3128. break;
  3129. }
  3130. }
  3131. $ret->{$env_data[$key]} = $tmp;
  3132. }
  3133. ++$key;
  3134. }
  3135. return $ret;
  3136. }
  3137. /**
  3138. */
  3139. protected function _vanished($modseq, Horde_Imap_Client_Ids $ids)
  3140. {
  3141. $pipeline = $this->_pipeline(
  3142. $this->_command('UID FETCH')->add(array(
  3143. strval($ids),
  3144. 'UID',
  3145. new Horde_Imap_Client_Data_Format_List(array(
  3146. 'VANISHED',
  3147. 'CHANGEDSINCE',
  3148. new Horde_Imap_Client_Data_Format_Number($modseq)
  3149. ))
  3150. ))
  3151. );
  3152. $pipeline->data['vanished'] = $this->getIdsOb();
  3153. return $this->_sendCmd($pipeline)->data['vanished'];
  3154. }
  3155. /**
  3156. */
  3157. protected function _store($options)
  3158. {
  3159. $pipeline = $this->_storeCmd($options);
  3160. $pipeline->data['modified'] = $this->getIdsOb();
  3161. try {
  3162. $resp = $this->_sendCmd($pipeline);
  3163. /* Check for EXPUNGEISSUED (RFC 2180 [4.2]/RFC 5530 [3]). */
  3164. if (!empty($resp->data['expungeissued'])) {
  3165. $this->noop();
  3166. }
  3167. return $resp->data['modified'];
  3168. } catch (Horde_Imap_Client_Exception_ServerResponse $e) {
  3169. /* A NO response, when coupled with a sequence STORE and
  3170. * non-SILENT behavior, most likely means that messages were
  3171. * expunged. RFC 2180 [4.2] */
  3172. if (empty($pipeline->data['store_silent']) &&
  3173. !empty($options['sequence']) &&
  3174. ($e->status === Horde_Imap_Client_Interaction_Server::NO)) {
  3175. $this->noop();
  3176. }
  3177. return $pipeline->data['modified'];
  3178. }
  3179. }
  3180. /**
  3181. * Create a store command.
  3182. *
  3183. * @param array $options See Horde_Imap_Client_Base#_store().
  3184. *
  3185. * @return Horde_Imap_Client_Interaction_Pipeline Pipeline object.
  3186. */
  3187. protected function _storeCmd($options)
  3188. {
  3189. $cmds = array();
  3190. $silent = empty($options['unchangedsince'])
  3191. ? !($this->_debug->debug || $this->_initCache(true))
  3192. : false;
  3193. if (!empty($options['replace'])) {
  3194. $cmds[] = array(
  3195. 'FLAGS' . ($silent ? '.SILENT' : ''),
  3196. $options['replace']
  3197. );
  3198. } else {
  3199. foreach (array('add' => '+', 'remove' => '-') as $k => $v) {
  3200. if (!empty($options[$k])) {
  3201. $cmds[] = array(
  3202. $v . 'FLAGS' . ($silent ? '.SILENT' : ''),
  3203. $options[$k]
  3204. );
  3205. }
  3206. }
  3207. }
  3208. $pipeline = $this->_pipeline();
  3209. $pipeline->data['store_silent'] = $silent;
  3210. foreach ($cmds as $val) {
  3211. $cmd = $this->_command(
  3212. empty($options['sequence']) ? 'UID STORE' : 'STORE'
  3213. )->add(strval($options['ids']));
  3214. if (!empty($options['unchangedsince'])) {
  3215. $cmd->add(new Horde_Imap_Client_Data_Format_List(array(
  3216. 'UNCHANGEDSINCE',
  3217. new Horde_Imap_Client_Data_Format_Number(intval($options['unchangedsince']))
  3218. )));
  3219. }
  3220. $cmd->add($val);
  3221. $pipeline->add($cmd);
  3222. }
  3223. return $pipeline;
  3224. }
  3225. /**
  3226. */
  3227. protected function _copy(Horde_Imap_Client_Mailbox $dest, $options)
  3228. {
  3229. /* Check for MOVE command (RFC 6851). */
  3230. $move_cmd = (!empty($options['move']) &&
  3231. $this->_capability('MOVE'));
  3232. $cmd = $this->_pipeline(
  3233. $this->_command(
  3234. ($options['ids']->sequence ? '' : 'UID ') . ($move_cmd ? 'MOVE' : 'COPY')
  3235. )->add(array(
  3236. strval($options['ids']),
  3237. $this->_getMboxFormatOb($dest)
  3238. ))
  3239. );
  3240. $cmd->data['copydest'] = $dest;
  3241. // COPY returns no untagged information (RFC 3501 [6.4.7])
  3242. try {
  3243. $resp = $this->_sendCmd($cmd);
  3244. } catch (Horde_Imap_Client_Exception $e) {
  3245. if (!empty($options['create']) &&
  3246. !empty($e->resp_data['trycreate'])) {
  3247. $this->createMailbox($dest);
  3248. unset($options['create']);
  3249. return $this->_copy($dest, $options);
  3250. }
  3251. throw $e;
  3252. }
  3253. // If moving, delete the old messages now. Short-circuit if nothing
  3254. // was moved.
  3255. if (!$move_cmd &&
  3256. !empty($options['move']) &&
  3257. (isset($resp->data['copyuid']) ||
  3258. !$this->_capability('UIDPLUS'))) {
  3259. $this->expunge($this->_selected, array(
  3260. 'delete' => true,
  3261. 'ids' => $options['ids']
  3262. ));
  3263. }
  3264. return isset($resp->data['copyuid'])
  3265. ? $resp->data['copyuid']
  3266. : true;
  3267. }
  3268. /**
  3269. */
  3270. protected function _setQuota(Horde_Imap_Client_Mailbox $root, $resources)
  3271. {
  3272. $limits = new Horde_Imap_Client_Data_Format_List();
  3273. foreach ($resources as $key => $val) {
  3274. $limits->add(array(
  3275. Horde_String::upper($key),
  3276. new Horde_Imap_Client_Data_Format_Number($val)
  3277. ));
  3278. }
  3279. $this->_sendCmd(
  3280. $this->_command('SETQUOTA')->add(array(
  3281. $this->_getMboxFormatOb($root),
  3282. $limits
  3283. ))
  3284. );
  3285. }
  3286. /**
  3287. */
  3288. protected function _getQuota(Horde_Imap_Client_Mailbox $root)
  3289. {
  3290. $pipeline = $this->_pipeline(
  3291. $this->_command('GETQUOTA')->add(
  3292. $this->_getMboxFormatOb($root)
  3293. )
  3294. );
  3295. $pipeline->data['quotaresp'] = array();
  3296. return reset($this->_sendCmd($pipeline)->data['quotaresp']);
  3297. }
  3298. /**
  3299. * Parse a QUOTA response (RFC 2087 [5.1]).
  3300. *
  3301. * @param Horde_Imap_Client_Interaction_Pipeline $pipeline Pipeline
  3302. * object.
  3303. * @param Horde_Imap_Client_Tokenize $data The server response.
  3304. */
  3305. protected function _parseQuota(
  3306. Horde_Imap_Client_Interaction_Pipeline $pipeline,
  3307. Horde_Imap_Client_Tokenize $data
  3308. )
  3309. {
  3310. $c = &$pipeline->data['quotaresp'];
  3311. $root = $data->next();
  3312. $c[$root] = array();
  3313. $data->next();
  3314. while (($curr = $data->next()) !== false) {
  3315. $c[$root][Horde_String::lower($curr)] = array(
  3316. 'usage' => $data->next(),
  3317. 'limit' => $data->next()
  3318. );
  3319. }
  3320. }
  3321. /**
  3322. */
  3323. protected function _getQuotaRoot(Horde_Imap_Client_Mailbox $mailbox)
  3324. {
  3325. $pipeline = $this->_pipeline(
  3326. $this->_command('GETQUOTAROOT')->add(
  3327. $this->_getMboxFormatOb($mailbox)
  3328. )
  3329. );
  3330. $pipeline->data['quotaresp'] = array();
  3331. return $this->_sendCmd($pipeline)->data['quotaresp'];
  3332. }
  3333. /**
  3334. */
  3335. protected function _setACL(Horde_Imap_Client_Mailbox $mailbox, $identifier,
  3336. $options)
  3337. {
  3338. // SETACL returns no untagged information (RFC 4314 [3.1]).
  3339. $this->_sendCmd(
  3340. $this->_command('SETACL')->add(array(
  3341. $this->_getMboxFormatOb($mailbox),
  3342. new Horde_Imap_Client_Data_Format_Astring($identifier),
  3343. new Horde_Imap_Client_Data_Format_Astring($options['rights'])
  3344. ))
  3345. );
  3346. }
  3347. /**
  3348. */
  3349. protected function _deleteACL(Horde_Imap_Client_Mailbox $mailbox, $identifier)
  3350. {
  3351. // DELETEACL returns no untagged information (RFC 4314 [3.2]).
  3352. $this->_sendCmd(
  3353. $this->_command('DELETEACL')->add(array(
  3354. $this->_getMboxFormatOb($mailbox),
  3355. new Horde_Imap_Client_Data_Format_Astring($identifier)
  3356. ))
  3357. );
  3358. }
  3359. /**
  3360. */
  3361. protected function _getACL(Horde_Imap_Client_Mailbox $mailbox)
  3362. {
  3363. return $this->_sendCmd(
  3364. $this->_command('GETACL')->add(
  3365. $this->_getMboxFormatOb($mailbox)
  3366. )
  3367. )->data['getacl'];
  3368. }
  3369. /**
  3370. * Parse an ACL response (RFC 4314 [3.6]).
  3371. *
  3372. * @param Horde_Imap_Client_Interaction_Pipeline $pipeline Pipeline
  3373. * object.
  3374. * @param Horde_Imap_Client_Tokenize $data The server response.
  3375. */
  3376. protected function _parseACL(
  3377. Horde_Imap_Client_Interaction_Pipeline $pipeline,
  3378. Horde_Imap_Client_Tokenize $data
  3379. )
  3380. {
  3381. $acl = array();
  3382. // Ignore mailbox argument -> index 1
  3383. $data->next();
  3384. while (($curr = $data->next()) !== false) {
  3385. $acl[$curr] = ($curr[0] === '-')
  3386. ? new Horde_Imap_Client_Data_AclNegative($data->next())
  3387. : new Horde_Imap_Client_Data_Acl($data->next());
  3388. }
  3389. $pipeline->data['getacl'] = $acl;
  3390. }
  3391. /**
  3392. */
  3393. protected function _listACLRights(Horde_Imap_Client_Mailbox $mailbox,
  3394. $identifier)
  3395. {
  3396. $resp = $this->_sendCmd(
  3397. $this->_command('LISTRIGHTS')->add(array(
  3398. $this->_getMboxFormatOb($mailbox),
  3399. new Horde_Imap_Client_Data_Format_Astring($identifier)
  3400. ))
  3401. );
  3402. return isset($resp->data['listaclrights'])
  3403. ? $resp->data['listaclrights']
  3404. : new Horde_Imap_Client_Data_AclRights();
  3405. }
  3406. /**
  3407. * Parse a LISTRIGHTS response (RFC 4314 [3.7]).
  3408. *
  3409. * @param Horde_Imap_Client_Interaction_Pipeline $pipeline Pipeline
  3410. * object.
  3411. * @param Horde_Imap_Client_Tokenize $data The server response.
  3412. */
  3413. protected function _parseListRights(
  3414. Horde_Imap_Client_Interaction_Pipeline $pipeline,
  3415. Horde_Imap_Client_Tokenize $data
  3416. )
  3417. {
  3418. // Ignore mailbox and identifier arguments
  3419. $data->next();
  3420. $data->next();
  3421. $pipeline->data['listaclrights'] = new Horde_Imap_Client_Data_AclRights(
  3422. str_split($data->next()),
  3423. $data->flushIterator()
  3424. );
  3425. }
  3426. /**
  3427. */
  3428. protected function _getMyACLRights(Horde_Imap_Client_Mailbox $mailbox)
  3429. {
  3430. $resp = $this->_sendCmd(
  3431. $this->_command('MYRIGHTS')->add(
  3432. $this->_getMboxFormatOb($mailbox)
  3433. )
  3434. );
  3435. return isset($resp->data['myrights'])
  3436. ? $resp->data['myrights']
  3437. : new Horde_Imap_Client_Data_Acl();
  3438. }
  3439. /**
  3440. * Parse a MYRIGHTS response (RFC 4314 [3.8]).
  3441. *
  3442. * @param Horde_Imap_Client_Interaction_Pipeline $pipeline Pipeline
  3443. * object.
  3444. * @param Horde_Imap_Client_Tokenize $data The server response.
  3445. */
  3446. protected function _parseMyRights(
  3447. Horde_Imap_Client_Interaction_Pipeline $pipeline,
  3448. Horde_Imap_Client_Tokenize $data
  3449. )
  3450. {
  3451. // Ignore 1st token (mailbox name)
  3452. $data->next();
  3453. $pipeline->data['myrights'] = new Horde_Imap_Client_Data_Acl($data->next());
  3454. }
  3455. /**
  3456. */
  3457. protected function _getMetadata(Horde_Imap_Client_Mailbox $mailbox,
  3458. $entries, $options)
  3459. {
  3460. $pipeline = $this->_pipeline();
  3461. $pipeline->data['metadata'] = array();
  3462. if ($this->_capability('METADATA') ||
  3463. (strlen($mailbox) && $this->_capability('METADATA-SERVER'))) {
  3464. $cmd_options = new Horde_Imap_Client_Data_Format_List();
  3465. if (!empty($options['maxsize'])) {
  3466. $cmd_options->add(array(
  3467. 'MAXSIZE',
  3468. new Horde_Imap_Client_Data_Format_Number($options['maxsize'])
  3469. ));
  3470. }
  3471. if (!empty($options['depth'])) {
  3472. $cmd_options->add(array(
  3473. 'DEPTH',
  3474. new Horde_Imap_Client_Data_Format_Number($options['depth'])
  3475. ));
  3476. }
  3477. $queries = new Horde_Imap_Client_Data_Format_List();
  3478. foreach ($entries as $md_entry) {
  3479. $queries->add(new Horde_Imap_Client_Data_Format_Astring($md_entry));
  3480. }
  3481. $cmd = $this->_command('GETMETADATA')->add(
  3482. $this->_getMboxFormatOb($mailbox)
  3483. );
  3484. if (count($cmd_options)) {
  3485. $cmd->add($cmd_options);
  3486. }
  3487. $cmd->add($queries);
  3488. $pipeline->add($cmd);
  3489. } else {
  3490. if (!$this->_capability('ANNOTATEMORE') &&
  3491. !$this->_capability('ANNOTATEMORE2')) {
  3492. throw new Horde_Imap_Client_Exception_NoSupportExtension('METADATA');
  3493. }
  3494. $queries = array();
  3495. foreach ($entries as $md_entry) {
  3496. list($entry, $type) = $this->_getAnnotateMoreEntry($md_entry);
  3497. if (!isset($queries[$type])) {
  3498. $queries[$type] = new Horde_Imap_Client_Data_Format_List();
  3499. }
  3500. $queries[$type]->add(new Horde_Imap_Client_Data_Format_String($entry));
  3501. }
  3502. foreach ($queries as $key => $val) {
  3503. // TODO: Honor maxsize and depth options.
  3504. $pipeline->add(
  3505. $this->_command('GETANNOTATION')->add(array(
  3506. $this->_getMboxFormatOb($mailbox),
  3507. $val,
  3508. new Horde_Imap_Client_Data_Format_String($key)
  3509. ))
  3510. );
  3511. }
  3512. }
  3513. return $this->_sendCmd($pipeline)->data['metadata'];
  3514. }
  3515. /**
  3516. * Split a name for the METADATA extension into the correct syntax for the
  3517. * older ANNOTATEMORE version.
  3518. *
  3519. * @param string $name A name for a metadata entry.
  3520. *
  3521. * @return array A list of two elements: The entry name and the value
  3522. * type.
  3523. *
  3524. * @throws Horde_Imap_Client_Exception
  3525. */
  3526. protected function _getAnnotateMoreEntry($name)
  3527. {
  3528. if (substr($name, 0, 7) === '/shared') {
  3529. return array(substr($name, 7), 'value.shared');
  3530. } else if (substr($name, 0, 8) === '/private') {
  3531. return array(substr($name, 8), 'value.priv');
  3532. }
  3533. $e = new Horde_Imap_Client_Exception(
  3534. Horde_Imap_Client_Translation::r("Invalid METADATA entry: \"%s\"."),
  3535. Horde_Imap_Client_Exception::METADATA_INVALID
  3536. );
  3537. $e->messagePrintf(array($name));
  3538. throw $e;
  3539. }
  3540. /**
  3541. */
  3542. protected function _setMetadata(Horde_Imap_Client_Mailbox $mailbox, $data)
  3543. {
  3544. if ($this->_capability('METADATA') ||
  3545. (strlen($mailbox) && $this->_capability('METADATA-SERVER'))) {
  3546. $data_elts = new Horde_Imap_Client_Data_Format_List();
  3547. foreach ($data as $key => $value) {
  3548. $data_elts->add(array(
  3549. new Horde_Imap_Client_Data_Format_Astring($key),
  3550. /* METADATA supports literal8 - thus, it implicitly
  3551. * supports non-ASCII characters in the data. */
  3552. new Horde_Imap_Client_Data_Format_Nstring_Nonascii($value)
  3553. ));
  3554. }
  3555. $cmd = $this->_command('SETMETADATA')->add(array(
  3556. $this->_getMboxFormatOb($mailbox),
  3557. $data_elts
  3558. ));
  3559. } else {
  3560. if (!$this->_capability('ANNOTATEMORE') &&
  3561. !$this->_capability('ANNOTATEMORE2')) {
  3562. throw new Horde_Imap_Client_Exception_NoSupportExtension('METADATA');
  3563. }
  3564. $cmd = $this->_pipeline();
  3565. foreach ($data as $md_entry => $value) {
  3566. list($entry, $type) = $this->_getAnnotateMoreEntry($md_entry);
  3567. $cmd->add(
  3568. $this->_command('SETANNOTATION')->add(array(
  3569. $this->_getMboxFormatOb($mailbox),
  3570. new Horde_Imap_Client_Data_Format_String($entry),
  3571. new Horde_Imap_Client_Data_Format_List(array(
  3572. new Horde_Imap_Client_Data_Format_String($type),
  3573. /* ANNOTATEMORE supports literal8 - thus, it
  3574. * implicitly supports non-ASCII characters in the
  3575. * data. */
  3576. new Horde_Imap_Client_Data_Format_Nstring_Nonascii($value)
  3577. ))
  3578. ))
  3579. );
  3580. }
  3581. }
  3582. $this->_sendCmd($cmd);
  3583. }
  3584. /**
  3585. * Parse an ANNOTATION response (ANNOTATEMORE/ANNOTATEMORE2).
  3586. *
  3587. * @param Horde_Imap_Client_Interaction_Pipeline $pipeline Pipeline
  3588. * object.
  3589. * @param Horde_Imap_Client_Tokenize $data The server response.
  3590. *
  3591. * @throws Horde_Imap_Client_Exception
  3592. */
  3593. protected function _parseAnnotation(
  3594. Horde_Imap_Client_Interaction_Pipeline $pipeline,
  3595. Horde_Imap_Client_Tokenize $data
  3596. )
  3597. {
  3598. // Mailbox name is in UTF7-IMAP.
  3599. $mbox = Horde_Imap_Client_Mailbox::get($data->next(), true);
  3600. $entry = $data->next();
  3601. // Ignore unsolicited responses.
  3602. if ($data->next() !== true) {
  3603. return;
  3604. }
  3605. while (($type = $data->next()) !== false) {
  3606. switch ($type) {
  3607. case 'value.priv':
  3608. $pipeline->data['metadata'][strval($mbox)]['/private' . $entry] = $data->next();
  3609. break;
  3610. case 'value.shared':
  3611. $pipeline->data['metadata'][strval($mbox)]['/shared' . $entry] = $data->next();
  3612. break;
  3613. default:
  3614. $e = new Horde_Imap_Client_Exception(
  3615. Horde_Imap_Client_Translation::r("Invalid METADATA value type \"%s\"."),
  3616. Horde_Imap_Client_Exception::METADATA_INVALID
  3617. );
  3618. $e->messagePrintf(array($type));
  3619. throw $e;
  3620. }
  3621. }
  3622. }
  3623. /**
  3624. * Parse a METADATA response (RFC 5464 [4.4]).
  3625. *
  3626. * @param Horde_Imap_Client_Interaction_Pipeline $pipeline Pipeline
  3627. * object.
  3628. * @param Horde_Imap_Client_Tokenize $data The server response.
  3629. *
  3630. * @throws Horde_Imap_Client_Exception
  3631. */
  3632. protected function _parseMetadata(
  3633. Horde_Imap_Client_Interaction_Pipeline $pipeline,
  3634. Horde_Imap_Client_Tokenize $data
  3635. )
  3636. {
  3637. // Mailbox name is in UTF7-IMAP.
  3638. $mbox = Horde_Imap_Client_Mailbox::get($data->next(), true);
  3639. // Ignore unsolicited responses.
  3640. if ($data->next() === true) {
  3641. while (($entry = $data->next()) !== false) {
  3642. $pipeline->data['metadata'][strval($mbox)][$entry] = $data->next();
  3643. }
  3644. }
  3645. }
  3646. /* Overriden methods. */
  3647. /**
  3648. * @param array $opts Options:
  3649. * - decrement: (boolean) If true, decrement the message count.
  3650. * - pipeline: (Horde_Imap_Client_Interaction_Pipeline) Pipeline object.
  3651. */
  3652. protected function _deleteMsgs(Horde_Imap_Client_Mailbox $mailbox,
  3653. Horde_Imap_Client_Ids $ids,
  3654. array $opts = array())
  3655. {
  3656. /* If there are pending FETCH cache writes, we need to write them
  3657. * before the UID -> sequence number mapping changes. */
  3658. if (isset($opts['pipeline'])) {
  3659. $this->_updateCache($opts['pipeline']->fetch);
  3660. }
  3661. $res = parent::_deleteMsgs($mailbox, $ids);
  3662. if (isset($this->_temp['expunged'])) {
  3663. $this->_temp['expunged']->add($res);
  3664. }
  3665. if (!empty($opts['decrement'])) {
  3666. $mbox_ob = $this->_mailboxOb();
  3667. $mbox_ob->setStatus(
  3668. Horde_Imap_Client::STATUS_MESSAGES,
  3669. $mbox_ob->getStatus(Horde_Imap_Client::STATUS_MESSAGES) - count($ids)
  3670. );
  3671. }
  3672. }
  3673. /* Internal functions. */
  3674. /**
  3675. * Return the proper mailbox format object based on the server's
  3676. * capabilities.
  3677. *
  3678. * @param string $mailbox The mailbox.
  3679. * @param boolean $list Is this object used in a LIST command?
  3680. *
  3681. * @return Horde_Imap_Client_Data_Format_Mailbox A mailbox format object.
  3682. */
  3683. protected function _getMboxFormatOb($mailbox, $list = false)
  3684. {
  3685. if ($this->_capability()->isEnabled('UTF8=ACCEPT')) {
  3686. try {
  3687. return $list
  3688. ? new Horde_Imap_Client_Data_Format_ListMailbox_Utf8($mailbox)
  3689. : new Horde_Imap_Client_Data_Format_Mailbox_Utf8($mailbox);
  3690. } catch (Horde_Imap_Client_Data_Format_Exception $e) {}
  3691. }
  3692. return $list
  3693. ? new Horde_Imap_Client_Data_Format_ListMailbox($mailbox)
  3694. : new Horde_Imap_Client_Data_Format_Mailbox($mailbox);
  3695. }
  3696. /**
  3697. * Sends command(s) to the IMAP server. A connection to the server must
  3698. * have already been made.
  3699. *
  3700. * @param mixed $cmd Either a Command object or a Pipeline object.
  3701. *
  3702. * @return Horde_Imap_Client_Interaction_Pipeline A pipeline object.
  3703. * @throws Horde_Imap_Client_Exception
  3704. */
  3705. protected function _sendCmd($cmd)
  3706. {
  3707. $pipeline = ($cmd instanceof Horde_Imap_Client_Interaction_Command)
  3708. ? $this->_pipeline($cmd)
  3709. : $cmd;
  3710. if (!empty($this->_cmdQueue)) {
  3711. /* Add commands in reverse order. */
  3712. foreach (array_reverse($this->_cmdQueue) as $val) {
  3713. $pipeline->add($val, true);
  3714. }
  3715. $this->_cmdQueue = array();
  3716. }
  3717. $cmd_list = array();
  3718. foreach ($pipeline as $val) {
  3719. if ($val->continuation) {
  3720. $this->_sendCmdChunk($pipeline, $cmd_list);
  3721. $this->_sendCmdChunk($pipeline, array($val));
  3722. $cmd_list = array();
  3723. } else {
  3724. $cmd_list[] = $val;
  3725. }
  3726. }
  3727. $this->_sendCmdChunk($pipeline, $cmd_list);
  3728. /* If any FLAGS responses contain MODSEQs but not UIDs, don't
  3729. * cache any data and immediately close the mailbox. */
  3730. foreach ($pipeline->data['modseqs_nouid'] as $val) {
  3731. if (!$pipeline->fetch[$val]->getUid()) {
  3732. $this->_debug->info(
  3733. 'Server provided FLAGS MODSEQ without providing UID.'
  3734. );
  3735. $this->close();
  3736. return $pipeline;
  3737. }
  3738. }
  3739. /* Update HIGHESTMODSEQ value. */
  3740. if (!empty($pipeline->data['modseqs'])) {
  3741. $modseq = max($pipeline->data['modseqs']);
  3742. $this->_mailboxOb()->setStatus(Horde_Imap_Client::STATUS_HIGHESTMODSEQ, $modseq);
  3743. /* CONDSTORE has not yet updated flag information, so don't update
  3744. * modseq yet. */
  3745. if ($this->_capability()->isEnabled('QRESYNC')) {
  3746. $this->_updateModSeq($modseq);
  3747. }
  3748. }
  3749. /* Update cache items. */
  3750. $this->_updateCache($pipeline->fetch);
  3751. return $pipeline;
  3752. }
  3753. /**
  3754. * Send a chunk of commands and/or continuation fragments to the server.
  3755. *
  3756. * @param Horde_Imap_Client_Interaction_Pipeline $pipeline The pipeline
  3757. * object.
  3758. * @param array $chunk List of commands to send.
  3759. *
  3760. * @throws Horde_Imap_Client_Exception
  3761. */
  3762. protected function _sendCmdChunk($pipeline, $chunk)
  3763. {
  3764. if (empty($chunk)) {
  3765. return;
  3766. }
  3767. $cmd_count = count($chunk);
  3768. $exception = null;
  3769. foreach ($chunk as $val) {
  3770. $val->pipeline = $pipeline;
  3771. try {
  3772. if ($this->_processCmd($pipeline, $val, $val)) {
  3773. $this->_connection->write('', true);
  3774. } else {
  3775. $cmd_count = 0;
  3776. }
  3777. } catch (Horde_Imap_Client_Exception $e) {
  3778. switch ($e->getCode()) {
  3779. case Horde_Imap_Client_Exception::SERVER_WRITEERROR:
  3780. $this->_temp['logout'] = true;
  3781. $this->logout();
  3782. break;
  3783. }
  3784. throw $e;
  3785. }
  3786. }
  3787. while ($cmd_count) {
  3788. try {
  3789. if ($this->_getLine($pipeline) instanceof Horde_Imap_Client_Interaction_Server_Tagged) {
  3790. --$cmd_count;
  3791. }
  3792. } catch (Horde_Imap_Client_Exception $e) {
  3793. switch ($e->getCode()) {
  3794. case $e::DISCONNECT:
  3795. /* Guaranteed to have no more data incoming, so we can
  3796. * immediately logout. */
  3797. $this->_temp['logout'] = true;
  3798. $this->logout();
  3799. throw $e;
  3800. }
  3801. /* For all other issues, catch and store exception; don't
  3802. * throw until all input is read since we need to clear
  3803. * incoming queue. (For now, only store first exception.) */
  3804. if (is_null($exception)) {
  3805. $exception = $e;
  3806. }
  3807. if (($e instanceof Horde_Imap_Client_Exception_ServerResponse) &&
  3808. $e->command) {
  3809. --$cmd_count;
  3810. }
  3811. }
  3812. }
  3813. if (!is_null($exception)) {
  3814. throw $exception;
  3815. }
  3816. }
  3817. /**
  3818. * Process/send a command to the remote server.
  3819. *
  3820. * @param Horde_Imap_Client_Interaction_Pipeline $pipeline The pipeline
  3821. * object.
  3822. * @param Horde_Imap_Client_Interaction_Command $cmd The master command.
  3823. * @param Horde_Imap_Client_Data_Format_List $data Commands to send.
  3824. *
  3825. * @return boolean True if EOL needed to finish command.
  3826. * @throws Horde_Imap_Client_Exception
  3827. * @throws Horde_Imap_Client_Exception_NoSupport
  3828. */
  3829. protected function _processCmd($pipeline, $cmd, $data)
  3830. {
  3831. if ($this->_debug->debug &&
  3832. ($data instanceof Horde_Imap_Client_Interaction_Command)) {
  3833. $data->startTimer();
  3834. }
  3835. foreach ($data as $key => $val) {
  3836. if ($val instanceof Horde_Imap_Client_Interaction_Command_Continuation) {
  3837. $this->_connection->write('', true);
  3838. /* Check for optional continuation responses when the command
  3839. * has already finished. */
  3840. if (!$cmd_continuation = $this->_processCmdContinuation($pipeline, $val->optional)) {
  3841. return false;
  3842. }
  3843. $this->_processCmd(
  3844. $pipeline,
  3845. $cmd,
  3846. $val->getCommands($cmd_continuation)
  3847. );
  3848. continue;
  3849. }
  3850. if (!is_null($debug_msg = array_shift($cmd->debug))) {
  3851. $this->_debug->client(
  3852. (($cmd == $data) ? $cmd->tag . ' ' : '') . $debug_msg
  3853. );
  3854. $this->_connection->client_debug = false;
  3855. }
  3856. if ($key) {
  3857. $this->_connection->write(' ');
  3858. }
  3859. if ($val instanceof Horde_Imap_Client_Data_Format_List) {
  3860. $this->_connection->write('(');
  3861. $this->_processCmd($pipeline, $cmd, $val);
  3862. $this->_connection->write(')');
  3863. } elseif (($val instanceof Horde_Imap_Client_Data_Format_String) &&
  3864. $val->literal()) {
  3865. $c = $this->_capability();
  3866. /* RFC 6855: If UTF8 extension is available, quote short
  3867. * strings instead of sending as literal. */
  3868. if ($c->isEnabled('UTF8=ACCEPT') && ($val->length() < 100)) {
  3869. $val->forceQuoted();
  3870. $this->_connection->write($val->escape());
  3871. } else {
  3872. /* RFC 3516/4466: Send literal8 if we have binary data. */
  3873. if ($cmd->literal8 &&
  3874. $val->binary() &&
  3875. ($c->query('BINARY') || $c->isEnabled('UTF8=ACCEPT'))) {
  3876. $binary = true;
  3877. $this->_connection->write('~');
  3878. } else {
  3879. $binary = false;
  3880. }
  3881. $literal_len = $val->length();
  3882. $this->_connection->write('{' . $literal_len);
  3883. /* RFC 2088 - If LITERAL+ is available, saves a roundtrip
  3884. * from the server. */
  3885. if ($cmd->literalplus && $c->query('LITERAL+')) {
  3886. $this->_connection->write('+}', true);
  3887. } else {
  3888. $this->_connection->write('}', true);
  3889. $this->_processCmdContinuation($pipeline);
  3890. }
  3891. if ($debug_msg) {
  3892. $this->_connection->client_debug = false;
  3893. }
  3894. $this->_connection->writeLiteral(
  3895. $val->getStream(),
  3896. $literal_len,
  3897. $binary
  3898. );
  3899. }
  3900. } else {
  3901. $this->_connection->write($val->escape());
  3902. }
  3903. }
  3904. return true;
  3905. }
  3906. /**
  3907. * Process a command continuation response.
  3908. *
  3909. * @param Horde_Imap_Client_Interaction_Pipeline $pipeline The pipeline
  3910. * object.
  3911. * @param boolean $noexception Don't throw
  3912. * exception if
  3913. * continuation
  3914. * does not occur.
  3915. *
  3916. * @return mixed A Horde_Imap_Client_Interaction_Server_Continuation
  3917. * object or false.
  3918. *
  3919. * @throws Horde_Imap_Client_Exception
  3920. */
  3921. protected function _processCmdContinuation($pipeline, $noexception = false)
  3922. {
  3923. do {
  3924. $ob = $this->_getLine($pipeline);
  3925. } while ($ob instanceof Horde_Imap_Client_Interaction_Server_Untagged);
  3926. if ($ob instanceof Horde_Imap_Client_Interaction_Server_Continuation) {
  3927. return $ob;
  3928. } elseif ($noexception) {
  3929. return false;
  3930. }
  3931. $this->_debug->info(
  3932. 'ERROR: Unexpected response from server while waiting for a continuation request.'
  3933. );
  3934. $e = new Horde_Imap_Client_Exception(
  3935. Horde_Imap_Client_Translation::r("Error when communicating with the mail server."),
  3936. Horde_Imap_Client_Exception::SERVER_READERROR
  3937. );
  3938. $e->details = strval($ob);
  3939. throw $e;
  3940. }
  3941. /**
  3942. * Shortcut to creating a new IMAP client command object.
  3943. *
  3944. * @param string $cmd The IMAP command.
  3945. *
  3946. * @return Horde_Imap_Client_Interaction_Command A command object.
  3947. */
  3948. protected function _command($cmd)
  3949. {
  3950. return new Horde_Imap_Client_Interaction_Command($cmd, ++$this->_tag);
  3951. }
  3952. /**
  3953. * Shortcut to creating a new pipeline object.
  3954. *
  3955. * @param Horde_Imap_Client_Interaction_Command $cmd An IMAP command to
  3956. * add.
  3957. *
  3958. * @return Horde_Imap_Client_Interaction_Pipeline A pipeline object.
  3959. */
  3960. protected function _pipeline($cmd = null)
  3961. {
  3962. if (!isset($this->_temp['fetchob'])) {
  3963. $this->_temp['fetchob'] = new Horde_Imap_Client_Fetch_Results(
  3964. $this->_fetchDataClass,
  3965. Horde_Imap_Client_Fetch_Results::SEQUENCE
  3966. );
  3967. }
  3968. $ob = new Horde_Imap_Client_Interaction_Pipeline(
  3969. clone $this->_temp['fetchob']
  3970. );
  3971. if (!is_null($cmd)) {
  3972. $ob->add($cmd);
  3973. }
  3974. return $ob;
  3975. }
  3976. /**
  3977. * Gets data from the IMAP server stream and parses it.
  3978. *
  3979. * @param Horde_Imap_Client_Interaction_Pipeline $pipeline Pipeline
  3980. * object.
  3981. *
  3982. * @return Horde_Imap_Client_Interaction_Server Server object.
  3983. *
  3984. * @throws Horde_Imap_Client_Exception
  3985. */
  3986. protected function _getLine(
  3987. Horde_Imap_Client_Interaction_Pipeline $pipeline
  3988. )
  3989. {
  3990. $server = Horde_Imap_Client_Interaction_Server::create(
  3991. $this->_connection->read()
  3992. );
  3993. switch (get_class($server)) {
  3994. case 'Horde_Imap_Client_Interaction_Server_Continuation':
  3995. $this->_responseCode($pipeline, $server);
  3996. break;
  3997. case 'Horde_Imap_Client_Interaction_Server_Tagged':
  3998. $cmd = $pipeline->complete($server);
  3999. if (is_null($cmd)) {
  4000. /* This indicates a "dangling" tagged response - it was either
  4001. * generated by an aborted previous pipeline object or is the
  4002. * result of spurious output by the server. Ignore. */
  4003. return $this->_getLine($pipeline);
  4004. }
  4005. if ($timer = $cmd->getTimer()) {
  4006. $this->_debug->info(sprintf(
  4007. 'Command %s took %s seconds.',
  4008. $cmd->tag,
  4009. $timer
  4010. ));
  4011. }
  4012. $this->_responseCode($pipeline, $server);
  4013. if (is_callable($cmd->on_success)) {
  4014. call_user_func($cmd->on_success);
  4015. }
  4016. break;
  4017. case 'Horde_Imap_Client_Interaction_Server_Untagged':
  4018. if (is_null($server->status)) {
  4019. $this->_serverResponse($pipeline, $server);
  4020. } else {
  4021. $this->_responseCode($pipeline, $server);
  4022. }
  4023. break;
  4024. }
  4025. switch ($server->status) {
  4026. case $server::BAD:
  4027. case $server::NO:
  4028. /* A tagged BAD response indicates that the tagged command caused
  4029. * the error. This information is unknown if untagged (RFC 3501
  4030. * [7.1.3]) - ignore these untagged responses.
  4031. * An untagged NO response indicates a warning; ignore and assume
  4032. * that it also included response text code that is handled
  4033. * elsewhere. Throw exception if tagged; command handlers can
  4034. * catch this if able to workaround this issue (RFC 3501
  4035. * [7.1.2]). */
  4036. if ($server instanceof Horde_Imap_Client_Interaction_Server_Tagged) {
  4037. /* Check for a on_error callback. If function returns true,
  4038. * ignore the error. */
  4039. if (($cmd = $pipeline->getCmd($server->tag)) &&
  4040. is_callable($cmd->on_error) &&
  4041. call_user_func($cmd->on_error)) {
  4042. break;
  4043. }
  4044. throw new Horde_Imap_Client_Exception_ServerResponse(
  4045. Horde_Imap_Client_Translation::r("IMAP error reported by server."),
  4046. 0,
  4047. $server,
  4048. $pipeline
  4049. );
  4050. }
  4051. break;
  4052. case $server::BYE:
  4053. /* A BYE response received as part of a logout command should be
  4054. * be treated like a regular command: a client MUST process the
  4055. * entire command until logging out (RFC 3501 [3.4; 7.1.5]). */
  4056. if (empty($this->_temp['logout'])) {
  4057. $e = new Horde_Imap_Client_Exception(
  4058. Horde_Imap_Client_Translation::r("IMAP Server closed the connection."),
  4059. Horde_Imap_Client_Exception::DISCONNECT
  4060. );
  4061. $e->details = strval($server);
  4062. throw $e;
  4063. }
  4064. break;
  4065. case $server::PREAUTH:
  4066. /* The user was pre-authenticated. (RFC 3501 [7.1.4]) */
  4067. $this->_temp['preauth'] = true;
  4068. break;
  4069. }
  4070. return $server;
  4071. }
  4072. /**
  4073. * Handle untagged server responses (see RFC 3501 [2.2.2]).
  4074. *
  4075. * @param Horde_Imap_Client_Interaction_Pipeline $pipeline Pipeline
  4076. * object.
  4077. * @param Horde_Imap_Client_Interaction_Server $ob Server
  4078. * response.
  4079. */
  4080. protected function _serverResponse(
  4081. Horde_Imap_Client_Interaction_Pipeline $pipeline,
  4082. Horde_Imap_Client_Interaction_Server $ob
  4083. )
  4084. {
  4085. $token = $ob->token;
  4086. /* First, catch untagged responses where the name appears first on the
  4087. * line. */
  4088. switch ($first = Horde_String::upper($token->current())) {
  4089. case 'CAPABILITY':
  4090. $this->_parseCapability($pipeline, $token->flushIterator());
  4091. break;
  4092. case 'LIST':
  4093. case 'LSUB':
  4094. $this->_parseList($pipeline, $token);
  4095. break;
  4096. case 'STATUS':
  4097. // Parse a STATUS response (RFC 3501 [7.2.4]).
  4098. $this->_parseStatus($token);
  4099. break;
  4100. case 'SEARCH':
  4101. case 'SORT':
  4102. // Parse a SEARCH/SORT response (RFC 3501 [7.2.5] & RFC 5256 [4]).
  4103. $this->_parseSearch($pipeline, $token->flushIterator());
  4104. break;
  4105. case 'ESEARCH':
  4106. // Parse an ESEARCH response (RFC 4466 [2.6.2]).
  4107. $this->_parseEsearch($pipeline, $token);
  4108. break;
  4109. case 'FLAGS':
  4110. $token->next();
  4111. $this->_mailboxOb()->setStatus(Horde_Imap_Client::STATUS_FLAGS, array_map('Horde_String::lower', $token->flushIterator()));
  4112. break;
  4113. case 'QUOTA':
  4114. $this->_parseQuota($pipeline, $token);
  4115. break;
  4116. case 'QUOTAROOT':
  4117. // Ignore this line - we can get this information from
  4118. // the untagged QUOTA responses.
  4119. break;
  4120. case 'NAMESPACE':
  4121. $this->_parseNamespace($pipeline, $token);
  4122. break;
  4123. case 'THREAD':
  4124. $this->_parseThread($pipeline, $token);
  4125. break;
  4126. case 'ACL':
  4127. $this->_parseACL($pipeline, $token);
  4128. break;
  4129. case 'LISTRIGHTS':
  4130. $this->_parseListRights($pipeline, $token);
  4131. break;
  4132. case 'MYRIGHTS':
  4133. $this->_parseMyRights($pipeline, $token);
  4134. break;
  4135. case 'ID':
  4136. // ID extension (RFC 2971)
  4137. $this->_parseID($pipeline, $token);
  4138. break;
  4139. case 'ENABLED':
  4140. // ENABLE extension (RFC 5161)
  4141. $this->_parseEnabled($token);
  4142. break;
  4143. case 'LANGUAGE':
  4144. // LANGUAGE extension (RFC 5255 [3.2])
  4145. $this->_parseLanguage($token);
  4146. break;
  4147. case 'COMPARATOR':
  4148. // I18NLEVEL=2 extension (RFC 5255 [4.7])
  4149. $this->_parseComparator($pipeline, $token);
  4150. break;
  4151. case 'VANISHED':
  4152. // QRESYNC extension (RFC 7162 [3.2.10])
  4153. $this->_parseVanished($pipeline, $token);
  4154. break;
  4155. case 'ANNOTATION':
  4156. // Parse an ANNOTATION response.
  4157. $this->_parseAnnotation($pipeline, $token);
  4158. break;
  4159. case 'METADATA':
  4160. // Parse a METADATA response.
  4161. $this->_parseMetadata($pipeline, $token);
  4162. break;
  4163. default:
  4164. // Next, look for responses where the keywords occur second.
  4165. switch (Horde_String::upper($token->next())) {
  4166. case 'EXISTS':
  4167. // EXISTS response - RFC 3501 [7.3.2]
  4168. $mbox_ob = $this->_mailboxOb();
  4169. // Increment UIDNEXT if it is set.
  4170. if ($mbox_ob->open &&
  4171. ($uidnext = $mbox_ob->getStatus(Horde_Imap_Client::STATUS_UIDNEXT))) {
  4172. $mbox_ob->setStatus(Horde_Imap_Client::STATUS_UIDNEXT, $uidnext + $first - $mbox_ob->getStatus(Horde_Imap_Client::STATUS_MESSAGES));
  4173. }
  4174. $mbox_ob->setStatus(Horde_Imap_Client::STATUS_MESSAGES, $first);
  4175. break;
  4176. case 'RECENT':
  4177. // RECENT response - RFC 3501 [7.3.1]
  4178. $this->_mailboxOb()->setStatus(Horde_Imap_Client::STATUS_RECENT, $first);
  4179. break;
  4180. case 'EXPUNGE':
  4181. // EXPUNGE response - RFC 3501 [7.4.1]
  4182. $this->_deleteMsgs($this->_selected, $this->getIdsOb($first, true), array(
  4183. 'decrement' => true,
  4184. 'pipeline' => $pipeline
  4185. ));
  4186. $pipeline->data['expunge_seen'] = true;
  4187. break;
  4188. case 'FETCH':
  4189. // FETCH response - RFC 3501 [7.4.2]
  4190. $this->_parseFetch($pipeline, $first, $token);
  4191. break;
  4192. }
  4193. break;
  4194. }
  4195. }
  4196. /**
  4197. * Handle status responses (see RFC 3501 [7.1]).
  4198. *
  4199. * @param Horde_Imap_Client_Interaction_Pipeline $pipeline Pipeline
  4200. * object.
  4201. * @param Horde_Imap_Client_Interaction_Server $ob Server object.
  4202. *
  4203. * @throws Horde_Imap_Client_Exception_ServerResponse
  4204. */
  4205. protected function _responseCode(
  4206. Horde_Imap_Client_Interaction_Pipeline $pipeline,
  4207. Horde_Imap_Client_Interaction_Server $ob
  4208. )
  4209. {
  4210. if (is_null($ob->responseCode)) {
  4211. return;
  4212. }
  4213. $rc = $ob->responseCode;
  4214. switch ($rc->code) {
  4215. case 'ALERT':
  4216. // Defined by RFC 5530 [3] - Treat as an alert for now.
  4217. case 'CONTACTADMIN':
  4218. // Used by Gmail - Treat as an alert for now.
  4219. // http://mailman13.u.washington.edu/pipermail/imap-protocol/2014-September/002324.html
  4220. case 'WEBALERT':
  4221. $this->_alerts->add(strval($ob->token), $rc->code);
  4222. break;
  4223. case 'BADCHARSET':
  4224. /* Store valid search charsets if returned by server. */
  4225. $s = $this->search_charset;
  4226. foreach ($rc->data[0] as $val) {
  4227. $s->setValid($val, true);
  4228. }
  4229. throw new Horde_Imap_Client_Exception_ServerResponse(
  4230. Horde_Imap_Client_Translation::r("Charset used in search query is not supported on the mail server."),
  4231. Horde_Imap_Client_Exception::BADCHARSET,
  4232. $ob,
  4233. $pipeline
  4234. );
  4235. case 'CAPABILITY':
  4236. $this->_parseCapability($pipeline, $rc->data);
  4237. break;
  4238. case 'PARSE':
  4239. /* Only throw error on NO/BAD. Message is human readable. */
  4240. switch ($ob->status) {
  4241. case Horde_Imap_Client_Interaction_Server::BAD:
  4242. case Horde_Imap_Client_Interaction_Server::NO:
  4243. $e = new Horde_Imap_Client_Exception_ServerResponse(
  4244. Horde_Imap_Client_Translation::r("The mail server was unable to parse the contents of the mail message: %s"),
  4245. Horde_Imap_Client_Exception::PARSEERROR,
  4246. $ob,
  4247. $pipeline
  4248. );
  4249. $e->messagePrintf(array(strval($ob->token)));
  4250. throw $e;
  4251. }
  4252. break;
  4253. case 'READ-ONLY':
  4254. $this->_mode = Horde_Imap_Client::OPEN_READONLY;
  4255. break;
  4256. case 'READ-WRITE':
  4257. $this->_mode = Horde_Imap_Client::OPEN_READWRITE;
  4258. break;
  4259. case 'TRYCREATE':
  4260. // RFC 3501 [7.1]
  4261. $pipeline->data['trycreate'] = true;
  4262. break;
  4263. case 'PERMANENTFLAGS':
  4264. $this->_mailboxOb()->setStatus(Horde_Imap_Client::STATUS_PERMFLAGS, array_map('Horde_String::lower', $rc->data[0]));
  4265. break;
  4266. case 'UIDNEXT':
  4267. $this->_mailboxOb()->setStatus(Horde_Imap_Client::STATUS_UIDNEXT, $rc->data[0]);
  4268. break;
  4269. case 'UIDVALIDITY':
  4270. $this->_mailboxOb()->setStatus(Horde_Imap_Client::STATUS_UIDVALIDITY, $rc->data[0]);
  4271. break;
  4272. case 'UNSEEN':
  4273. /* This is different from the STATUS UNSEEN response - this item,
  4274. * if defined, returns the first UNSEEN message in the mailbox. */
  4275. $this->_mailboxOb()->setStatus(Horde_Imap_Client::STATUS_FIRSTUNSEEN, $rc->data[0]);
  4276. break;
  4277. case 'REFERRAL':
  4278. // Defined by RFC 2221
  4279. $pipeline->data['referral'] = new Horde_Imap_Client_Url_Imap($rc->data[0]);
  4280. break;
  4281. case 'UNKNOWN-CTE':
  4282. // Defined by RFC 3516
  4283. throw new Horde_Imap_Client_Exception_ServerResponse(
  4284. Horde_Imap_Client_Translation::r("The mail server was unable to parse the contents of the mail message."),
  4285. Horde_Imap_Client_Exception::UNKNOWNCTE,
  4286. $ob,
  4287. $pipeline
  4288. );
  4289. case 'APPENDUID':
  4290. // Defined by RFC 4315
  4291. // APPENDUID: [0] = UIDVALIDITY, [1] = UID(s)
  4292. $pipeline->data['appenduid'] = $this->getIdsOb($rc->data[1]);
  4293. break;
  4294. case 'COPYUID':
  4295. // Defined by RFC 4315
  4296. // COPYUID: [0] = UIDVALIDITY, [1] = UIDFROM, [2] = UIDTO
  4297. $pipeline->data['copyuid'] = array_combine(
  4298. $this->getIdsOb($rc->data[1])->ids,
  4299. $this->getIdsOb($rc->data[2])->ids
  4300. );
  4301. /* Use UIDPLUS information to move cached data to new mailbox (see
  4302. * RFC 4549 [4.2.2.1]). Need to move now, because a MOVE might
  4303. * EXPUNGE immediately afterwards. */
  4304. $this->_moveCache($pipeline->data['copydest'], $pipeline->data['copyuid'], $rc->data[0]);
  4305. break;
  4306. case 'UIDNOTSTICKY':
  4307. // Defined by RFC 4315 [3]
  4308. $this->_mailboxOb()->setStatus(Horde_Imap_Client::STATUS_UIDNOTSTICKY, true);
  4309. break;
  4310. case 'BADURL':
  4311. // Defined by RFC 4469 [4.1]
  4312. throw new Horde_Imap_Client_Exception_ServerResponse(
  4313. Horde_Imap_Client_Translation::r("Could not save message on server."),
  4314. Horde_Imap_Client_Exception::CATENATE_BADURL,
  4315. $ob,
  4316. $pipeline
  4317. );
  4318. case 'TOOBIG':
  4319. // Defined by RFC 4469 [4.2]
  4320. throw new Horde_Imap_Client_Exception_ServerResponse(
  4321. Horde_Imap_Client_Translation::r("Could not save message data because it is too large."),
  4322. Horde_Imap_Client_Exception::CATENATE_TOOBIG,
  4323. $ob,
  4324. $pipeline
  4325. );
  4326. case 'HIGHESTMODSEQ':
  4327. // Defined by RFC 7162 [3.1.2.1]
  4328. $pipeline->data['modseqs'][] = $rc->data[0];
  4329. break;
  4330. case 'NOMODSEQ':
  4331. // Defined by RFC 7162 [3.1.2.2]
  4332. $pipeline->data['modseqs'][] = 0;
  4333. break;
  4334. case 'MODIFIED':
  4335. // Defined by RFC 7162 [3.1.3]
  4336. $pipeline->data['modified']->add($rc->data[0]);
  4337. break;
  4338. case 'CLOSED':
  4339. // Defined by RFC 7162 [3.2.11]
  4340. if (isset($pipeline->data['qresyncmbox'])) {
  4341. /* If there is any pending FETCH cache entries, flush them
  4342. * now before changing mailboxes. */
  4343. $this->_updateCache($pipeline->fetch);
  4344. $pipeline->fetch->clear();
  4345. $this->_changeSelected(
  4346. $pipeline->data['qresyncmbox'][0],
  4347. $pipeline->data['qresyncmbox'][1]
  4348. );
  4349. unset($pipeline->data['qresyncmbox']);
  4350. }
  4351. break;
  4352. case 'NOTSAVED':
  4353. // Defined by RFC 5182 [2.5]
  4354. $pipeline->data['searchnotsaved'] = true;
  4355. break;
  4356. case 'BADCOMPARATOR':
  4357. // Defined by RFC 5255 [4.9]
  4358. throw new Horde_Imap_Client_Exception_ServerResponse(
  4359. Horde_Imap_Client_Translation::r("The comparison algorithm was not recognized by the server."),
  4360. Horde_Imap_Client_Exception::BADCOMPARATOR,
  4361. $ob,
  4362. $pipeline
  4363. );
  4364. case 'METADATA':
  4365. $md = $rc->data[0];
  4366. switch ($md[0]) {
  4367. case 'LONGENTRIES':
  4368. // Defined by RFC 5464 [4.2.1]
  4369. $pipeline->data['metadata']['*longentries'] = intval($md[1]);
  4370. break;
  4371. case 'MAXSIZE':
  4372. // Defined by RFC 5464 [4.3]
  4373. throw new Horde_Imap_Client_Exception_ServerResponse(
  4374. Horde_Imap_Client_Translation::r("The metadata item could not be saved because it is too large."),
  4375. Horde_Imap_Client_Exception::METADATA_MAXSIZE,
  4376. $ob,
  4377. $pipeline
  4378. );
  4379. case 'NOPRIVATE':
  4380. // Defined by RFC 5464 [4.3]
  4381. throw new Horde_Imap_Client_Exception_ServerResponse(
  4382. Horde_Imap_Client_Translation::r("The metadata item could not be saved because the server does not support private annotations."),
  4383. Horde_Imap_Client_Exception::METADATA_NOPRIVATE,
  4384. $ob,
  4385. $pipeline
  4386. );
  4387. case 'TOOMANY':
  4388. // Defined by RFC 5464 [4.3]
  4389. throw new Horde_Imap_Client_Exception_ServerResponse(
  4390. Horde_Imap_Client_Translation::r("The metadata item could not be saved because the maximum number of annotations has been exceeded."),
  4391. Horde_Imap_Client_Exception::METADATA_TOOMANY,
  4392. $ob,
  4393. $pipeline
  4394. );
  4395. }
  4396. break;
  4397. case 'UNAVAILABLE':
  4398. // Defined by RFC 5530 [3]
  4399. $pipeline->data['loginerr'] = new Horde_Imap_Client_Exception(
  4400. Horde_Imap_Client_Translation::r("Remote server is temporarily unavailable."),
  4401. Horde_Imap_Client_Exception::LOGIN_UNAVAILABLE
  4402. );
  4403. break;
  4404. case 'AUTHENTICATIONFAILED':
  4405. // Defined by RFC 5530 [3]
  4406. $pipeline->data['loginerr'] = new Horde_Imap_Client_Exception(
  4407. Horde_Imap_Client_Translation::r("Authentication failed."),
  4408. Horde_Imap_Client_Exception::LOGIN_AUTHENTICATIONFAILED
  4409. );
  4410. break;
  4411. case 'AUTHORIZATIONFAILED':
  4412. // Defined by RFC 5530 [3]
  4413. $pipeline->data['loginerr'] = new Horde_Imap_Client_Exception(
  4414. Horde_Imap_Client_Translation::r("Authentication was successful, but authorization failed."),
  4415. Horde_Imap_Client_Exception::LOGIN_AUTHORIZATIONFAILED
  4416. );
  4417. break;
  4418. case 'EXPIRED':
  4419. // Defined by RFC 5530 [3]
  4420. $pipeline->data['loginerr'] = new Horde_Imap_Client_Exception(
  4421. Horde_Imap_Client_Translation::r("Authentication credentials have expired."),
  4422. Horde_Imap_Client_Exception::LOGIN_EXPIRED
  4423. );
  4424. break;
  4425. case 'PRIVACYREQUIRED':
  4426. // Defined by RFC 5530 [3]
  4427. $pipeline->data['loginerr'] = new Horde_Imap_Client_Exception(
  4428. Horde_Imap_Client_Translation::r("Operation failed due to a lack of a secure connection."),
  4429. Horde_Imap_Client_Exception::LOGIN_PRIVACYREQUIRED
  4430. );
  4431. break;
  4432. case 'NOPERM':
  4433. // Defined by RFC 5530 [3]
  4434. throw new Horde_Imap_Client_Exception_ServerResponse(
  4435. Horde_Imap_Client_Translation::r("You do not have adequate permissions to carry out this operation."),
  4436. Horde_Imap_Client_Exception::NOPERM,
  4437. $ob,
  4438. $pipeline
  4439. );
  4440. case 'INUSE':
  4441. // Defined by RFC 5530 [3]
  4442. throw new Horde_Imap_Client_Exception_ServerResponse(
  4443. Horde_Imap_Client_Translation::r("There was a temporary issue when attempting this operation. Please try again later."),
  4444. Horde_Imap_Client_Exception::INUSE,
  4445. $ob,
  4446. $pipeline
  4447. );
  4448. case 'EXPUNGEISSUED':
  4449. // Defined by RFC 5530 [3]
  4450. $pipeline->data['expungeissued'] = true;
  4451. break;
  4452. case 'CORRUPTION':
  4453. // Defined by RFC 5530 [3]
  4454. throw new Horde_Imap_Client_Exception_ServerResponse(
  4455. Horde_Imap_Client_Translation::r("The mail server is reporting corrupt data in your mailbox."),
  4456. Horde_Imap_Client_Exception::CORRUPTION,
  4457. $ob,
  4458. $pipeline
  4459. );
  4460. case 'SERVERBUG':
  4461. case 'CLIENTBUG':
  4462. case 'CANNOT':
  4463. // Defined by RFC 5530 [3]
  4464. $this->_debug->info(
  4465. 'ERROR: mail server explicitly reporting an error.'
  4466. );
  4467. break;
  4468. case 'LIMIT':
  4469. // Defined by RFC 5530 [3]
  4470. throw new Horde_Imap_Client_Exception_ServerResponse(
  4471. Horde_Imap_Client_Translation::r("The mail server has denied the request."),
  4472. Horde_Imap_Client_Exception::LIMIT,
  4473. $ob,
  4474. $pipeline
  4475. );
  4476. case 'OVERQUOTA':
  4477. // Defined by RFC 5530 [3]
  4478. throw new Horde_Imap_Client_Exception_ServerResponse(
  4479. Horde_Imap_Client_Translation::r("The operation failed because the quota has been exceeded on the mail server."),
  4480. Horde_Imap_Client_Exception::OVERQUOTA,
  4481. $ob,
  4482. $pipeline
  4483. );
  4484. case 'ALREADYEXISTS':
  4485. // Defined by RFC 5530 [3]
  4486. throw new Horde_Imap_Client_Exception_ServerResponse(
  4487. Horde_Imap_Client_Translation::r("The object could not be created because it already exists."),
  4488. Horde_Imap_Client_Exception::ALREADYEXISTS,
  4489. $ob,
  4490. $pipeline
  4491. );
  4492. case 'NONEXISTENT':
  4493. // Defined by RFC 5530 [3]
  4494. throw new Horde_Imap_Client_Exception_ServerResponse(
  4495. Horde_Imap_Client_Translation::r("The object could not be deleted because it does not exist."),
  4496. Horde_Imap_Client_Exception::NONEXISTENT,
  4497. $ob,
  4498. $pipeline
  4499. );
  4500. case 'USEATTR':
  4501. // Defined by RFC 6154 [3]
  4502. throw new Horde_Imap_Client_Exception_ServerResponse(
  4503. Horde_Imap_Client_Translation::r("The special-use attribute requested for the mailbox is not supported."),
  4504. Horde_Imap_Client_Exception::USEATTR,
  4505. $ob,
  4506. $pipeline
  4507. );
  4508. case 'DOWNGRADED':
  4509. // Defined by RFC 6858 [3]
  4510. $downgraded = $this->getIdsOb($rc->data[0]);
  4511. foreach ($pipeline->fetch as $val) {
  4512. if (in_array($val->getUid(), $downgraded)) {
  4513. $val->setDowngraded(true);
  4514. }
  4515. }
  4516. break;
  4517. case 'XPROXYREUSE':
  4518. // The proxy connection was reused, so no need to do login tasks.
  4519. $pipeline->data['proxyreuse'] = true;
  4520. break;
  4521. default:
  4522. // Unknown response codes SHOULD be ignored - RFC 3501 [7.1]
  4523. break;
  4524. }
  4525. }
  4526. }