PageRenderTime 60ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 1ms

/roundcubemail-0.7.2-dep/program/include/rcube_imap_generic.php

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

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