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

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

http://github.com/moodle/moodle
PHP | 5169 lines | 3392 code | 640 blank | 1137 comment | 471 complexity | cac63893e689657c347c9f55d02fb112 MD5 | raw file
Possible License(s): MIT, AGPL-3.0, MPL-2.0-no-copyleft-exception, LGPL-3.0, GPL-3.0, Apache-2.0, LGPL-2.1, BSD-3-Clause

Large files files are truncated, but you can click here to view the full file

  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. $selec

Large files files are truncated, but you can click here to view the full file