PageRenderTime 63ms CodeModel.GetById 20ms RepoModel.GetById 1ms app.codeStats 0ms

/mail/program/include/rcube_imap_generic.php

https://bitbucket.org/alisher/itworks
PHP | 3747 lines | 2488 code | 477 blank | 782 comment | 676 complexity | 14fde4e9009387ed7dd32dc89533a2bf MD5 | raw file
Possible License(s): GPL-3.0, LGPL-2.1

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

  1. <?php
  2. /**
  3. +-----------------------------------------------------------------------+
  4. | program/include/rcube_imap_generic.php |
  5. | |
  6. | This file is part of the Roundcube Webmail client |
  7. | Copyright (C) 2005-2010, The Roundcube Dev Team |
  8. | Copyright (C) 2011, Kolab Systems AG |
  9. | |
  10. | Licensed under the GNU General Public License version 3 or |
  11. | any later version with exceptions for skins & plugins. |
  12. | See the README file for a full license statement. |
  13. | |
  14. | PURPOSE: |
  15. | Provide alternative IMAP library that doesn't rely on the standard |
  16. | C-Client based version. This allows to function regardless |
  17. | of whether or not the PHP build it's running on has IMAP |
  18. | functionality built-in. |
  19. | |
  20. | Based on Iloha IMAP Library. See http://ilohamail.org/ for details |
  21. | |
  22. +-----------------------------------------------------------------------+
  23. | Author: Aleksander Machniak <alec@alec.pl> |
  24. | Author: Ryo Chijiiwa <Ryo@IlohaMail.org> |
  25. +-----------------------------------------------------------------------+
  26. $Id$
  27. */
  28. /**
  29. * Struct representing an e-mail message header
  30. *
  31. * @package Mail
  32. * @author Aleksander Machniak <alec@alec.pl>
  33. */
  34. class rcube_mail_header
  35. {
  36. public $id;
  37. public $uid;
  38. public $subject;
  39. public $from;
  40. public $to;
  41. public $cc;
  42. public $replyto;
  43. public $in_reply_to;
  44. public $date;
  45. public $messageID;
  46. public $size;
  47. public $encoding;
  48. public $charset;
  49. public $ctype;
  50. public $timestamp;
  51. public $bodystructure;
  52. public $internaldate;
  53. public $references;
  54. public $priority;
  55. public $mdn_to;
  56. public $others = array();
  57. public $flags = array();
  58. // map header to rcube_mail_header object property
  59. private $obj_headers = array(
  60. 'date' => 'date',
  61. 'from' => 'from',
  62. 'to' => 'to',
  63. 'subject' => 'subject',
  64. 'reply-to' => 'replyto',
  65. 'cc' => 'cc',
  66. 'bcc' => 'bcc',
  67. 'content-transfer-encoding' => 'encoding',
  68. 'in-reply-to' => 'in_reply_to',
  69. 'content-type' => 'ctype',
  70. 'references' => 'references',
  71. 'return-receipt-to' => 'mdn_to',
  72. 'disposition-notification-to' => 'mdn_to',
  73. 'x-confirm-reading-to' => 'mdn_to',
  74. 'message-id' => 'messageID',
  75. 'x-priority' => 'priority',
  76. );
  77. /**
  78. * Returns header value
  79. */
  80. public function get($name, $decode = true)
  81. {
  82. $name = strtolower($name);
  83. if (isset($this->obj_headers[$name])) {
  84. $value = $this->{$this->obj_headers[$name]};
  85. }
  86. else {
  87. $value = $this->others[$name];
  88. }
  89. return $decode ? rcube_mime::decode_header($value, $this->charset) : $value;
  90. }
  91. /**
  92. * Sets header value
  93. */
  94. public function set($name, $value)
  95. {
  96. $name = strtolower($name);
  97. if (isset($this->obj_headers[$name])) {
  98. $this->{$this->obj_headers[$name]} = $value;
  99. }
  100. else {
  101. $this->others[$name] = $value;
  102. }
  103. }
  104. }
  105. // For backward compatibility with cached messages (#1486602)
  106. class iilBasicHeader extends rcube_mail_header
  107. {
  108. }
  109. // Support objects created in git-master (0.9)
  110. class rcube_message_header extends rcube_mail_header
  111. {
  112. }
  113. /**
  114. * PHP based wrapper class to connect to an IMAP server
  115. *
  116. * @package Mail
  117. * @author Aleksander Machniak <alec@alec.pl>
  118. */
  119. class rcube_imap_generic
  120. {
  121. public $error;
  122. public $errornum;
  123. public $result;
  124. public $resultcode;
  125. public $selected;
  126. public $data = array();
  127. public $flags = array(
  128. 'SEEN' => '\\Seen',
  129. 'DELETED' => '\\Deleted',
  130. 'ANSWERED' => '\\Answered',
  131. 'DRAFT' => '\\Draft',
  132. 'FLAGGED' => '\\Flagged',
  133. 'FORWARDED' => '$Forwarded',
  134. 'MDNSENT' => '$MDNSent',
  135. '*' => '\\*',
  136. );
  137. private $fp;
  138. private $host;
  139. private $logged = false;
  140. private $capability = array();
  141. private $capability_readed = false;
  142. private $prefs;
  143. private $cmd_tag;
  144. private $cmd_num = 0;
  145. private $resourceid;
  146. private $_debug = false;
  147. private $_debug_handler = false;
  148. const ERROR_OK = 0;
  149. const ERROR_NO = -1;
  150. const ERROR_BAD = -2;
  151. const ERROR_BYE = -3;
  152. const ERROR_UNKNOWN = -4;
  153. const ERROR_COMMAND = -5;
  154. const ERROR_READONLY = -6;
  155. const COMMAND_NORESPONSE = 1;
  156. const COMMAND_CAPABILITY = 2;
  157. const COMMAND_LASTLINE = 4;
  158. /**
  159. * Object constructor
  160. */
  161. function __construct()
  162. {
  163. }
  164. /**
  165. * Send simple (one line) command to the connection stream
  166. *
  167. * @param string $string Command string
  168. * @param bool $endln True if CRLF need to be added at the end of command
  169. *
  170. * @param int Number of bytes sent, False on error
  171. */
  172. function putLine($string, $endln=true)
  173. {
  174. if (!$this->fp)
  175. return false;
  176. if ($this->_debug) {
  177. $this->debug('C: '. rtrim($string));
  178. }
  179. $res = fwrite($this->fp, $string . ($endln ? "\r\n" : ''));
  180. if ($res === false) {
  181. @fclose($this->fp);
  182. $this->fp = null;
  183. }
  184. return $res;
  185. }
  186. /**
  187. * Send command to the connection stream with Command Continuation
  188. * Requests (RFC3501 7.5) and LITERAL+ (RFC2088) support
  189. *
  190. * @param string $string Command string
  191. * @param bool $endln True if CRLF need to be added at the end of command
  192. *
  193. * @return int|bool Number of bytes sent, False on error
  194. */
  195. function putLineC($string, $endln=true)
  196. {
  197. if (!$this->fp) {
  198. return false;
  199. }
  200. if ($endln) {
  201. $string .= "\r\n";
  202. }
  203. $res = 0;
  204. if ($parts = preg_split('/(\{[0-9]+\}\r\n)/m', $string, -1, PREG_SPLIT_DELIM_CAPTURE)) {
  205. for ($i=0, $cnt=count($parts); $i<$cnt; $i++) {
  206. if (preg_match('/^\{([0-9]+)\}\r\n$/', $parts[$i+1], $matches)) {
  207. // LITERAL+ support
  208. if ($this->prefs['literal+']) {
  209. $parts[$i+1] = sprintf("{%d+}\r\n", $matches[1]);
  210. }
  211. $bytes = $this->putLine($parts[$i].$parts[$i+1], false);
  212. if ($bytes === false)
  213. return false;
  214. $res += $bytes;
  215. // don't wait if server supports LITERAL+ capability
  216. if (!$this->prefs['literal+']) {
  217. $line = $this->readLine(1000);
  218. // handle error in command
  219. if ($line[0] != '+')
  220. return false;
  221. }
  222. $i++;
  223. }
  224. else {
  225. $bytes = $this->putLine($parts[$i], false);
  226. if ($bytes === false)
  227. return false;
  228. $res += $bytes;
  229. }
  230. }
  231. }
  232. return $res;
  233. }
  234. /**
  235. * Reads line from the connection stream
  236. *
  237. * @param int $size Buffer size
  238. *
  239. * @return string Line of text response
  240. */
  241. function readLine($size=1024)
  242. {
  243. $line = '';
  244. if (!$size) {
  245. $size = 1024;
  246. }
  247. do {
  248. if ($this->eof()) {
  249. return $line ? $line : NULL;
  250. }
  251. $buffer = fgets($this->fp, $size);
  252. if ($buffer === false) {
  253. $this->closeSocket();
  254. break;
  255. }
  256. if ($this->_debug) {
  257. $this->debug('S: '. rtrim($buffer));
  258. }
  259. $line .= $buffer;
  260. } while (substr($buffer, -1) != "\n");
  261. return $line;
  262. }
  263. /**
  264. * Reads more data from the connection stream when provided
  265. * data contain string literal
  266. *
  267. * @param string $line Response text
  268. * @param bool $escape Enables escaping
  269. *
  270. * @return string Line of text response
  271. */
  272. function multLine($line, $escape = false)
  273. {
  274. $line = rtrim($line);
  275. if (preg_match('/\{([0-9]+)\}$/', $line, $m)) {
  276. $out = '';
  277. $str = substr($line, 0, -strlen($m[0]));
  278. $bytes = $m[1];
  279. while (strlen($out) < $bytes) {
  280. $line = $this->readBytes($bytes);
  281. if ($line === NULL)
  282. break;
  283. $out .= $line;
  284. }
  285. $line = $str . ($escape ? $this->escape($out) : $out);
  286. }
  287. return $line;
  288. }
  289. /**
  290. * Reads specified number of bytes from the connection stream
  291. *
  292. * @param int $bytes Number of bytes to get
  293. *
  294. * @return string Response text
  295. */
  296. function readBytes($bytes)
  297. {
  298. $data = '';
  299. $len = 0;
  300. while ($len < $bytes && !$this->eof())
  301. {
  302. $d = fread($this->fp, $bytes-$len);
  303. if ($this->_debug) {
  304. $this->debug('S: '. $d);
  305. }
  306. $data .= $d;
  307. $data_len = strlen($data);
  308. if ($len == $data_len) {
  309. break; // nothing was read -> exit to avoid apache lockups
  310. }
  311. $len = $data_len;
  312. }
  313. return $data;
  314. }
  315. /**
  316. * Reads complete response to the IMAP command
  317. *
  318. * @param array $untagged Will be filled with untagged response lines
  319. *
  320. * @return string Response text
  321. */
  322. function readReply(&$untagged=null)
  323. {
  324. do {
  325. $line = trim($this->readLine(1024));
  326. // store untagged response lines
  327. if ($line[0] == '*')
  328. $untagged[] = $line;
  329. } while ($line[0] == '*');
  330. if ($untagged)
  331. $untagged = join("\n", $untagged);
  332. return $line;
  333. }
  334. /**
  335. * Response parser.
  336. *
  337. * @param string $string Response text
  338. * @param string $err_prefix Error message prefix
  339. *
  340. * @return int Response status
  341. */
  342. function parseResult($string, $err_prefix='')
  343. {
  344. if (preg_match('/^[a-z0-9*]+ (OK|NO|BAD|BYE)(.*)$/i', trim($string), $matches)) {
  345. $res = strtoupper($matches[1]);
  346. $str = trim($matches[2]);
  347. if ($res == 'OK') {
  348. $this->errornum = self::ERROR_OK;
  349. } else if ($res == 'NO') {
  350. $this->errornum = self::ERROR_NO;
  351. } else if ($res == 'BAD') {
  352. $this->errornum = self::ERROR_BAD;
  353. } else if ($res == 'BYE') {
  354. $this->closeSocket();
  355. $this->errornum = self::ERROR_BYE;
  356. }
  357. if ($str) {
  358. $str = trim($str);
  359. // get response string and code (RFC5530)
  360. if (preg_match("/^\[([a-z-]+)\]/i", $str, $m)) {
  361. $this->resultcode = strtoupper($m[1]);
  362. $str = trim(substr($str, strlen($m[1]) + 2));
  363. }
  364. else {
  365. $this->resultcode = null;
  366. // parse response for [APPENDUID 1204196876 3456]
  367. if (preg_match("/^\[APPENDUID [0-9]+ ([0-9,:*]+)\]/i", $str, $m)) {
  368. $this->data['APPENDUID'] = $m[1];
  369. }
  370. }
  371. $this->result = $str;
  372. if ($this->errornum != self::ERROR_OK) {
  373. $this->error = $err_prefix ? $err_prefix.$str : $str;
  374. }
  375. }
  376. return $this->errornum;
  377. }
  378. return self::ERROR_UNKNOWN;
  379. }
  380. /**
  381. * Checks connection stream state.
  382. *
  383. * @return bool True if connection is closed
  384. */
  385. private function eof()
  386. {
  387. if (!is_resource($this->fp)) {
  388. return true;
  389. }
  390. // If a connection opened by fsockopen() wasn't closed
  391. // by the server, feof() will hang.
  392. $start = microtime(true);
  393. if (feof($this->fp) ||
  394. ($this->prefs['timeout'] && (microtime(true) - $start > $this->prefs['timeout']))
  395. ) {
  396. $this->closeSocket();
  397. return true;
  398. }
  399. return false;
  400. }
  401. /**
  402. * Closes connection stream.
  403. */
  404. private function closeSocket()
  405. {
  406. @fclose($this->fp);
  407. $this->fp = null;
  408. }
  409. /**
  410. * Error code/message setter.
  411. */
  412. function setError($code, $msg='')
  413. {
  414. $this->errornum = $code;
  415. $this->error = $msg;
  416. }
  417. /**
  418. * Checks response status.
  419. * Checks if command response line starts with specified prefix (or * BYE/BAD)
  420. *
  421. * @param string $string Response text
  422. * @param string $match Prefix to match with (case-sensitive)
  423. * @param bool $error Enables BYE/BAD checking
  424. * @param bool $nonempty Enables empty response checking
  425. *
  426. * @return bool True any check is true or connection is closed.
  427. */
  428. function startsWith($string, $match, $error=false, $nonempty=false)
  429. {
  430. if (!$this->fp) {
  431. return true;
  432. }
  433. if (strncmp($string, $match, strlen($match)) == 0) {
  434. return true;
  435. }
  436. if ($error && preg_match('/^\* (BYE|BAD) /i', $string, $m)) {
  437. if (strtoupper($m[1]) == 'BYE') {
  438. $this->closeSocket();
  439. }
  440. return true;
  441. }
  442. if ($nonempty && !strlen($string)) {
  443. return true;
  444. }
  445. return false;
  446. }
  447. private function hasCapability($name)
  448. {
  449. if (empty($this->capability) || $name == '') {
  450. return false;
  451. }
  452. if (in_array($name, $this->capability)) {
  453. return true;
  454. }
  455. else if (strpos($name, '=')) {
  456. return false;
  457. }
  458. $result = array();
  459. foreach ($this->capability as $cap) {
  460. $entry = explode('=', $cap);
  461. if ($entry[0] == $name) {
  462. $result[] = $entry[1];
  463. }
  464. }
  465. return !empty($result) ? $result : false;
  466. }
  467. /**
  468. * Capabilities checker
  469. *
  470. * @param string $name Capability name
  471. *
  472. * @return mixed Capability values array for key=value pairs, true/false for others
  473. */
  474. function getCapability($name)
  475. {
  476. $result = $this->hasCapability($name);
  477. if (!empty($result)) {
  478. return $result;
  479. }
  480. else if ($this->capability_readed) {
  481. return false;
  482. }
  483. // get capabilities (only once) because initial
  484. // optional CAPABILITY response may differ
  485. $result = $this->execute('CAPABILITY');
  486. if ($result[0] == self::ERROR_OK) {
  487. $this->parseCapability($result[1]);
  488. }
  489. $this->capability_readed = true;
  490. return $this->hasCapability($name);
  491. }
  492. function clearCapability()
  493. {
  494. $this->capability = array();
  495. $this->capability_readed = false;
  496. }
  497. /**
  498. * DIGEST-MD5/CRAM-MD5/PLAIN Authentication
  499. *
  500. * @param string $user
  501. * @param string $pass
  502. * @param string $type Authentication type (PLAIN/CRAM-MD5/DIGEST-MD5)
  503. *
  504. * @return resource Connection resourse on success, error code on error
  505. */
  506. function authenticate($user, $pass, $type='PLAIN')
  507. {
  508. if ($type == 'CRAM-MD5' || $type == 'DIGEST-MD5') {
  509. if ($type == 'DIGEST-MD5' && !class_exists('Auth_SASL')) {
  510. $this->setError(self::ERROR_BYE,
  511. "The Auth_SASL package is required for DIGEST-MD5 authentication");
  512. return self::ERROR_BAD;
  513. }
  514. $this->putLine($this->nextTag() . " AUTHENTICATE $type");
  515. $line = trim($this->readReply());
  516. if ($line[0] == '+') {
  517. $challenge = substr($line, 2);
  518. }
  519. else {
  520. return $this->parseResult($line);
  521. }
  522. if ($type == 'CRAM-MD5') {
  523. // RFC2195: CRAM-MD5
  524. $ipad = '';
  525. $opad = '';
  526. // initialize ipad, opad
  527. for ($i=0; $i<64; $i++) {
  528. $ipad .= chr(0x36);
  529. $opad .= chr(0x5C);
  530. }
  531. // pad $pass so it's 64 bytes
  532. $padLen = 64 - strlen($pass);
  533. for ($i=0; $i<$padLen; $i++) {
  534. $pass .= chr(0);
  535. }
  536. // generate hash
  537. $hash = md5($this->_xor($pass, $opad) . pack("H*",
  538. md5($this->_xor($pass, $ipad) . base64_decode($challenge))));
  539. $reply = base64_encode($user . ' ' . $hash);
  540. // send result
  541. $this->putLine($reply);
  542. }
  543. else {
  544. // RFC2831: DIGEST-MD5
  545. // proxy authorization
  546. if (!empty($this->prefs['auth_cid'])) {
  547. $authc = $this->prefs['auth_cid'];
  548. $pass = $this->prefs['auth_pw'];
  549. }
  550. else {
  551. $authc = $user;
  552. $user = '';
  553. }
  554. $auth_sasl = Auth_SASL::factory('digestmd5');
  555. $reply = base64_encode($auth_sasl->getResponse($authc, $pass,
  556. base64_decode($challenge), $this->host, 'imap', $user));
  557. // send result
  558. $this->putLine($reply);
  559. $line = trim($this->readReply());
  560. if ($line[0] == '+') {
  561. $challenge = substr($line, 2);
  562. }
  563. else {
  564. return $this->parseResult($line);
  565. }
  566. // check response
  567. $challenge = base64_decode($challenge);
  568. if (strpos($challenge, 'rspauth=') === false) {
  569. $this->setError(self::ERROR_BAD,
  570. "Unexpected response from server to DIGEST-MD5 response");
  571. return self::ERROR_BAD;
  572. }
  573. $this->putLine('');
  574. }
  575. $line = $this->readReply();
  576. $result = $this->parseResult($line);
  577. }
  578. else { // PLAIN
  579. // proxy authorization
  580. if (!empty($this->prefs['auth_cid'])) {
  581. $authc = $this->prefs['auth_cid'];
  582. $pass = $this->prefs['auth_pw'];
  583. }
  584. else {
  585. $authc = $user;
  586. $user = '';
  587. }
  588. $reply = base64_encode($user . chr(0) . $authc . chr(0) . $pass);
  589. // RFC 4959 (SASL-IR): save one round trip
  590. if ($this->getCapability('SASL-IR')) {
  591. list($result, $line) = $this->execute("AUTHENTICATE PLAIN", array($reply),
  592. self::COMMAND_LASTLINE | self::COMMAND_CAPABILITY);
  593. }
  594. else {
  595. $this->putLine($this->nextTag() . " AUTHENTICATE PLAIN");
  596. $line = trim($this->readReply());
  597. if ($line[0] != '+') {
  598. return $this->parseResult($line);
  599. }
  600. // send result, get reply and process it
  601. $this->putLine($reply);
  602. $line = $this->readReply();
  603. $result = $this->parseResult($line);
  604. }
  605. }
  606. if ($result == self::ERROR_OK) {
  607. // optional CAPABILITY response
  608. if ($line && preg_match('/\[CAPABILITY ([^]]+)\]/i', $line, $matches)) {
  609. $this->parseCapability($matches[1], true);
  610. }
  611. return $this->fp;
  612. }
  613. else {
  614. $this->setError($result, "AUTHENTICATE $type: $line");
  615. }
  616. return $result;
  617. }
  618. /**
  619. * LOGIN Authentication
  620. *
  621. * @param string $user
  622. * @param string $pass
  623. *
  624. * @return resource Connection resourse on success, error code on error
  625. */
  626. function login($user, $password)
  627. {
  628. list($code, $response) = $this->execute('LOGIN', array(
  629. $this->escape($user), $this->escape($password)), self::COMMAND_CAPABILITY);
  630. // re-set capabilities list if untagged CAPABILITY response provided
  631. if (preg_match('/\* CAPABILITY (.+)/i', $response, $matches)) {
  632. $this->parseCapability($matches[1], true);
  633. }
  634. if ($code == self::ERROR_OK) {
  635. return $this->fp;
  636. }
  637. return $code;
  638. }
  639. /**
  640. * Detects hierarchy delimiter
  641. *
  642. * @return string The delimiter
  643. */
  644. function getHierarchyDelimiter()
  645. {
  646. if ($this->prefs['delimiter']) {
  647. return $this->prefs['delimiter'];
  648. }
  649. // try (LIST "" ""), should return delimiter (RFC2060 Sec 6.3.8)
  650. list($code, $response) = $this->execute('LIST',
  651. array($this->escape(''), $this->escape('')));
  652. if ($code == self::ERROR_OK) {
  653. $args = $this->tokenizeResponse($response, 4);
  654. $delimiter = $args[3];
  655. if (strlen($delimiter) > 0) {
  656. return ($this->prefs['delimiter'] = $delimiter);
  657. }
  658. }
  659. return NULL;
  660. }
  661. /**
  662. * NAMESPACE handler (RFC 2342)
  663. *
  664. * @return array Namespace data hash (personal, other, shared)
  665. */
  666. function getNamespace()
  667. {
  668. if (array_key_exists('namespace', $this->prefs)) {
  669. return $this->prefs['namespace'];
  670. }
  671. if (!$this->getCapability('NAMESPACE')) {
  672. return self::ERROR_BAD;
  673. }
  674. list($code, $response) = $this->execute('NAMESPACE');
  675. if ($code == self::ERROR_OK && preg_match('/^\* NAMESPACE /', $response)) {
  676. $data = $this->tokenizeResponse(substr($response, 11));
  677. }
  678. if (!is_array($data)) {
  679. return $code;
  680. }
  681. $this->prefs['namespace'] = array(
  682. 'personal' => $data[0],
  683. 'other' => $data[1],
  684. 'shared' => $data[2],
  685. );
  686. return $this->prefs['namespace'];
  687. }
  688. /**
  689. * Connects to IMAP server and authenticates.
  690. *
  691. * @param string $host Server hostname or IP
  692. * @param string $user User name
  693. * @param string $password Password
  694. * @param array $options Connection and class options
  695. *
  696. * @return bool True on success, False on failure
  697. */
  698. function connect($host, $user, $password, $options=null)
  699. {
  700. // set options
  701. if (is_array($options)) {
  702. $this->prefs = $options;
  703. }
  704. // set auth method
  705. if (!empty($this->prefs['auth_type'])) {
  706. $auth_method = strtoupper($this->prefs['auth_type']);
  707. } else {
  708. $auth_method = 'CHECK';
  709. }
  710. $result = false;
  711. // initialize connection
  712. $this->error = '';
  713. $this->errornum = self::ERROR_OK;
  714. $this->selected = null;
  715. $this->user = $user;
  716. $this->host = $host;
  717. $this->logged = false;
  718. // check input
  719. if (empty($host)) {
  720. $this->setError(self::ERROR_BAD, "Empty host");
  721. return false;
  722. }
  723. if (empty($user)) {
  724. $this->setError(self::ERROR_NO, "Empty user");
  725. return false;
  726. }
  727. if (empty($password)) {
  728. $this->setError(self::ERROR_NO, "Empty password");
  729. return false;
  730. }
  731. if (!$this->prefs['port']) {
  732. $this->prefs['port'] = 143;
  733. }
  734. // check for SSL
  735. if ($this->prefs['ssl_mode'] && $this->prefs['ssl_mode'] != 'tls') {
  736. $host = $this->prefs['ssl_mode'] . '://' . $host;
  737. }
  738. if ($this->prefs['timeout'] <= 0) {
  739. $this->prefs['timeout'] = ini_get('default_socket_timeout');
  740. }
  741. // Connect
  742. $this->fp = @fsockopen($host, $this->prefs['port'], $errno, $errstr, $this->prefs['timeout']);
  743. if (!$this->fp) {
  744. $this->setError(self::ERROR_BAD, sprintf("Could not connect to %s:%d: %s", $host, $this->prefs['port'], $errstr));
  745. return false;
  746. }
  747. if ($this->prefs['timeout'] > 0)
  748. stream_set_timeout($this->fp, $this->prefs['timeout']);
  749. $line = trim(fgets($this->fp, 8192));
  750. if ($this->_debug) {
  751. // set connection identifier for debug output
  752. preg_match('/#([0-9]+)/', (string)$this->fp, $m);
  753. $this->resourceid = strtoupper(substr(md5($m[1].$this->user.microtime()), 0, 4));
  754. if ($line)
  755. $this->debug('S: '. $line);
  756. }
  757. // Connected to wrong port or connection error?
  758. if (!preg_match('/^\* (OK|PREAUTH)/i', $line)) {
  759. if ($line)
  760. $error = sprintf("Wrong startup greeting (%s:%d): %s", $host, $this->prefs['port'], $line);
  761. else
  762. $error = sprintf("Empty startup greeting (%s:%d)", $host, $this->prefs['port']);
  763. $this->setError(self::ERROR_BAD, $error);
  764. $this->closeConnection();
  765. return false;
  766. }
  767. // RFC3501 [7.1] optional CAPABILITY response
  768. if (preg_match('/\[CAPABILITY ([^]]+)\]/i', $line, $matches)) {
  769. $this->parseCapability($matches[1], true);
  770. }
  771. // TLS connection
  772. if ($this->prefs['ssl_mode'] == 'tls' && $this->getCapability('STARTTLS')) {
  773. if (version_compare(PHP_VERSION, '5.1.0', '>=')) {
  774. $res = $this->execute('STARTTLS');
  775. if ($res[0] != self::ERROR_OK) {
  776. $this->closeConnection();
  777. return false;
  778. }
  779. if (!stream_socket_enable_crypto($this->fp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
  780. $this->setError(self::ERROR_BAD, "Unable to negotiate TLS");
  781. $this->closeConnection();
  782. return false;
  783. }
  784. // Now we're secure, capabilities need to be reread
  785. $this->clearCapability();
  786. }
  787. }
  788. // Send ID info
  789. if (!empty($this->prefs['ident']) && $this->getCapability('ID')) {
  790. $this->id($this->prefs['ident']);
  791. }
  792. $auth_methods = array();
  793. $result = null;
  794. // check for supported auth methods
  795. if ($auth_method == 'CHECK') {
  796. if ($auth_caps = $this->getCapability('AUTH')) {
  797. $auth_methods = $auth_caps;
  798. }
  799. // RFC 2595 (LOGINDISABLED) LOGIN disabled when connection is not secure
  800. $login_disabled = $this->getCapability('LOGINDISABLED');
  801. if (($key = array_search('LOGIN', $auth_methods)) !== false) {
  802. if ($login_disabled) {
  803. unset($auth_methods[$key]);
  804. }
  805. }
  806. else if (!$login_disabled) {
  807. $auth_methods[] = 'LOGIN';
  808. }
  809. // Use best (for security) supported authentication method
  810. foreach (array('DIGEST-MD5', 'CRAM-MD5', 'CRAM_MD5', 'PLAIN', 'LOGIN') as $auth_method) {
  811. if (in_array($auth_method, $auth_methods)) {
  812. break;
  813. }
  814. }
  815. }
  816. else {
  817. // Prevent from sending credentials in plain text when connection is not secure
  818. if ($auth_method == 'LOGIN' && $this->getCapability('LOGINDISABLED')) {
  819. $this->setError(self::ERROR_BAD, "Login disabled by IMAP server");
  820. $this->closeConnection();
  821. return false;
  822. }
  823. // replace AUTH with CRAM-MD5 for backward compat.
  824. if ($auth_method == 'AUTH') {
  825. $auth_method = 'CRAM-MD5';
  826. }
  827. }
  828. // pre-login capabilities can be not complete
  829. $this->capability_readed = false;
  830. // Authenticate
  831. switch ($auth_method) {
  832. case 'CRAM_MD5':
  833. $auth_method = 'CRAM-MD5';
  834. case 'CRAM-MD5':
  835. case 'DIGEST-MD5':
  836. case 'PLAIN':
  837. $result = $this->authenticate($user, $password, $auth_method);
  838. break;
  839. case 'LOGIN':
  840. $result = $this->login($user, $password);
  841. break;
  842. default:
  843. $this->setError(self::ERROR_BAD, "Configuration error. Unknown auth method: $auth_method");
  844. }
  845. // Connected and authenticated
  846. if (is_resource($result)) {
  847. if ($this->prefs['force_caps']) {
  848. $this->clearCapability();
  849. }
  850. $this->logged = true;
  851. return true;
  852. }
  853. $this->closeConnection();
  854. return false;
  855. }
  856. /**
  857. * Checks connection status
  858. *
  859. * @return bool True if connection is active and user is logged in, False otherwise.
  860. */
  861. function connected()
  862. {
  863. return ($this->fp && $this->logged) ? true : false;
  864. }
  865. /**
  866. * Closes connection with logout.
  867. */
  868. function closeConnection()
  869. {
  870. if ($this->putLine($this->nextTag() . ' LOGOUT')) {
  871. $this->readReply();
  872. }
  873. $this->closeSocket();
  874. }
  875. /**
  876. * Executes SELECT command (if mailbox is already not in selected state)
  877. *
  878. * @param string $mailbox Mailbox name
  879. * @param array $qresync_data QRESYNC data (RFC5162)
  880. *
  881. * @return boolean True on success, false on error
  882. */
  883. function select($mailbox, $qresync_data = null)
  884. {
  885. if (!strlen($mailbox)) {
  886. return false;
  887. }
  888. if ($this->selected === $mailbox) {
  889. return true;
  890. }
  891. /*
  892. Temporary commented out because Courier returns \Noselect for INBOX
  893. Requires more investigation
  894. if (is_array($this->data['LIST']) && is_array($opts = $this->data['LIST'][$mailbox])) {
  895. if (in_array('\\Noselect', $opts)) {
  896. return false;
  897. }
  898. }
  899. */
  900. $params = array($this->escape($mailbox));
  901. // QRESYNC data items
  902. // 0. the last known UIDVALIDITY,
  903. // 1. the last known modification sequence,
  904. // 2. the optional set of known UIDs, and
  905. // 3. an optional parenthesized list of known sequence ranges and their
  906. // corresponding UIDs.
  907. if (!empty($qresync_data)) {
  908. if (!empty($qresync_data[2]))
  909. $qresync_data[2] = self::compressMessageSet($qresync_data[2]);
  910. $params[] = array('QRESYNC', $qresync_data);
  911. }
  912. list($code, $response) = $this->execute('SELECT', $params);
  913. if ($code == self::ERROR_OK) {
  914. $response = explode("\r\n", $response);
  915. foreach ($response as $line) {
  916. if (preg_match('/^\* ([0-9]+) (EXISTS|RECENT)$/i', $line, $m)) {
  917. $this->data[strtoupper($m[2])] = (int) $m[1];
  918. }
  919. else if (preg_match('/^\* OK \[/i', $line, $match)) {
  920. $line = substr($line, 6);
  921. if (preg_match('/^(UIDNEXT|UIDVALIDITY|UNSEEN) ([0-9]+)/i', $line, $match)) {
  922. $this->data[strtoupper($match[1])] = (int) $match[2];
  923. }
  924. else if (preg_match('/^(HIGHESTMODSEQ) ([0-9]+)/i', $line, $match)) {
  925. $this->data[strtoupper($match[1])] = (string) $match[2];
  926. }
  927. else if (preg_match('/^(NOMODSEQ)/i', $line, $match)) {
  928. $this->data[strtoupper($match[1])] = true;
  929. }
  930. else if (preg_match('/^PERMANENTFLAGS \(([^\)]+)\)/iU', $line, $match)) {
  931. $this->data['PERMANENTFLAGS'] = explode(' ', $match[1]);
  932. }
  933. }
  934. // QRESYNC FETCH response (RFC5162)
  935. else if (preg_match('/^\* ([0-9+]) FETCH/i', $line, $match)) {
  936. $line = substr($line, strlen($match[0]));
  937. $fetch_data = $this->tokenizeResponse($line, 1);
  938. $data = array('id' => $match[1]);
  939. for ($i=0, $size=count($fetch_data); $i<$size; $i+=2) {
  940. $data[strtolower($fetch_data[$i])] = $fetch_data[$i+1];
  941. }
  942. $this->data['QRESYNC'][$data['uid']] = $data;
  943. }
  944. // QRESYNC VANISHED response (RFC5162)
  945. else if (preg_match('/^\* VANISHED [()EARLIER]*/i', $line, $match)) {
  946. $line = substr($line, strlen($match[0]));
  947. $v_data = $this->tokenizeResponse($line, 1);
  948. $this->data['VANISHED'] = $v_data;
  949. }
  950. }
  951. $this->data['READ-WRITE'] = $this->resultcode != 'READ-ONLY';
  952. $this->selected = $mailbox;
  953. return true;
  954. }
  955. return false;
  956. }
  957. /**
  958. * Executes STATUS command
  959. *
  960. * @param string $mailbox Mailbox name
  961. * @param array $items Additional requested item names. By default
  962. * MESSAGES and UNSEEN are requested. Other defined
  963. * in RFC3501: UIDNEXT, UIDVALIDITY, RECENT
  964. *
  965. * @return array Status item-value hash
  966. * @since 0.5-beta
  967. */
  968. function status($mailbox, $items=array())
  969. {
  970. if (!strlen($mailbox)) {
  971. return false;
  972. }
  973. if (!in_array('MESSAGES', $items)) {
  974. $items[] = 'MESSAGES';
  975. }
  976. if (!in_array('UNSEEN', $items)) {
  977. $items[] = 'UNSEEN';
  978. }
  979. list($code, $response) = $this->execute('STATUS', array($this->escape($mailbox),
  980. '(' . implode(' ', (array) $items) . ')'));
  981. if ($code == self::ERROR_OK && preg_match('/\* STATUS /i', $response)) {
  982. $result = array();
  983. $response = substr($response, 9); // remove prefix "* STATUS "
  984. list($mbox, $items) = $this->tokenizeResponse($response, 2);
  985. // Fix for #1487859. Some buggy server returns not quoted
  986. // folder name with spaces. Let's try to handle this situation
  987. if (!is_array($items) && ($pos = strpos($response, '(')) !== false) {
  988. $response = substr($response, $pos);
  989. $items = $this->tokenizeResponse($response, 1);
  990. if (!is_array($items)) {
  991. return $result;
  992. }
  993. }
  994. for ($i=0, $len=count($items); $i<$len; $i += 2) {
  995. $result[$items[$i]] = $items[$i+1];
  996. }
  997. $this->data['STATUS:'.$mailbox] = $result;
  998. return $result;
  999. }
  1000. return false;
  1001. }
  1002. /**
  1003. * Executes EXPUNGE command
  1004. *
  1005. * @param string $mailbox Mailbox name
  1006. * @param string $messages Message UIDs to expunge
  1007. *
  1008. * @return boolean True on success, False on error
  1009. */
  1010. function expunge($mailbox, $messages=NULL)
  1011. {
  1012. if (!$this->select($mailbox)) {
  1013. return false;
  1014. }
  1015. if (!$this->data['READ-WRITE']) {
  1016. $this->setError(self::ERROR_READONLY, "Mailbox is read-only", 'EXPUNGE');
  1017. return false;
  1018. }
  1019. // Clear internal status cache
  1020. unset($this->data['STATUS:'.$mailbox]);
  1021. if ($messages)
  1022. $result = $this->execute('UID EXPUNGE', array($messages), self::COMMAND_NORESPONSE);
  1023. else
  1024. $result = $this->execute('EXPUNGE', null, self::COMMAND_NORESPONSE);
  1025. if ($result == self::ERROR_OK) {
  1026. $this->selected = null; // state has changed, need to reselect
  1027. return true;
  1028. }
  1029. return false;
  1030. }
  1031. /**
  1032. * Executes CLOSE command
  1033. *
  1034. * @return boolean True on success, False on error
  1035. * @since 0.5
  1036. */
  1037. function close()
  1038. {
  1039. $result = $this->execute('CLOSE', NULL, self::COMMAND_NORESPONSE);
  1040. if ($result == self::ERROR_OK) {
  1041. $this->selected = null;
  1042. return true;
  1043. }
  1044. return false;
  1045. }
  1046. /**
  1047. * Folder subscription (SUBSCRIBE)
  1048. *
  1049. * @param string $mailbox Mailbox name
  1050. *
  1051. * @return boolean True on success, False on error
  1052. */
  1053. function subscribe($mailbox)
  1054. {
  1055. $result = $this->execute('SUBSCRIBE', array($this->escape($mailbox)),
  1056. self::COMMAND_NORESPONSE);
  1057. return ($result == self::ERROR_OK);
  1058. }
  1059. /**
  1060. * Folder unsubscription (UNSUBSCRIBE)
  1061. *
  1062. * @param string $mailbox Mailbox name
  1063. *
  1064. * @return boolean True on success, False on error
  1065. */
  1066. function unsubscribe($mailbox)
  1067. {
  1068. $result = $this->execute('UNSUBSCRIBE', array($this->escape($mailbox)),
  1069. self::COMMAND_NORESPONSE);
  1070. return ($result == self::ERROR_OK);
  1071. }
  1072. /**
  1073. * Folder creation (CREATE)
  1074. *
  1075. * @param string $mailbox Mailbox name
  1076. *
  1077. * @return bool True on success, False on error
  1078. */
  1079. function createFolder($mailbox)
  1080. {
  1081. $result = $this->execute('CREATE', array($this->escape($mailbox)),
  1082. self::COMMAND_NORESPONSE);
  1083. return ($result == self::ERROR_OK);
  1084. }
  1085. /**
  1086. * Folder renaming (RENAME)
  1087. *
  1088. * @param string $mailbox Mailbox name
  1089. *
  1090. * @return bool True on success, False on error
  1091. */
  1092. function renameFolder($from, $to)
  1093. {
  1094. $result = $this->execute('RENAME', array($this->escape($from), $this->escape($to)),
  1095. self::COMMAND_NORESPONSE);
  1096. return ($result == self::ERROR_OK);
  1097. }
  1098. /**
  1099. * Executes DELETE command
  1100. *
  1101. * @param string $mailbox Mailbox name
  1102. *
  1103. * @return boolean True on success, False on error
  1104. */
  1105. function deleteFolder($mailbox)
  1106. {
  1107. $result = $this->execute('DELETE', array($this->escape($mailbox)),
  1108. self::COMMAND_NORESPONSE);
  1109. return ($result == self::ERROR_OK);
  1110. }
  1111. /**
  1112. * Removes all messages in a folder
  1113. *
  1114. * @param string $mailbox Mailbox name
  1115. *
  1116. * @return boolean True on success, False on error
  1117. */
  1118. function clearFolder($mailbox)
  1119. {
  1120. $num_in_trash = $this->countMessages($mailbox);
  1121. if ($num_in_trash > 0) {
  1122. $res = $this->flag($mailbox, '1:*', 'DELETED');
  1123. }
  1124. if ($res) {
  1125. if ($this->selected === $mailbox)
  1126. $res = $this->close();
  1127. else
  1128. $res = $this->expunge($mailbox);
  1129. }
  1130. return $res;
  1131. }
  1132. /**
  1133. * Returns list of mailboxes
  1134. *
  1135. * @param string $ref Reference name
  1136. * @param string $mailbox Mailbox name
  1137. * @param array $status_opts (see self::_listMailboxes)
  1138. * @param array $select_opts (see self::_listMailboxes)
  1139. *
  1140. * @return array List of mailboxes or hash of options if $status_opts argument
  1141. * is non-empty.
  1142. */
  1143. function listMailboxes($ref, $mailbox, $status_opts=array(), $select_opts=array())
  1144. {
  1145. return $this->_listMailboxes($ref, $mailbox, false, $status_opts, $select_opts);
  1146. }
  1147. /**
  1148. * Returns list of subscribed mailboxes
  1149. *
  1150. * @param string $ref Reference name
  1151. * @param string $mailbox Mailbox name
  1152. * @param array $status_opts (see self::_listMailboxes)
  1153. *
  1154. * @return array List of mailboxes or hash of options if $status_opts argument
  1155. * is non-empty.
  1156. */
  1157. function listSubscribed($ref, $mailbox, $status_opts=array())
  1158. {
  1159. return $this->_listMailboxes($ref, $mailbox, true, $status_opts, NULL);
  1160. }
  1161. /**
  1162. * IMAP LIST/LSUB command
  1163. *
  1164. * @param string $ref Reference name
  1165. * @param string $mailbox Mailbox name
  1166. * @param bool $subscribed Enables returning subscribed mailboxes only
  1167. * @param array $status_opts List of STATUS options (RFC5819: LIST-STATUS)
  1168. * Possible: MESSAGES, RECENT, UIDNEXT, UIDVALIDITY, UNSEEN
  1169. * @param array $select_opts List of selection options (RFC5258: LIST-EXTENDED)
  1170. * Possible: SUBSCRIBED, RECURSIVEMATCH, REMOTE
  1171. *
  1172. * @return array List of mailboxes or hash of options if $status_ops argument
  1173. * is non-empty.
  1174. */
  1175. private function _listMailboxes($ref, $mailbox, $subscribed=false,
  1176. $status_opts=array(), $select_opts=array())
  1177. {
  1178. if (!strlen($mailbox)) {
  1179. $mailbox = '*';
  1180. }
  1181. $args = array();
  1182. if (!empty($select_opts) && $this->getCapability('LIST-EXTENDED')) {
  1183. $select_opts = (array) $select_opts;
  1184. $args[] = '(' . implode(' ', $select_opts) . ')';
  1185. }
  1186. $args[] = $this->escape($ref);
  1187. $args[] = $this->escape($mailbox);
  1188. if (!empty($status_opts) && $this->getCapability('LIST-STATUS')) {
  1189. $status_opts = (array) $status_opts;
  1190. $lstatus = true;
  1191. $args[] = 'RETURN (STATUS (' . implode(' ', $status_opts) . '))';
  1192. }
  1193. list($code, $response) = $this->execute($subscribed ? 'LSUB' : 'LIST', $args);
  1194. if ($code == self::ERROR_OK) {
  1195. $folders = array();
  1196. $last = 0;
  1197. $pos = 0;
  1198. $response .= "\r\n";
  1199. while ($pos = strpos($response, "\r\n", $pos+1)) {
  1200. // literal string, not real end-of-command-line
  1201. if ($response[$pos-1] == '}') {
  1202. continue;
  1203. }
  1204. $line = substr($response, $last, $pos - $last);
  1205. $last = $pos + 2;
  1206. if (!preg_match('/^\* (LIST|LSUB|STATUS) /i', $line, $m)) {
  1207. continue;
  1208. }
  1209. $cmd = strtoupper($m[1]);
  1210. $line = substr($line, strlen($m[0]));
  1211. // * LIST (<options>) <delimiter> <mailbox>
  1212. if ($cmd == 'LIST' || $cmd == 'LSUB') {
  1213. list($opts, $delim, $mailbox) = $this->tokenizeResponse($line, 3);
  1214. // Add to result array
  1215. if (!$lstatus) {
  1216. $folders[] = $mailbox;
  1217. }
  1218. else {
  1219. $folders[$mailbox] = array();
  1220. }
  1221. // store LSUB options only if not empty, this way
  1222. // we can detect a situation when LIST doesn't return specified folder
  1223. if (!empty($opts) || $cmd == 'LIST') {
  1224. // Add to options array
  1225. if (empty($this->data['LIST'][$mailbox]))
  1226. $this->data['LIST'][$mailbox] = $opts;
  1227. else if (!empty($opts))
  1228. $this->data['LIST'][$mailbox] = array_unique(array_merge(
  1229. $this->data['LIST'][$mailbox], $opts));
  1230. }
  1231. }
  1232. // * STATUS <mailbox> (<result>)
  1233. else if ($cmd == 'STATUS') {
  1234. list($mailbox, $status) = $this->tokenizeResponse($line, 2);
  1235. for ($i=0, $len=count($status); $i<$len; $i += 2) {
  1236. list($name, $value) = $this->tokenizeResponse($status, 2);
  1237. $folders[$mailbox][$name] = $value;
  1238. }
  1239. }
  1240. }
  1241. return $folders;
  1242. }
  1243. return false;
  1244. }
  1245. /**
  1246. * Returns count of all messages in a folder
  1247. *
  1248. * @param string $mailbox Mailbox name
  1249. *
  1250. * @return int Number of messages, False on error
  1251. */
  1252. function countMessages($mailbox, $refresh = false)
  1253. {
  1254. if ($refresh) {
  1255. $this->selected = null;
  1256. }
  1257. if ($this->selected === $mailbox) {
  1258. return $this->data['EXISTS'];
  1259. }
  1260. // Check internal cache
  1261. $cache = $this->data['STATUS:'.$mailbox];
  1262. if (!empty($cache) && isset($cache['MESSAGES'])) {
  1263. return (int) $cache['MESSAGES'];
  1264. }
  1265. // Try STATUS (should be faster than SELECT)
  1266. $counts = $this->status($mailbox);
  1267. if (is_array($counts)) {
  1268. return (int) $counts['MESSAGES'];
  1269. }
  1270. return false;
  1271. }
  1272. /**
  1273. * Returns count of messages with \Recent flag in a folder
  1274. *
  1275. * @param string $mailbox Mailbox name
  1276. *
  1277. * @return int Number of messages, False on error
  1278. */
  1279. function countRecent($mailbox)
  1280. {
  1281. if (!strlen($mailbox)) {
  1282. $mailbox = 'INBOX';
  1283. }
  1284. $this->select($mailbox);
  1285. if ($this->selected === $mailbox) {
  1286. return $this->data['RECENT'];
  1287. }
  1288. return false;
  1289. }
  1290. /**
  1291. * Returns count of messages without \Seen flag in a specified folder
  1292. *
  1293. * @param string $mailbox Mailbox name
  1294. *
  1295. * @return int Number of messages, False on error
  1296. */
  1297. function countUnseen($mailbox)
  1298. {
  1299. // Check internal cache
  1300. $cache = $this->data['STATUS:'.$mailbox];
  1301. if (!empty($cache) && isset($cache['UNSEEN'])) {
  1302. return (int) $cache['UNSEEN'];
  1303. }
  1304. // Try STATUS (should be faster than SELECT+SEARCH)
  1305. $counts = $this->status($mailbox);
  1306. if (is_array($counts)) {
  1307. return (int) $counts['UNSEEN'];
  1308. }
  1309. // Invoke SEARCH as a fallback
  1310. $index = $this->search($mailbox, 'ALL UNSEEN', false, array('COUNT'));
  1311. if (!$index->is_error()) {
  1312. return $index->count();
  1313. }
  1314. return false;
  1315. }
  1316. /**
  1317. * Executes ID command (RFC2971)
  1318. *
  1319. * @param array $items Client identification information key/value hash
  1320. *
  1321. * @return array Server identification information key/value hash
  1322. * @since 0.6
  1323. */
  1324. function id($items=array())
  1325. {
  1326. if (is_array($items) && !empty($items)) {
  1327. foreach ($items as $key => $value) {
  1328. $args[] = $this->escape($key, true);
  1329. $args[] = $this->escape($value, true);
  1330. }
  1331. }
  1332. list($code, $response) = $this->execute('ID', array(
  1333. !empty($args) ? '(' . implode(' ', (array) $args) . ')' : $this->escape(null)
  1334. ));
  1335. if ($code == self::ERROR_OK && preg_match('/\* ID /i', $response)) {
  1336. $response = substr($response, 5); // remove prefix "* ID "
  1337. $items = $this->tokenizeResponse($response, 1);
  1338. $result = null;
  1339. for ($i=0, $len=count($items); $i<$len; $i += 2) {
  1340. $result[$items[$i]] = $items[$i+1];
  1341. }
  1342. return $result;
  1343. }
  1344. return false;
  1345. }
  1346. /**
  1347. * Executes ENABLE command (RFC5161)
  1348. *
  1349. * @param mixed $extension Extension name to enable (or array of names)
  1350. *
  1351. * @return array|bool List of enabled extensions, False on error
  1352. * @since 0.6
  1353. */
  1354. function enable($extension)
  1355. {
  1356. if (empty($extension))
  1357. return false;
  1358. if (!$this->hasCapability('ENABLE'))
  1359. return false;
  1360. if (!is_array($extension))
  1361. $extension = array($extension);
  1362. list($code, $response) = $this->execute('ENABLE', $extension);
  1363. if ($code == self::ERROR_OK && preg_match('/\* ENABLED /i', $response)) {
  1364. $response = substr($response, 10); // remove prefix "* ENABLED "
  1365. $result = (array) $this->tokenizeResponse($response);
  1366. return $result;
  1367. }
  1368. return false;
  1369. }
  1370. /**
  1371. * Executes SORT command
  1372. *
  1373. * @param string $mailbox Mailbox name
  1374. * @param string $field Field to sort by (ARRIVAL, CC, DATE, FROM, SIZE, SUBJECT, TO)
  1375. * @param string $add Searching criteria
  1376. * @param bool $return_uid Enables UID SORT usage
  1377. * @param string $encoding Character set
  1378. *
  1379. * @return rcube_result_index Response data
  1380. */
  1381. function sort($mailbox, $field, $add='', $return_uid=false, $encoding = 'US-ASCII')
  1382. {
  1383. require_once dirname(__FILE__) . '/rcube_result_index.php';
  1384. $field = strtoupper($field);
  1385. if ($field == 'INTERNALDATE') {
  1386. $field = 'ARRIVAL';
  1387. }
  1388. $fields = array('ARRIVAL' => 1,'CC' => 1,'DATE' => 1,
  1389. 'FROM' => 1, 'SIZE' => 1, 'SUBJECT' => 1, 'TO' => 1);
  1390. if (!$fields[$field]) {
  1391. return new rcube_result_index($mailbox);
  1392. }
  1393. if (!$this->select($mailbox)) {
  1394. return new rcube_result_index($mailbox);
  1395. }
  1396. // RFC 5957: SORT=DISPLAY
  1397. if (($field == 'FROM' || $field == 'TO') && $this->getCapability('SORT=DISPLAY')) {
  1398. $field = 'DISPLAY' . $field;
  1399. }
  1400. // message IDs
  1401. if (!empty($add))
  1402. $add = $this->compressMessageSet($add);
  1403. list($code, $response) = $this->execute($return_uid ? 'UID SORT' : 'SORT',
  1404. array("($field)", $encoding, 'ALL' . (!empty($add) ? ' '.$ad

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