PageRenderTime 78ms CodeModel.GetById 30ms RepoModel.GetById 1ms app.codeStats 0ms

/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
  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, "Unable to send command: $request");
  1374. return false;
  1375. }
  1376. do {
  1377. $line = $this->readLine(4096);
  1378. if (!$line)
  1379. break;
  1380. // Sample reply line:
  1381. // * 321 FETCH (UID 2417 RFC822.SIZE 2730 FLAGS (\Seen)
  1382. // INTERNALDATE "16-Nov-2008 21:08:46 +0100" BODYSTRUCTURE (...)
  1383. // BODY[HEADER.FIELDS ...
  1384. if (preg_match('/^\* ([0-9]+) FETCH/', $line, $m)) {
  1385. $id = intval($m[1]);
  1386. $result[$id] = new rcube_mail_header;
  1387. $result[$id]->id = $id;
  1388. $result[$id]->subject = '';
  1389. $result[$id]->messageID = 'mid:' . $id;
  1390. $lines = array();
  1391. $line = substr($line, strlen($m[0]) + 2);
  1392. $ln = 0;
  1393. // get complete entry
  1394. while (preg_match('/\{([0-9]+)\}\r\n$/', $line, $m)) {
  1395. $bytes = $m[1];
  1396. $out = '';
  1397. while (strlen($out) < $bytes) {
  1398. $out = $this->readBytes($bytes);
  1399. if ($out === NULL)
  1400. break;
  1401. $line .= $out;
  1402. }
  1403. $str = $this->readLine(4096);
  1404. if ($str === false)
  1405. break;
  1406. $line .= $str;
  1407. }
  1408. // Tokenize response and assign to object properties
  1409. while (list($name, $value) = $this->tokenizeResponse($line, 2)) {
  1410. if ($name == 'UID') {
  1411. $result[$id]->uid = intval($value);
  1412. }
  1413. else if ($name == 'RFC822.SIZE') {
  1414. $result[$id]->size = intval($value);
  1415. }
  1416. else if ($name == 'RFC822.TEXT') {
  1417. $result[$id]->body = $value;
  1418. }
  1419. else if ($name == 'INTERNALDATE') {
  1420. $result[$id]->internaldate = $value;
  1421. $result[$id]->date = $value;
  1422. $result[$id]->timestamp = $this->StrToTime($value);
  1423. }
  1424. else if ($name == 'FLAGS') {
  1425. if (!empty($value)) {
  1426. foreach ((array)$value as $flag) {
  1427. $flag = str_replace(array('$', '\\'), '', $flag);
  1428. $flag = strtoupper($flag);
  1429. $result[$id]->flags[$flag] = true;
  1430. }
  1431. }
  1432. }
  1433. else if ($name == 'MODSEQ') {
  1434. $result[$id]->modseq = $value[0];
  1435. }
  1436. else if ($name == 'ENVELOPE') {
  1437. $result[$id]->envelope = $value;
  1438. }
  1439. else if ($name == 'BODYSTRUCTURE' || ($name == 'BODY' && count($value) > 2)) {
  1440. if (!is_array($value[0]) && (strtolower($value[0]) == 'message' && strtolower($value[1]) == 'rfc822')) {
  1441. $value = array($value);
  1442. }
  1443. $result[$id]->bodystructure = $value;
  1444. }
  1445. else if ($name == 'RFC822') {
  1446. $result[$id]->body = $value;
  1447. }
  1448. else if ($name == 'BODY') {
  1449. $body = $this->tokenizeResponse($line, 1);
  1450. if ($value[0] == 'HEADER.FIELDS')
  1451. $headers = $body;
  1452. else if (!empty($value))
  1453. $result[$id]->bodypart[$value[0]] = $body;
  1454. else
  1455. $result[$id]->body = $body;
  1456. }
  1457. }
  1458. // create array with header field:data
  1459. if (!empty($headers)) {
  1460. $headers = explode("\n", trim($headers));
  1461. foreach ($headers as $hid => $resln) {
  1462. if (ord($resln[0]) <= 32) {
  1463. $lines[$ln] .= (empty($lines[$ln]) ? '' : "\n") . trim($resln);
  1464. } else {
  1465. $lines[++$ln] = trim($resln);
  1466. }
  1467. }
  1468. while (list($lines_key, $str) = each($lines)) {
  1469. list($field, $string) = explode(':', $str, 2);
  1470. $field = strtolower($field);
  1471. $string = preg_replace('/\n[\t\s]*/', ' ', trim($string));
  1472. switch ($field) {
  1473. case 'date';
  1474. $result[$id]->date = $string;
  1475. $result[$id]->timestamp = $this->strToTime($string);
  1476. break;
  1477. case 'from':
  1478. $result[$id]->from = $string;
  1479. break;
  1480. case 'to':
  1481. $result[$id]->to = preg_replace('/undisclosed-recipients:[;,]*/', '', $string);
  1482. break;
  1483. case 'subject':
  1484. $result[$id]->subject = $string;
  1485. break;
  1486. case 'reply-to':
  1487. $result[$id]->replyto = $string;
  1488. break;
  1489. case 'cc':
  1490. $result[$id]->cc = $string;
  1491. break;
  1492. case 'bcc':
  1493. $result[$id]->bcc = $string;
  1494. break;
  1495. case 'content-transfer-encoding':
  1496. $result[$id]->encoding = $string;
  1497. break;
  1498. case 'content-type':
  1499. $ctype_parts = preg_split('/[; ]/', $string);
  1500. $result[$id]->ctype = strtolower(array_shift($ctype_parts));
  1501. if (preg_match('/charset\s*=\s*"?([a-z0-9\-\.\_]+)"?/i', $string, $regs)) {
  1502. $result[$id]->charset = $regs[1];
  1503. }
  1504. break;
  1505. case 'in-reply-to':
  1506. $result[$id]->in_reply_to = str_replace(array("\n", '<', '>'), '', $string);
  1507. break;
  1508. case 'references':
  1509. $result[$id]->references = $string;
  1510. break;
  1511. case 'return-receipt-to':
  1512. case 'disposition-notification-to':
  1513. case 'x-confirm-reading-to':
  1514. $result[$id]->mdn_to = $string;
  1515. break;
  1516. case 'message-id':
  1517. $result[$id]->messageID = $string;
  1518. break;
  1519. case 'x-priority':
  1520. if (preg_match('/^(\d+)/', $string, $matches)) {
  1521. $result[$id]->priority = intval($matches[1]);
  1522. }
  1523. break;
  1524. default:
  1525. if (strlen($field) > 2) {
  1526. $result[$id]->others[$field] = $string;
  1527. }
  1528. break;
  1529. }
  1530. }
  1531. }
  1532. }
  1533. // VANISHED response (QRESYNC RFC5162)
  1534. // Sample: * VANISHED (EARLIER) 300:310,405,411
  1535. else if (preg_match('/^\* VANISHED [()EARLIER]*/i', $line, $match)) {
  1536. $line = substr($line, strlen($match[0]));
  1537. $v_data = $this->tokenizeResponse($line, 1);
  1538. $this->data['VANISHED'] = $v_data;
  1539. }
  1540. } while (!$this->startsWith($line, $key, true));
  1541. return $result;
  1542. }
  1543. function fetchHeaders($mailbox, $message_set, $is_uid = false, $bodystr = false, $add = '')
  1544. {
  1545. $query_items = array('UID', 'RFC822.SIZE', 'FLAGS', 'INTERNALDATE');
  1546. if ($bodystr)
  1547. $query_items[] = 'BODYSTRUCTURE';
  1548. $query_items[] = 'BODY.PEEK[HEADER.FIELDS ('
  1549. . 'DATE FROM TO SUBJECT CONTENT-TYPE CC REPLY-TO LIST-POST DISPOSITION-NOTIFICATION-TO X-PRIORITY'
  1550. . ($add ? ' ' . trim($add) : '')
  1551. . ')]';
  1552. $result = $this->fetch($mailbox, $message_set, $is_uid, $query_items);
  1553. return $result;
  1554. }
  1555. function fetchHeader($mailbox, $id, $uidfetch=false, $bodystr=false, $add='')
  1556. {
  1557. $a = $this->fetchHeaders($mailbox, $id, $uidfetch, $bodystr, $add);
  1558. if (is_array($a)) {
  1559. return array_shift($a);
  1560. }
  1561. return false;
  1562. }
  1563. function sortHeaders($a, $field, $flag)
  1564. {
  1565. if (empty($field)) {
  1566. $field = 'uid';
  1567. }
  1568. else {
  1569. $field = strtolower($field);
  1570. }
  1571. if ($field == 'date' || $field == 'internaldate') {
  1572. $field = 'timestamp';
  1573. }
  1574. if (empty($flag)) {
  1575. $flag = 'ASC';
  1576. } else {
  1577. $flag = strtoupper($flag);
  1578. }
  1579. $c = count($a);
  1580. if ($c > 0) {
  1581. // Strategy:
  1582. // First, we'll create an "index" array.
  1583. // Then, we'll use sort() on that array,
  1584. // and use that to sort the main array.
  1585. // create "index" array
  1586. $index = array();
  1587. reset($a);
  1588. while (list($key, $val) = each($a)) {
  1589. if ($field == 'timestamp') {
  1590. $data = $this->strToTime($val->date);
  1591. if (!$data) {
  1592. $data = $val->timestamp;
  1593. }
  1594. } else {
  1595. $data = $val->$field;
  1596. if (is_string($data)) {
  1597. $data = str_replace('"', '', $data);
  1598. if ($field == 'subject') {
  1599. $data = preg_replace('/^(Re: \s*|Fwd:\s*|Fw:\s*)+/i', '', $data);
  1600. }
  1601. $data = strtoupper($data);
  1602. }
  1603. }
  1604. $index[$key] = $data;
  1605. }
  1606. // sort index
  1607. if ($flag == 'ASC') {
  1608. asort($index);
  1609. } else {
  1610. arsort($index);
  1611. }
  1612. // form new array based on index
  1613. $result = array();
  1614. reset($index);
  1615. while (list($key, $val) = each($index)) {
  1616. $result[$key] = $a[$key];
  1617. }
  1618. }
  1619. return $result;
  1620. }
  1621. function modFlag($mailbox, $messages, $flag, $mod)
  1622. {
  1623. if ($mod != '+' && $mod != '-') {
  1624. $mod = '+';
  1625. }
  1626. if (!$this->select($mailbox)) {
  1627. return false;
  1628. }
  1629. if (!$this->data['READ-WRITE']) {
  1630. $this->setError(self::ERROR_READONLY, "Mailbox is read-only", 'STORE');
  1631. return false;
  1632. }
  1633. // Clear internal status cache
  1634. if ($flag == 'SEEN') {
  1635. unset($this->data['STATUS:'.$mailbox]['UNSEEN']);
  1636. }
  1637. $flag = $this->flags[strtoupper($flag)];
  1638. $result = $this->execute('UID STORE', array(
  1639. $this->compressMessageSet($messages), $mod . 'FLAGS.SILENT', "($flag)"),
  1640. self::COMMAND_NORESPONSE);
  1641. return ($result == self::ERROR_OK);
  1642. }
  1643. function flag($mailbox, $messages, $flag) {
  1644. return $this->modFlag($mailbox, $messages, $flag, '+');
  1645. }
  1646. function unflag($mailbox, $messages, $flag) {
  1647. return $this->modFlag($mailbox, $messages, $flag, '-');
  1648. }
  1649. function delete($mailbox, $messages) {
  1650. return $this->modFlag($mailbox, $messages, 'DELETED', '+');
  1651. }
  1652. function copy($messages, $from, $to)
  1653. {
  1654. if (!$this->select($from)) {
  1655. return false;
  1656. }
  1657. // Clear internal status cache
  1658. unset($this->data['STATUS:'.$to]);
  1659. $result = $this->execute('UID COPY', array(
  1660. $this->compressMessageSet($messages), $this->escape($to)),
  1661. self::COMMAND_NORESPONSE);
  1662. return ($result == self::ERROR_OK);
  1663. }
  1664. function move($messages, $from, $to)
  1665. {
  1666. if (!$this->select($from)) {
  1667. return false;
  1668. }
  1669. if (!$this->data['READ-WRITE']) {
  1670. $this->setError(self::ERROR_READONLY, "Mailbox is read-only", 'STORE');
  1671. return false;
  1672. }
  1673. $r = $this->copy($messages, $from, $to);
  1674. if ($r) {
  1675. // Clear internal status cache
  1676. unset($this->data['STATUS:'.$from]);
  1677. return $this->delete($from, $messages);
  1678. }
  1679. return $r;
  1680. }
  1681. // Don't be tempted to change $str to pass by reference to speed this up - it will slow it down by about
  1682. // 7 times instead :-) See comments on http://uk2.php.net/references and this article:
  1683. // http://derickrethans.nl/files/phparch-php-variables-article.pdf
  1684. private function parseThread($str, $begin, $end, $root, $parent, $depth, &$depthmap, &$haschildren)
  1685. {
  1686. $node = array();
  1687. if ($str[$begin] != '(') {
  1688. $stop = $begin + strspn($str, '1234567890', $begin, $end - $begin);
  1689. $msg = substr($str, $begin, $stop - $begin);
  1690. if ($msg == 0)
  1691. return $node;
  1692. if (is_null($root))
  1693. $root = $msg;
  1694. $depthmap[$msg] = $depth;
  1695. $haschildren[$msg] = false;
  1696. if (!is_null($parent))
  1697. $haschildren[$parent] = true;
  1698. if ($stop + 1 < $end)
  1699. $node[$msg] = $this->parseThread($str, $stop + 1, $end, $root, $msg, $depth + 1, $depthmap, $haschildren);
  1700. else
  1701. $node[$msg] = array();
  1702. } else {
  1703. $off = $begin;
  1704. while ($off < $end) {
  1705. $start = $off;
  1706. $off++;
  1707. $n = 1;
  1708. while ($n > 0) {
  1709. $p = strpos($str, ')', $off);
  1710. if ($p === false) {
  1711. error_log("Mismatched brackets parsing IMAP THREAD response:");
  1712. error_log(substr($str, ($begin < 10) ? 0 : ($begin - 10), $end - $begin + 20));
  1713. error_log(str_repeat(' ', $off - (($begin < 10) ? 0 : ($begin - 10))));
  1714. return $node;
  1715. }
  1716. $p1 = strpos($str, '(', $off);
  1717. if ($p1 !== false && $p1 < $p) {
  1718. $off = $p1 + 1;
  1719. $n++;
  1720. } else {
  1721. $off = $p + 1;
  1722. $n--;
  1723. }
  1724. }
  1725. $node += $this->parseThread($str, $start + 1, $off - 1, $root, $parent, $depth, $depthmap, $haschildren);
  1726. }
  1727. }
  1728. return $node;
  1729. }
  1730. function thread($mailbox, $algorithm='REFERENCES', $criteria='', $encoding='US-ASCII')
  1731. {
  1732. $old_sel = $this->selected;
  1733. if (!$this->select($mailbox)) {
  1734. return false;
  1735. }
  1736. // return empty result when folder is empty and we're just after SELECT
  1737. if ($old_sel != $mailbox && !$this->data['EXISTS']) {
  1738. return array(array(), array(), array());
  1739. }
  1740. $encoding = $encoding ? trim($encoding) : 'US-ASCII';
  1741. $algorithm = $algorithm ? trim($algorithm) : 'REFERENCES';
  1742. $criteria = $criteria ? 'ALL '.trim($criteria) : 'ALL';
  1743. $data = '';
  1744. list($code, $response) = $this->execute('THREAD', array(
  1745. $algorithm, $encoding, $criteria));
  1746. if ($code == self::ERROR_OK) {
  1747. // remove prefix...
  1748. $response = substr($response, stripos($response, '* THREAD') + 9);
  1749. // ...unilateral untagged server responses
  1750. if ($pos = strpos($response, '*')) {
  1751. $response = substr($response, 0, $pos);
  1752. }
  1753. $response = str_replace("\r\n", '', $response);
  1754. $depthmap = array();
  1755. $haschildren = array();
  1756. $tree = $this->parseThread($response, 0, strlen($response),
  1757. null, null, 0, $depthmap, $haschildren);
  1758. return array($tree, $depthmap, $haschildren);
  1759. }
  1760. return false;
  1761. }
  1762. /**
  1763. * Executes SEARCH command
  1764. *
  1765. * @param string $mailbox Mailbox name
  1766. * @param string $criteria Searching criteria
  1767. * @param bool $return_uid Enable UID in result instead of sequence ID
  1768. * @param array $items Return items (MIN, MAX, COUNT, ALL)
  1769. *
  1770. * @return array Message identifiers or item-value hash
  1771. */
  1772. function search($mailbox, $criteria, $return_uid=false, $items=array())
  1773. {
  1774. $old_sel = $this->selected;
  1775. if (!$this->select($mailbox)) {
  1776. return false;
  1777. }
  1778. // return empty result when folder is empty and we're just after SELECT
  1779. if ($old_sel != $mailbox && !$this->data['EXISTS']) {
  1780. if (!empty($items))
  1781. return array_combine($items, array_fill(0, count($items), 0));
  1782. else
  1783. return array();
  1784. }
  1785. $esearch = empty($items) ? false : $this->getCapability('ESEARCH');
  1786. $criteria = trim($criteria);
  1787. $params = '';
  1788. // RFC4731: ESEARCH
  1789. if (!empty($items) && $esearch) {
  1790. $params .= 'RETURN (' . implode(' ', $items) . ')';
  1791. }
  1792. if (!empty($criteria)) {
  1793. $modseq = stripos($criteria, 'MODSEQ') !== false;
  1794. $params .= ($params ? ' ' : '') . $criteria;
  1795. }
  1796. else {
  1797. $params .= 'ALL';
  1798. }
  1799. list($code, $response) = $this->execute($return_uid ? 'UID SEARCH' : 'SEARCH',
  1800. array($params));
  1801. if ($code == self::ERROR_OK) {
  1802. // remove prefix...
  1803. $response = substr($response, stripos($response,
  1804. $esearch ? '* ESEARCH' : '* SEARCH') + ($esearch ? 10 : 9));
  1805. // ...and unilateral untagged server responses
  1806. if ($pos = strpos($response, '*')) {
  1807. $response = rtrim(substr($response, 0, $pos));
  1808. }
  1809. // remove MODSEQ response
  1810. if ($modseq) {
  1811. if (preg_match('/\(MODSEQ ([0-9]+)\)$/', $response, $m)) {
  1812. $response = substr($response, 0, -strlen($m[0]));
  1813. }
  1814. }
  1815. if ($esearch) {
  1816. // Skip prefix: ... (TAG "A285") UID ...
  1817. $this->tokenizeResponse($response, $return_uid ? 2 : 1);
  1818. $result = array();
  1819. for ($i=0; $i<count($items); $i++) {
  1820. // If the SEARCH returns no matches, the server MUST NOT
  1821. // include the item result option in the ESEARCH response
  1822. if ($ret = $this->tokenizeResponse($response, 2)) {
  1823. list ($name, $value) = $ret;
  1824. $result[$name] = $value;
  1825. }
  1826. }
  1827. return $result;
  1828. }
  1829. else {
  1830. $response = preg_split('/[\s\r\n]+/', $response, -1, PREG_SPLIT_NO_EMPTY);
  1831. if (!empty($items)) {
  1832. $result = array();
  1833. if (in_array('COUNT', $items)) {
  1834. $result['COUNT'] = count($response);
  1835. }
  1836. if (in_array('MIN', $items)) {
  1837. $result['MIN'] = !empty($response) ? min($response) : 0;
  1838. }
  1839. if (in_array('MAX', $items)) {
  1840. $result['MAX'] = !empty($response) ? max($response) : 0;
  1841. }
  1842. if (in_array('ALL', $items)) {
  1843. $result['ALL'] = $this->compressMessageSet($response, true);
  1844. }
  1845. return $result;
  1846. }
  1847. else {
  1848. return $response;
  1849. }
  1850. }
  1851. }
  1852. return false;
  1853. }
  1854. /**
  1855. * Returns list of mailboxes
  1856. *
  1857. * @param string $ref Reference name
  1858. * @param string $mailbox Mailbox name
  1859. * @param array $status_opts (see self::_listMailboxes)
  1860. * @param array $select_opts (see self::_listMailboxes)
  1861. *
  1862. * @return array List of mailboxes or hash of options if $status_opts argument
  1863. * is non-empty.
  1864. */
  1865. function listMailboxes($ref, $mailbox, $status_opts=array(), $select_opts=array())
  1866. {
  1867. return $this->_listMailboxes($ref, $mailbox, false, $status_opts, $select_opts);
  1868. }
  1869. /**
  1870. * Returns list of subscribed mailboxes
  1871. *
  1872. * @param string $ref Reference name
  1873. * @param string $mailbox Mailbox name
  1874. * @param array $status_opts (see self::_listMailboxes)
  1875. *
  1876. * @return array List of mailboxes or hash of options if $status_opts argument
  1877. * is non-empty.
  1878. */
  1879. function listSubscribed($ref, $mailbox, $status_opts=array())
  1880. {
  1881. return $this->_listMailboxes($ref, $mailbox, true, $status_opts, NULL);
  1882. }
  1883. /**
  1884. * IMAP LIST/LSUB command
  1885. *
  1886. * @param string $ref Reference name
  1887. * @param string $mailbox Mailbox name
  1888. * @param bool $subscribed Enables returning subscribed mailboxes only
  1889. * @param array $status_opts List of STATUS options (RFC5819: LIST-STATUS)
  1890. * Possible: MESSAGES, RECENT, UIDNEXT, UIDVALIDITY, UNSEEN
  1891. * @param array $select_opts List of selection options (RFC5258: LIST-EXTENDED)
  1892. * Possible: SUBSCRIBED, RECURSIVEMATCH, REMOTE
  1893. *
  1894. * @return array List of mailboxes or hash of options if $status_ops argument
  1895. * is non-empty.
  1896. */
  1897. private function _listMailboxes($ref, $mailbox, $subscribed=false,
  1898. $status_opts=array(), $select_opts=array())
  1899. {
  1900. if (!strlen($mailbox)) {
  1901. $mailbox = '*';
  1902. }
  1903. $args = array();
  1904. if (!empty($select_opts) && $this->getCapability('LIST-EXTENDED')) {
  1905. $select_opts = (array) $select_opts;
  1906. $args[] = '(' . implode(' ', $select_opts) . ')';
  1907. }
  1908. $args[] = $this->escape($ref);
  1909. $args[] = $this->escape($mailbox);
  1910. if (!empty($status_opts) && $this->getCapability('LIST-STATUS')) {
  1911. $status_opts = (array) $status_opts;
  1912. $lstatus = true;
  1913. $args[] = 'RETURN (STATUS (' . implode(' ', $status_opts) . '))';
  1914. }
  1915. list($code, $response) = $this->execute($subscribed ? 'LSUB' : 'LIST', $args);
  1916. if ($code == self::ERROR_OK) {
  1917. $folders = array();
  1918. $last = 0;
  1919. $pos = 0;
  1920. $response .= "\r\n";
  1921. while ($pos = strpos($response, "\r\n", $pos+1)) {
  1922. // literal string, not real end-of-command-line
  1923. if ($response[$pos-1] == '}') {
  1924. continue;
  1925. }
  1926. $line = substr($response, $last, $pos - $last);
  1927. $last = $pos + 2;
  1928. if (!preg_match('/^\* (LIST|LSUB|STATUS) /i', $line, $m)) {
  1929. continue;
  1930. }
  1931. $cmd = strtoupper($m[1]);
  1932. $line = substr($line, strlen($m[0]));
  1933. // * LIST (<options>) <delimiter> <mailbox>
  1934. if ($cmd == 'LIST' || $cmd == 'LSUB') {
  1935. list($opts, $delim, $mailbox) = $this->tokenizeResponse($line, 3);
  1936. // Add to result array
  1937. if (!$lstatus) {
  1938. $folders[] = $mailbox;
  1939. }
  1940. else {
  1941. $folders[$mailbox] = array();
  1942. }
  1943. // store LSUB options only if not empty, this way
  1944. // we can detect a situation when LIST doesn't return specified folder
  1945. if (!empty($opts) || $cmd == 'LIST') {
  1946. // Add to options array
  1947. if (empty($this->data['LIST'][$mailbox]))
  1948. $this->data['LIST'][$mailbox] = $opts;
  1949. else if (!empty($opts))
  1950. $this->data['LIST'][$mailbox] = array_unique(array_merge(
  1951. $this->data['LIST'][$mailbox], $opts));
  1952. }
  1953. }
  1954. // * STATUS <mailbox> (<result>)
  1955. else if ($cmd == 'STATUS') {
  1956. list($mailbox, $status) = $this->tokenizeResponse($line, 2);
  1957. for ($i=0, $len=count($status); $i<$len; $i += 2) {
  1958. list($name, $value) = $this->tokenizeResponse($status, 2);
  1959. $folders[$mailbox][$name] = $value;
  1960. }
  1961. }
  1962. }
  1963. return $folders;
  1964. }
  1965. return false;
  1966. }
  1967. function fetchMIMEHeaders($mailbox, $uid, $parts, $mime=true)
  1968. {
  1969. if (!$this->select($mailbox)) {
  1970. return false;
  1971. }
  1972. $result = false;
  1973. $parts = (array) $parts;
  1974. $key = $this->nextTag();
  1975. $peeks = array();
  1976. $type = $mime ? 'MIME' : 'HEADER';
  1977. // format request
  1978. foreach ($parts as $part) {
  1979. $peeks[] = "BODY.PEEK[$part.$type]";
  1980. }
  1981. $request = "$key UID FETCH $uid (" . implode(' ', $peeks) . ')';
  1982. // send request
  1983. if (!$this->putLine($request)) {
  1984. $this->setError(self::ERROR_COMMAND, "Unable to send command: $request");
  1985. return false;
  1986. }
  1987. do {
  1988. $line = $this->readLine(1024);
  1989. if (preg_match('/^\* [0-9]+ FETCH [0-9UID( ]+BODY\[([0-9\.]+)\.'.$type.'\]/', $line, $matches)) {
  1990. $idx = $matches[1];
  1991. $headers = '';
  1992. // get complete entry
  1993. if (preg_match('/\{([0-9]+)\}\r\n$/', $line, $m)) {
  1994. $bytes = $m[1];
  1995. $out = '';
  1996. while (strlen($out) < $bytes) {
  1997. $out = $this->readBytes($bytes);
  1998. if ($out === null)
  1999. break;
  2000. $headers .= $out;
  2001. }
  2002. }
  2003. $result[$idx] = trim($headers);
  2004. }
  2005. } while (!$this->startsWith($line, $key, true));
  2006. return $result;
  2007. }
  2008. function fetchPartHeader($mailbox, $id, $is_uid=false, $part=NULL)
  2009. {
  2010. $part = empty($part) ? 'HEADER' : $part.'.MIME';
  2011. return $this->handlePartBody($mailbox, $id, $is_uid, $part);
  2012. }
  2013. function handlePartBody($mailbox, $id, $is_uid=false, $part='', $encoding=NULL, $print=NULL, $file=NULL)
  2014. {
  2015. if (!$this->select($mailbox)) {
  2016. return false;
  2017. }
  2018. switch ($encoding) {
  2019. case 'base64':
  2020. $mode = 1;
  2021. break;
  2022. case 'quoted-printable':
  2023. $mode = 2;
  2024. break;
  2025. case 'x-uuencode':
  2026. case 'x-uue':
  2027. case 'uue':
  2028. case 'uuencode':
  2029. $mode = 3;
  2030. break;
  2031. default:
  2032. $mode = 0;
  2033. }
  2034. // format request
  2035. $reply_key = '* ' . $id;
  2036. $key = $this->nextTag();
  2037. $request = $key . ($is_uid ? ' UID' : '') . " FETCH $id (BODY.PEEK[$part])";
  2038. // send request
  2039. if (!$this->putLine($request)) {
  2040. $this->setError(self::ERROR_COMMAND, "Unable to send command: $request");
  2041. return false;
  2042. }
  2043. // receive reply line
  2044. do {
  2045. $line = rtrim($this->readLine(1024));
  2046. $a = explode(' ', $line);
  2047. } while (!($end = $this->startsWith($line, $key, true)) && $a[2] != 'FETCH');
  2048. $len = strlen($line);
  2049. $result = false;
  2050. if ($a[2] != 'FETCH') {
  2051. }
  2052. // handle empty "* X FETCH ()" response
  2053. else if ($line[$len-1] == ')' && $line[$len-2] != '(') {
  2054. // one line response, get everything between first and last quotes
  2055. if (substr($line, -4, 3) == 'NIL') {
  2056. // NIL response
  2057. $result = '';
  2058. } else {
  2059. $from = strpos($line, '"') + 1;
  2060. $to = strrpos($line, '"');
  2061. $len = $to - $from;
  2062. $result = substr($line, $from, $len);
  2063. }
  2064. if ($mode == 1) {
  2065. $result = base64_decode($result);
  2066. }
  2067. else if ($mode == 2) {
  2068. $result = quoted_printable_decode($result);
  2069. }
  2070. else if ($mode == 3) {
  2071. $result = convert_uudecode($result);
  2072. }
  2073. } else if ($line[$len-1] == '}') {
  2074. // multi-line request, find sizes of content and receive that many bytes
  2075. $from = strpos($line, '{') + 1;
  2076. $to = strrpos($line, '}');
  2077. $len = $to - $from;
  2078. $sizeStr = substr($line, $from, $len);
  2079. $bytes = (int)$sizeStr;
  2080. $prev = '';
  2081. while ($bytes > 0) {
  2082. $line = $this->readLine(4096);
  2083. if ($line === NULL) {
  2084. break;
  2085. }
  2086. $len = strlen($line);
  2087. if ($len > $bytes) {
  2088. $line = substr($line, 0, $bytes);
  2089. $len = strlen($line);
  2090. }
  2091. $bytes -= $len;
  2092. // BASE64
  2093. if ($mode == 1) {
  2094. $line = rtrim($line, "\t\r\n\0\x0B");
  2095. // create chunks with proper length for base64 decoding
  2096. $line = $prev.$line;
  2097. $length = strlen($line);
  2098. if ($length % 4) {
  2099. $length = floor($length / 4) * 4;
  2100. $prev = substr($line, $length);
  2101. $line = substr($line, 0, $length);
  2102. }
  2103. else
  2104. $prev = '';
  2105. $line = base64_decode($line);
  2106. // QUOTED-PRINTABLE
  2107. } else if ($mode == 2) {
  2108. $line = rtrim($line, "\t\r\0\x0B");
  2109. $line = quoted_printable_decode($line);
  2110. // UUENCODE
  2111. } else if ($mode == 3) {
  2112. $line = rtrim($line, "\t\r\n\0\x0B");
  2113. if ($line == 'end' || preg_match('/^begin\s+[0-7]+\s+.+$/', $line))
  2114. continue;
  2115. $line = convert_uudecode($line);
  2116. // default
  2117. } else {
  2118. $line = rtrim($line, "\t\r\n\0\x0B") . "\n";
  2119. }
  2120. if ($file)
  2121. fwrite($file, $line);
  2122. else if ($print)
  2123. echo $line;
  2124. else
  2125. $result .= $line;
  2126. }
  2127. }
  2128. // read in anything up until last line
  2129. if (!$end)
  2130. do {
  2131. $line = $this->readLine(1024);
  2132. } while (!$this->startsWith($line, $key, true));
  2133. if ($result !== false) {
  2134. if ($file) {
  2135. fwrite($file, $result);
  2136. } else if ($print) {
  2137. echo $result;
  2138. } else
  2139. return $result;
  2140. return true;
  2141. }
  2142. return false;
  2143. }
  2144. function createFolder($mailbox)
  2145. {
  2146. $result = $this->execute('CREATE', array($this->escape($mailbox)),
  2147. self::COMMAND_NORESPONSE);
  2148. return ($result == self::ERROR_OK);
  2149. }
  2150. function renameFolder($from, $to)
  2151. {
  2152. $result = $this->execute('RENAME', array($this->escape($from), $this->escape($to)),
  2153. self::COMMAND_NORESPONSE);
  2154. return ($result == self::ERROR_OK);
  2155. }
  2156. /**
  2157. * Handler for IMAP APPEND command
  2158. *
  2159. * @param string $mailbox Mailbox name
  2160. * @param string $message Message content
  2161. *
  2162. * @return string|bool On success APPENDUID response (if available) or True, False on failure
  2163. */
  2164. function append($mailbox, &$message)
  2165. {
  2166. unset($this->data['APPENDUID']);
  2167. if (!$mailbox) {
  2168. return false;
  2169. }
  2170. $message = str_replace("\r", '', $message);
  2171. $message = str_replace("\n", "\r\n", $message);
  2172. $len = strlen($message);
  2173. if (!$len) {
  2174. return false;
  2175. }
  2176. $key = $this->nextTag();
  2177. $request = sprintf("$key APPEND %s (\\Seen) {%d%s}", $this->escape($mailbox),
  2178. $len, ($this->prefs['literal+'] ? '+' : ''));
  2179. if ($this->putLine($request)) {
  2180. // Don't wait when LITERAL+ is supported
  2181. if (!$this->prefs['literal+']) {
  2182. $line = $this->readReply();
  2183. if ($line[0] != '+') {
  2184. $this->parseResult($line, 'APPEND: ');
  2185. return false;
  2186. }
  2187. }
  2188. if (!$this->putLine($message)) {
  2189. return false;
  2190. }
  2191. do {
  2192. $line = $this->readLine();
  2193. } while (!$this->startsWith($line, $key, true, true));
  2194. // Clear internal status cache
  2195. unset($this->data['STATUS:'.$mailbox]);
  2196. if ($this->parseResult($line, 'APPEND: ') != self::ERROR_OK)
  2197. return false;
  2198. else if (!empty($this->data['APPENDUID']))
  2199. return $this->data['APPENDUID'];
  2200. else
  2201. return true;
  2202. }
  2203. else {
  2204. $this->setError(self::ERROR_COMMAND, "Unable to send command: $request");
  2205. }
  2206. return false;
  2207. }
  2208. /**
  2209. * Handler for IMAP APPEND command.
  2210. *
  2211. * @param string $mailbox Mailbox name
  2212. * @param string $path Path to the file with message body
  2213. * @param string $headers Message headers
  2214. *
  2215. * @return string|bool On success APPENDUID response (if available) or True, False on failure
  2216. */
  2217. function appendFromFile($mailbox, $path, $headers=null)
  2218. {
  2219. unset($this->data['APPENDUID']);
  2220. if (!$mailbox) {
  2221. return false;
  2222. }
  2223. // open message file
  2224. $in_fp = false;
  2225. if (file_exists(realpath($path))) {
  2226. $in_fp = fopen($path, 'r');
  2227. }
  2228. if (!$in_fp) {
  2229. $this->setError(self::ERROR_UNKNOWN, "Couldn't open $path for reading");
  2230. return false;
  2231. }
  2232. $body_separator = "\r\n\r\n";
  2233. $len = filesize($path);
  2234. if (!$len) {
  2235. return false;
  2236. }
  2237. if ($headers) {
  2238. $headers = preg_replace('/[\r\n]+$/', '', $headers);
  2239. $len += strlen($headers) + strlen($body_separator);
  2240. }
  2241. // send APPEND command
  2242. $key = $this->nextTag();
  2243. $request = sprintf("$key APPEND %s (\\Seen) {%d%s}", $this->escape($mailbox),
  2244. $len, ($this->prefs['literal+'] ? '+' : ''));
  2245. if ($this->putLine($request)) {
  2246. // Don't wait when LITERAL+ is supported
  2247. if (!$this->prefs['literal+']) {
  2248. $line = $this->readReply();
  2249. if ($line[0] != '+') {
  2250. $this->parseResult($line, 'APPEND: ');
  2251. return false;
  2252. }
  2253. }
  2254. // send headers with body separator
  2255. if ($headers) {
  2256. $this->putLine($headers . $body_separator, false);
  2257. }
  2258. // send file
  2259. while (!feof($in_fp) && $this->fp) {
  2260. $buffer = fgets($in_fp, 4096);
  2261. $this->putLine($buffer, false);
  2262. }
  2263. fclose($in_fp);
  2264. if (!$this->putLine('')) { // \r\n
  2265. return false;
  2266. }
  2267. // read response
  2268. do {
  2269. $line = $this->readLine();
  2270. } while (!$this->startsWith($line, $key, true, true));
  2271. // Clear internal status cache
  2272. unset($this->data['STATUS:'.$mailbox]);
  2273. if ($this->parseResult($line, 'APPEND: ') != self::ERROR_OK)
  2274. return false;
  2275. else if (!empty($this->data['APPENDUID']))
  2276. return $this->data['APPENDUID'];
  2277. else
  2278. return true;
  2279. }
  2280. else {
  2281. $this->setError(self::ERROR_COMMAND, "Unable to send command: $request");
  2282. }
  2283. return false;
  2284. }
  2285. function getQuota()
  2286. {
  2287. /*
  2288. * GETQUOTAROOT "INBOX"
  2289. * QUOTAROOT INBOX user/rchijiiwa1
  2290. * QUOTA user/rchijiiwa1 (STORAGE 654 9765)
  2291. * OK Completed
  2292. */
  2293. $result = false;
  2294. $quota_lines = array();
  2295. $key = $this->nextTag();
  2296. $command = $key . ' GETQUOTAROOT INBOX';
  2297. // get line(s) containing quota info
  2298. if ($this->putLine($command)) {
  2299. do {
  2300. $line = rtrim($this->readLine(5000));
  2301. if (preg_match('/^\* QUOTA /', $line)) {
  2302. $quota_lines[] = $line;
  2303. }
  2304. } while (!$this->startsWith($line, $key, true, true));
  2305. }
  2306. else {
  2307. $this->setError(self::ERROR_COMMAND, "Unable to send command: $command");
  2308. }
  2309. // return false if not found, parse if found
  2310. $min_free = PHP_INT_MAX;
  2311. foreach ($quota_lines as $key => $quota_line) {
  2312. $quota_line = str_replace(array('(', ')'), '', $quota_line);
  2313. $parts = explode(' ', $quota_line);
  2314. $storage_part = array_search('STORAGE', $parts);
  2315. if (!$storage_part) {
  2316. continue;
  2317. }
  2318. $used = intval($parts[$storage_part+1]);
  2319. $total = intval($parts[$storage_part+2]);
  2320. $free = $total - $used;
  2321. // return lowest available space from all quotas
  2322. if ($free < $min_free) {
  2323. $min_free = $free;
  2324. $result['used'] = $used;
  2325. $result['total'] = $total;
  2326. $result['percent'] = min(100, round(($used/max(1,$total))*100));
  2327. $result['free'] = 100 - $result['percent'];
  2328. }
  2329. }
  2330. return $result;
  2331. }
  2332. /**
  2333. * Send the SETACL command (RFC4314)
  2334. *
  2335. * @param string $mailbox Mailbox name
  2336. * @param string $user User name
  2337. * @param mixed $acl ACL string or array
  2338. *
  2339. * @return boolean True on success, False on failure
  2340. *
  2341. * @since 0.5-beta
  2342. */
  2343. function setACL($mailbox, $user, $acl)
  2344. {
  2345. if (is_array($acl)) {
  2346. $acl = implode('', $acl);
  2347. }
  2348. $result = $this->execute('SETACL', array(
  2349. $this->escape($mailbox), $this->escape($user), strtolower($acl)),
  2350. self::COMMAND_NORESPONSE);
  2351. return ($result == self::ERROR_OK);
  2352. }
  2353. /**
  2354. * Send the DELETEACL command (RFC4314)
  2355. *
  2356. * @param string $mailbox Mailbox name
  2357. * @param string $user User name
  2358. *
  2359. * @return boolean True on success, False on failure
  2360. *
  2361. * @since 0.5-beta
  2362. */
  2363. function deleteACL($mailbox, $user)
  2364. {
  2365. $result = $this->execute('DELETEACL', array(
  2366. $this->escape($mailbox), $this->escape($user)),
  2367. self::COMMAND_NORESPONSE);
  2368. return ($result == self::ERROR_OK);
  2369. }
  2370. /**
  2371. * Send the GETACL command (RFC4314)
  2372. *
  2373. * @param string $mailbox Mailbox name
  2374. *
  2375. * @return array User-rights array on success, NULL on error
  2376. * @since 0.5-beta
  2377. */
  2378. function getACL($mailbox)
  2379. {
  2380. list($code, $response) = $this->execute('GETACL', array($this->escape($mailbox)));
  2381. if ($code == self::ERROR_OK && preg_match('/^\* ACL /i', $response)) {
  2382. // Parse server response (remove "* ACL ")
  2383. $response = substr($response, 6);
  2384. $ret = $this->tokenizeResponse($response);
  2385. $mbox = array_shift($ret);
  2386. $size = count($ret);
  2387. // Create user-rights hash array
  2388. // @TODO: consider implementing fixACL() method according to RFC4314.2.1.1
  2389. // so we could return only standard rights defined in RFC4314,
  2390. // excluding 'c' and 'd' defined in RFC2086.
  2391. if ($size % 2 == 0) {
  2392. for ($i=0; $i<$size; $i++) {
  2393. $ret[$ret[$i]] = str_split($ret[++$i]);
  2394. unset($ret[$i-1]);
  2395. unset($ret[$i]);
  2396. }
  2397. return $ret;
  2398. }
  2399. $this->setError(self::ERROR_COMMAND, "Incomplete ACL response");
  2400. return NULL;
  2401. }
  2402. return NULL;
  2403. }
  2404. /**
  2405. * Send the LISTRIGHTS command (RFC4314)
  2406. *
  2407. * @param string $mailbox Mailbox name
  2408. * @param string $user User name
  2409. *
  2410. * @return array List of user rights
  2411. * @since 0.5-beta
  2412. */
  2413. function listRights($mailbox, $user)
  2414. {
  2415. list($code, $response) = $this->execute('LISTRIGHTS', array(
  2416. $this->escape($mailbox), $this->escape($user)));
  2417. if ($code == self::ERROR_OK && preg_match('/^\* LISTRIGHTS /i', $response)) {
  2418. // Parse server response (remove "* LISTRIGHTS ")
  2419. $response = substr($response, 13);
  2420. $ret_mbox = $this->tokenizeResponse($response, 1);
  2421. $ret_user = $this->tokenizeResponse($response, 1);
  2422. $granted = $this->tokenizeResponse($response, 1);
  2423. $optional = trim($response);
  2424. return array(
  2425. 'granted' => str_split($granted),
  2426. 'optional' => explode(' ', $optional),
  2427. );
  2428. }
  2429. return NULL;
  2430. }
  2431. /**
  2432. * Send the MYRIGHTS command (RFC4314)
  2433. *
  2434. * @param string $mailbox Mailbox name
  2435. *
  2436. * @return array MYRIGHTS response on success, NULL on error
  2437. * @since 0.5-beta
  2438. */
  2439. function myRights($mailbox)
  2440. {
  2441. list($code, $response) = $this->execute('MYRIGHTS', array($this->escape($mailbox)));
  2442. if ($code == self::ERROR_OK && preg_match('/^\* MYRIGHTS /i', $response)) {
  2443. // Parse server response (remove "* MYRIGHTS ")
  2444. $response = substr($response, 11);
  2445. $ret_mbox = $this->tokenizeResponse($response, 1);
  2446. $rights = $this->tokenizeResponse($response, 1);
  2447. return str_split($rights);
  2448. }
  2449. return NULL;
  2450. }
  2451. /**
  2452. * Send the SETMETADATA command (RFC5464)
  2453. *
  2454. * @param string $mailbox Mailbox name
  2455. * @param array $entries Entry-value array (use NULL value as NIL)
  2456. *
  2457. * @return boolean True on success, False on failure
  2458. * @since 0.5-beta
  2459. */
  2460. function setMetadata($mailbox, $entries)
  2461. {
  2462. if (!is_array($entries) || empty($entries)) {
  2463. $this->setError(self::ERROR_COMMAND, "Wrong argument for SETMETADATA command");
  2464. return false;
  2465. }
  2466. foreach ($entries as $name => $value) {
  2467. $entries[$name] = $this->escape($name) . ' ' . $this->escape($value);
  2468. }
  2469. $entries = implode(' ', $entries);
  2470. $result = $this->execute('SETMETADATA', array(
  2471. $this->escape($mailbox), '(' . $entries . ')'),
  2472. self::COMMAND_NORESPONSE);
  2473. return ($result == self::ERROR_OK);
  2474. }
  2475. /**
  2476. * Send the SETMETADATA command with NIL values (RFC5464)
  2477. *
  2478. * @param string $mailbox Mailbox name
  2479. * @param array $entries Entry names array
  2480. *
  2481. * @return boolean True on success, False on failure
  2482. *
  2483. * @since 0.5-beta
  2484. */
  2485. function deleteMetadata($mailbox, $entries)
  2486. {
  2487. if (!is_array($entries) && !empty($entries)) {
  2488. $entries = explode(' ', $entries);
  2489. }
  2490. if (empty($entries)) {
  2491. $this->setError(self::ERROR_COMMAND, "Wrong argument for SETMETADATA command");
  2492. return false;
  2493. }
  2494. foreach ($entries as $entry) {
  2495. $data[$entry] = NULL;
  2496. }
  2497. return $this->setMetadata($mailbox, $data);
  2498. }
  2499. /**
  2500. * Send the GETMETADATA command (RFC5464)
  2501. *
  2502. * @param string $mailbox Mailbox name
  2503. * @param array $entries Entries
  2504. * @param array $options Command options (with MAXSIZE and DEPTH keys)
  2505. *
  2506. * @return array GETMETADATA result on success, NULL on error
  2507. *
  2508. * @since 0.5-beta
  2509. */
  2510. function getMetadata($mailbox, $entries, $options=array())
  2511. {
  2512. if (!is_array($entries)) {
  2513. $entries = array($entries);
  2514. }
  2515. // create entries string
  2516. foreach ($entries as $idx => $name) {
  2517. $entries[$idx] = $this->escape($name);
  2518. }
  2519. $optlist = '';
  2520. $entlist = '(' . implode(' ', $entries) . ')';
  2521. // create options string
  2522. if (is_array($options)) {
  2523. $options = array_change_key_case($options, CASE_UPPER);
  2524. $opts = array();
  2525. if (!empty($options['MAXSIZE'])) {
  2526. $opts[] = 'MAXSIZE '.intval($options['MAXSIZE']);
  2527. }
  2528. if (!empty($options['DEPTH'])) {
  2529. $opts[] = 'DEPTH '.intval($options['DEPTH']);
  2530. }
  2531. if ($opts) {
  2532. $optlist = '(' . implode(' ', $opts) . ')';
  2533. }
  2534. }
  2535. $optlist .= ($optlist ? ' ' : '') . $entlist;
  2536. list($code, $response) = $this->execute('GETMETADATA', array(
  2537. $this->escape($mailbox), $optlist));
  2538. if ($code == self::ERROR_OK) {
  2539. $result = array();
  2540. $data = $this->tokenizeResponse($response);
  2541. // The METADATA response can contain multiple entries in a single
  2542. // response or multiple responses for each entry or group of entries
  2543. if (!empty($data) && ($size = count($data))) {
  2544. for ($i=0; $i<$size; $i++) {
  2545. if (isset($mbox) && is_array($data[$i])) {
  2546. $size_sub = count($data[$i]);
  2547. for ($x=0; $x<$size_sub; $x++) {
  2548. $result[$mbox][$data[$i][$x]] = $data[$i][++$x];
  2549. }
  2550. unset($data[$i]);
  2551. }
  2552. else if ($data[$i] == '*') {
  2553. if ($data[$i+1] == 'METADATA') {
  2554. $mbox = $data[$i+2];
  2555. unset($data[$i]); // "*"
  2556. unset($data[++$i]); // "METADATA"
  2557. unset($data[++$i]); // Mailbox
  2558. }
  2559. // get rid of other untagged responses
  2560. else {
  2561. unset($mbox);
  2562. unset($data[$i]);
  2563. }
  2564. }
  2565. else if (isset($mbox)) {
  2566. $result[$mbox][$data[$i]] = $data[++$i];
  2567. unset($data[$i]);
  2568. unset($data[$i-1]);
  2569. }
  2570. else {
  2571. unset($data[$i]);
  2572. }
  2573. }
  2574. }
  2575. return $result;
  2576. }
  2577. return NULL;
  2578. }
  2579. /**
  2580. * Send the SETANNOTATION command (draft-daboo-imap-annotatemore)
  2581. *
  2582. * @param string $mailbox Mailbox name
  2583. * @param array $data Data array where each item is an array with
  2584. * three elements: entry name, attribute name, value
  2585. *
  2586. * @return boolean True on success, False on failure
  2587. * @since 0.5-beta
  2588. */
  2589. function setAnnotation($mailbox, $data)
  2590. {
  2591. if (!is_array($data) || empty($data)) {
  2592. $this->setError(self::ERROR_COMMAND, "Wrong argument for SETANNOTATION command");
  2593. return false;
  2594. }
  2595. foreach ($data as $entry) {
  2596. // ANNOTATEMORE drafts before version 08 require quoted parameters
  2597. $entries[] = sprintf('%s (%s %s)', $this->escape($entry[0], true),
  2598. $this->escape($entry[1], true), $this->escape($entry[2], true));
  2599. }
  2600. $entries = implode(' ', $entries);
  2601. $result = $this->execute('SETANNOTATION', array(
  2602. $this->escape($mailbox), $entries), self::COMMAND_NORESPONSE);
  2603. return ($result == self::ERROR_OK);
  2604. }
  2605. /**
  2606. * Send the SETANNOTATION command with NIL values (draft-daboo-imap-annotatemore)
  2607. *
  2608. * @param string $mailbox Mailbox name
  2609. * @param array $data Data array where each item is an array with
  2610. * two elements: entry name and attribute name
  2611. *
  2612. * @return boolean True on success, False on failure
  2613. *
  2614. * @since 0.5-beta
  2615. */
  2616. function deleteAnnotation($mailbox, $data)
  2617. {
  2618. if (!is_array($data) || empty($data)) {
  2619. $this->setError(self::ERROR_COMMAND, "Wrong argument for SETANNOTATION command");
  2620. return false;
  2621. }
  2622. return $this->setAnnotation($mailbox, $data);
  2623. }
  2624. /**
  2625. * Send the GETANNOTATION command (draft-daboo-imap-annotatemore)
  2626. *
  2627. * @param string $mailbox Mailbox name
  2628. * @param array $entries Entries names
  2629. * @param array $attribs Attribs names
  2630. *
  2631. * @return array Annotations result on success, NULL on error
  2632. *
  2633. * @since 0.5-beta
  2634. */
  2635. function getAnnotation($mailbox, $entries, $attribs)
  2636. {
  2637. if (!is_array($entries)) {
  2638. $entries = array($entries);
  2639. }
  2640. // create entries string
  2641. // ANNOTATEMORE drafts before version 08 require quoted parameters
  2642. foreach ($entries as $idx => $name) {
  2643. $entries[$idx] = $this->escape($name, true);
  2644. }
  2645. $entries = '(' . implode(' ', $entries) . ')';
  2646. if (!is_array($attribs)) {
  2647. $attribs = array($attribs);
  2648. }
  2649. // create entries string
  2650. foreach ($attribs as $idx => $name) {
  2651. $attribs[$idx] = $this->escape($name, true);
  2652. }
  2653. $attribs = '(' . implode(' ', $attribs) . ')';
  2654. list($code, $response) = $this->execute('GETANNOTATION', array(
  2655. $this->escape($mailbox), $entries, $attribs));
  2656. if ($code == self::ERROR_OK) {
  2657. $result = array();
  2658. $data = $this->tokenizeResponse($response);
  2659. // Here we returns only data compatible with METADATA result format
  2660. if (!empty($data) && ($size = count($data))) {
  2661. for ($i=0; $i<$size; $i++) {
  2662. $entry = $data[$i];
  2663. if (isset($mbox) && is_array($entry)) {
  2664. $attribs = $entry;
  2665. $entry = $last_entry;
  2666. }
  2667. else if ($entry == '*') {
  2668. if ($data[$i+1] == 'ANNOTATION') {
  2669. $mbox = $data[$i+2];
  2670. unset($data[$i]); // "*"
  2671. unset($data[++$i]); // "ANNOTATION"
  2672. unset($data[++$i]); // Mailbox
  2673. }
  2674. // get rid of other untagged responses
  2675. else {
  2676. unset($mbox);
  2677. unset($data[$i]);
  2678. }
  2679. continue;
  2680. }
  2681. else if (isset($mbox)) {
  2682. $attribs = $data[++$i];
  2683. }
  2684. else {
  2685. unset($data[$i]);
  2686. continue;
  2687. }
  2688. if (!empty($attribs)) {
  2689. for ($x=0, $len=count($attribs); $x<$len;) {
  2690. $attr = $attribs[$x++];
  2691. $value = $attribs[$x++];
  2692. if ($attr == 'value.priv') {
  2693. $result[$mbox]['/private' . $entry] = $value;
  2694. }
  2695. else if ($attr == 'value.shared') {
  2696. $result[$mbox]['/shared' . $entry] = $value;
  2697. }
  2698. }
  2699. }
  2700. $last_entry = $entry;
  2701. unset($data[$i]);
  2702. }
  2703. }
  2704. return $result;
  2705. }
  2706. return NULL;
  2707. }
  2708. /**
  2709. * Returns BODYSTRUCTURE for the specified message.
  2710. *
  2711. * @param string $mailbox Folder name
  2712. * @param int $id Message sequence number or UID
  2713. * @param bool $is_uid True if $id is an UID
  2714. *
  2715. * @return array/bool Body structure array or False on error.
  2716. * @since 0.6
  2717. */
  2718. function getStructure($mailbox, $id, $is_uid = false)
  2719. {
  2720. $result = $this->fetch($mailbox, $id, $is_uid, array('BODYSTRUCTURE'));
  2721. if (is_array($result)) {
  2722. $result = array_shift($result);
  2723. return $result->bodystructure;
  2724. }
  2725. return false;
  2726. }
  2727. /**
  2728. * Returns data of a message part according to specified structure.
  2729. *
  2730. * @param array $structure Message structure (getStructure() result)
  2731. * @param string $part Message part identifier
  2732. *
  2733. * @return array Part data as hash array (type, encoding, charset, size)
  2734. */
  2735. static function getStructurePartData($structure, $part)
  2736. {
  2737. $part_a = self::getStructurePartArray($structure, $part);
  2738. $data = array();
  2739. if (empty($part_a)) {
  2740. return $data;
  2741. }
  2742. // content-type
  2743. if (is_array($part_a[0])) {
  2744. $data['type'] = 'multipart';
  2745. }
  2746. else {
  2747. $data['type'] = strtolower($part_a[0]);
  2748. // encoding
  2749. $data['encoding'] = strtolower($part_a[5]);
  2750. // charset
  2751. if (is_array($part_a[2])) {
  2752. while (list($key, $val) = each($part_a[2])) {
  2753. if (strcasecmp($val, 'charset') == 0) {
  2754. $data['charset'] = $part_a[2][$key+1];
  2755. break;
  2756. }
  2757. }
  2758. }
  2759. }
  2760. // size
  2761. $data['size'] = intval($part_a[6]);
  2762. return $data;
  2763. }
  2764. static function getStructurePartArray($a, $part)
  2765. {
  2766. if (!is_array($a)) {
  2767. return false;
  2768. }
  2769. if (empty($part)) {
  2770. return $a;
  2771. }
  2772. $ctype = is_string($a[0]) && is_string($a[1]) ? $a[0] . '/' . $a[1] : '';
  2773. if (strcasecmp($ctype, 'message/rfc822') == 0) {
  2774. $a = $a[8];
  2775. }
  2776. if (strpos($part, '.') > 0) {
  2777. $orig_part = $part;
  2778. $pos = strpos($part, '.');
  2779. $rest = substr($orig_part, $pos+1);
  2780. $part = substr($orig_part, 0, $pos);
  2781. return self::getStructurePartArray($a[$part-1], $rest);
  2782. }
  2783. else if ($part > 0) {
  2784. if (is_array($a[$part-1]))
  2785. return $a[$part-1];
  2786. else
  2787. return $a;
  2788. }
  2789. }
  2790. /**
  2791. * Creates next command identifier (tag)
  2792. *
  2793. * @return string Command identifier
  2794. * @since 0.5-beta
  2795. */
  2796. function nextTag()
  2797. {
  2798. $this->cmd_num++;
  2799. $this->cmd_tag = sprintf('A%04d', $this->cmd_num);
  2800. return $this->cmd_tag;
  2801. }
  2802. /**
  2803. * Sends IMAP command and parses result
  2804. *
  2805. * @param string $command IMAP command
  2806. * @param array $arguments Command arguments
  2807. * @param int $options Execution options
  2808. *
  2809. * @return mixed Response code or list of response code and data
  2810. * @since 0.5-beta
  2811. */
  2812. function execute($command, $arguments=array(), $options=0)
  2813. {
  2814. $tag = $this->nextTag();
  2815. $query = $tag . ' ' . $command;
  2816. $noresp = ($options & self::COMMAND_NORESPONSE);
  2817. $response = $noresp ? null : '';
  2818. if (!empty($arguments)) {
  2819. foreach ($arguments as $arg) {
  2820. $query .= ' ' . self::r_implode($arg);
  2821. }
  2822. }
  2823. // Send command
  2824. if (!$this->putLineC($query)) {
  2825. $this->setError(self::ERROR_COMMAND, "Unable to send command: $query");
  2826. return $noresp ? self::ERROR_COMMAND : array(self::ERROR_COMMAND, '');
  2827. }
  2828. // Parse response
  2829. do {
  2830. $line = $this->readLine(4096);
  2831. if ($response !== null) {
  2832. $response .= $line;
  2833. }
  2834. } while (!$this->startsWith($line, $tag . ' ', true, true));
  2835. $code = $this->parseResult($line, $command . ': ');
  2836. // Remove last line from response
  2837. if ($response) {
  2838. $line_len = min(strlen($response), strlen($line) + 2);
  2839. $response = substr($response, 0, -$line_len);
  2840. }
  2841. // optional CAPABILITY response
  2842. if (($options & self::COMMAND_CAPABILITY) && $code == self::ERROR_OK
  2843. && preg_match('/\[CAPABILITY ([^]]+)\]/i', $line, $matches)
  2844. ) {
  2845. $this->parseCapability($matches[1], true);
  2846. }
  2847. // return last line only (without command tag, result and response code)
  2848. if ($line && ($options & self::COMMAND_LASTLINE)) {
  2849. $response = preg_replace("/^$tag (OK|NO|BAD|BYE|PREAUTH)?\s*(\[[a-z-]+\])?\s*/i", '', trim($line));
  2850. }
  2851. return $noresp ? $code : array($code, $response);
  2852. }
  2853. /**
  2854. * Splits IMAP response into string tokens
  2855. *
  2856. * @param string &$str The IMAP's server response
  2857. * @param int $num Number of tokens to return
  2858. *
  2859. * @return mixed Tokens array or string if $num=1
  2860. * @since 0.5-beta
  2861. */
  2862. static function tokenizeResponse(&$str, $num=0)
  2863. {
  2864. $result = array();
  2865. while (!$num || count($result) < $num) {
  2866. // remove spaces from the beginning of the string
  2867. $str = ltrim($str);
  2868. switch ($str[0]) {
  2869. // String literal
  2870. case '{':
  2871. if (($epos = strpos($str, "}\r\n", 1)) == false) {
  2872. // error
  2873. }
  2874. if (!is_numeric(($bytes = substr($str, 1, $epos - 1)))) {
  2875. // error
  2876. }
  2877. $result[] = $bytes ? substr($str, $epos + 3, $bytes) : '';
  2878. // Advance the string
  2879. $str = substr($str, $epos + 3 + $bytes);
  2880. break;
  2881. // Quoted string
  2882. case '"':
  2883. $len = strlen($str);
  2884. for ($pos=1; $pos<$len; $pos++) {
  2885. if ($str[$pos] == '"') {
  2886. break;
  2887. }
  2888. if ($str[$pos] == "\\") {
  2889. if ($str[$pos + 1] == '"' || $str[$pos + 1] == "\\") {
  2890. $pos++;
  2891. }
  2892. }
  2893. }
  2894. if ($str[$pos] != '"') {
  2895. // error
  2896. }
  2897. // we need to strip slashes for a quoted string
  2898. $result[] = stripslashes(substr($str, 1, $pos - 1));
  2899. $str = substr($str, $pos + 1);
  2900. break;
  2901. // Parenthesized list
  2902. case '(':
  2903. case '[':
  2904. $str = substr($str, 1);
  2905. $result[] = self::tokenizeResponse($str);
  2906. break;
  2907. case ')':
  2908. case ']':
  2909. $str = substr($str, 1);
  2910. return $result;
  2911. break;
  2912. // String atom, number, NIL, *, %
  2913. default:
  2914. // empty string
  2915. if ($str === '' || $str === null) {
  2916. break 2;
  2917. }
  2918. // excluded chars: SP, CTL, ), [, ]
  2919. if (preg_match('/^([^\x00-\x20\x29\x5B\x5D\x7F]+)/', $str, $m)) {
  2920. $result[] = $m[1] == 'NIL' ? NULL : $m[1];
  2921. $str = substr($str, strlen($m[1]));
  2922. }
  2923. break;
  2924. }
  2925. }
  2926. return $num == 1 ? $result[0] : $result;
  2927. }
  2928. static function r_implode($element)
  2929. {
  2930. $string = '';
  2931. if (is_array($element)) {
  2932. reset($element);
  2933. while (list($key, $value) = each($element)) {
  2934. $string .= ' ' . self::r_implode($value);
  2935. }
  2936. }
  2937. else {
  2938. return $element;
  2939. }
  2940. return '(' . trim($string) . ')';
  2941. }
  2942. private function _xor($string, $string2)
  2943. {
  2944. $result = '';
  2945. $size = strlen($string);
  2946. for ($i=0; $i<$size; $i++) {
  2947. $result .= chr(ord($string[$i]) ^ ord($string2[$i]));
  2948. }
  2949. return $result;
  2950. }
  2951. /**
  2952. * Converts datetime string into unix timestamp
  2953. *
  2954. * @param string $date Date string
  2955. *
  2956. * @return int Unix timestamp
  2957. */
  2958. static function strToTime($date)
  2959. {
  2960. // support non-standard "GMTXXXX" literal
  2961. $date = preg_replace('/GMT\s*([+-][0-9]+)/', '\\1', $date);
  2962. // if date parsing fails, we have a date in non-rfc format
  2963. // remove token from the end and try again
  2964. while (($ts = intval(@strtotime($date))) <= 0) {
  2965. $d = explode(' ', $date);
  2966. array_pop($d);
  2967. if (empty($d)) {
  2968. break;
  2969. }
  2970. $date = implode(' ', $d);
  2971. }
  2972. return $ts < 0 ? 0 : $ts;
  2973. }
  2974. private function parseCapability($str, $trusted=false)
  2975. {
  2976. $str = preg_replace('/^\* CAPABILITY /i', '', $str);
  2977. $this->capability = explode(' ', strtoupper($str));
  2978. if (!isset($this->prefs['literal+']) && in_array('LITERAL+', $this->capability)) {
  2979. $this->prefs['literal+'] = true;
  2980. }
  2981. if ($trusted) {
  2982. $this->capability_readed = true;
  2983. }
  2984. }
  2985. /**
  2986. * Escapes a string when it contains special characters (RFC3501)
  2987. *
  2988. * @param string $string IMAP string
  2989. * @param boolean $force_quotes Forces string quoting (for atoms)
  2990. *
  2991. * @return string String atom, quoted-string or string literal
  2992. * @todo lists
  2993. */
  2994. static function escape($string, $force_quotes=false)
  2995. {
  2996. if ($string === null) {
  2997. return 'NIL';
  2998. }
  2999. if ($string === '') {
  3000. return '""';
  3001. }
  3002. // atom-string (only safe characters)
  3003. if (!$force_quotes && !preg_match('/[\x00-\x20\x22\x25\x28-\x2A\x5B-\x5D\x7B\x7D\x80-\xFF]/', $string)) {
  3004. return $string;
  3005. }
  3006. // quoted-string
  3007. if (!preg_match('/[\r\n\x00\x80-\xFF]/', $string)) {
  3008. return '"' . addcslashes($string, '\\"') . '"';
  3009. }
  3010. // literal-string
  3011. return sprintf("{%d}\r\n%s", strlen($string), $string);
  3012. }
  3013. static function unEscape($string)
  3014. {
  3015. return stripslashes($string);
  3016. }
  3017. /**
  3018. * Set the value of the debugging flag.
  3019. *
  3020. * @param boolean $debug New value for the debugging flag.
  3021. *
  3022. * @since 0.5-stable
  3023. */
  3024. function setDebug($debug, $handler = null)
  3025. {
  3026. $this->_debug = $debug;
  3027. $this->_debug_handler = $handler;
  3028. }
  3029. /**
  3030. * Write the given debug text to the current debug output handler.
  3031. *
  3032. * @param string $message Debug mesage text.
  3033. *
  3034. * @since 0.5-stable
  3035. */
  3036. private function debug($message)
  3037. {
  3038. if ($this->resourceid) {
  3039. $message = sprintf('[%s] %s', $this->resourceid, $message);
  3040. }
  3041. if ($this->_debug_handler) {
  3042. call_user_func_array($this->_debug_handler, array(&$this, $message));
  3043. } else {
  3044. echo "DEBUG: $message\n";
  3045. }
  3046. }
  3047. }