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

/mail/program/include/rcube_imap_generic.php

https://bitbucket.org/alisher/itworks
PHP | 3747 lines | 2488 code | 477 blank | 782 comment | 676 complexity | 14fde4e9009387ed7dd32dc89533a2bf MD5 | raw file
Possible License(s): GPL-3.0, LGPL-2.1
  1. <?php
  2. /**
  3. +-----------------------------------------------------------------------+
  4. | program/include/rcube_imap_generic.php |
  5. | |
  6. | This file is part of the Roundcube Webmail client |
  7. | Copyright (C) 2005-2010, The Roundcube Dev Team |
  8. | Copyright (C) 2011, Kolab Systems AG |
  9. | |
  10. | Licensed under the GNU General Public License version 3 or |
  11. | any later version with exceptions for skins & plugins. |
  12. | See the README file for a full license statement. |
  13. | |
  14. | PURPOSE: |
  15. | Provide alternative IMAP library that doesn't rely on the standard |
  16. | C-Client based version. This allows to function regardless |
  17. | of whether or not the PHP build it's running on has IMAP |
  18. | functionality built-in. |
  19. | |
  20. | Based on Iloha IMAP Library. See http://ilohamail.org/ for details |
  21. | |
  22. +-----------------------------------------------------------------------+
  23. | Author: Aleksander Machniak <alec@alec.pl> |
  24. | Author: Ryo Chijiiwa <Ryo@IlohaMail.org> |
  25. +-----------------------------------------------------------------------+
  26. $Id$
  27. */
  28. /**
  29. * Struct representing an e-mail message header
  30. *
  31. * @package Mail
  32. * @author Aleksander Machniak <alec@alec.pl>
  33. */
  34. class rcube_mail_header
  35. {
  36. public $id;
  37. public $uid;
  38. public $subject;
  39. public $from;
  40. public $to;
  41. public $cc;
  42. public $replyto;
  43. public $in_reply_to;
  44. public $date;
  45. public $messageID;
  46. public $size;
  47. public $encoding;
  48. public $charset;
  49. public $ctype;
  50. public $timestamp;
  51. public $bodystructure;
  52. public $internaldate;
  53. public $references;
  54. public $priority;
  55. public $mdn_to;
  56. public $others = array();
  57. public $flags = array();
  58. // map header to rcube_mail_header object property
  59. private $obj_headers = array(
  60. 'date' => 'date',
  61. 'from' => 'from',
  62. 'to' => 'to',
  63. 'subject' => 'subject',
  64. 'reply-to' => 'replyto',
  65. 'cc' => 'cc',
  66. 'bcc' => 'bcc',
  67. 'content-transfer-encoding' => 'encoding',
  68. 'in-reply-to' => 'in_reply_to',
  69. 'content-type' => 'ctype',
  70. 'references' => 'references',
  71. 'return-receipt-to' => 'mdn_to',
  72. 'disposition-notification-to' => 'mdn_to',
  73. 'x-confirm-reading-to' => 'mdn_to',
  74. 'message-id' => 'messageID',
  75. 'x-priority' => 'priority',
  76. );
  77. /**
  78. * Returns header value
  79. */
  80. public function get($name, $decode = true)
  81. {
  82. $name = strtolower($name);
  83. if (isset($this->obj_headers[$name])) {
  84. $value = $this->{$this->obj_headers[$name]};
  85. }
  86. else {
  87. $value = $this->others[$name];
  88. }
  89. return $decode ? rcube_mime::decode_header($value, $this->charset) : $value;
  90. }
  91. /**
  92. * Sets header value
  93. */
  94. public function set($name, $value)
  95. {
  96. $name = strtolower($name);
  97. if (isset($this->obj_headers[$name])) {
  98. $this->{$this->obj_headers[$name]} = $value;
  99. }
  100. else {
  101. $this->others[$name] = $value;
  102. }
  103. }
  104. }
  105. // For backward compatibility with cached messages (#1486602)
  106. class iilBasicHeader extends rcube_mail_header
  107. {
  108. }
  109. // Support objects created in git-master (0.9)
  110. class rcube_message_header extends rcube_mail_header
  111. {
  112. }
  113. /**
  114. * PHP based wrapper class to connect to an IMAP server
  115. *
  116. * @package Mail
  117. * @author Aleksander Machniak <alec@alec.pl>
  118. */
  119. class rcube_imap_generic
  120. {
  121. public $error;
  122. public $errornum;
  123. public $result;
  124. public $resultcode;
  125. public $selected;
  126. public $data = array();
  127. public $flags = array(
  128. 'SEEN' => '\\Seen',
  129. 'DELETED' => '\\Deleted',
  130. 'ANSWERED' => '\\Answered',
  131. 'DRAFT' => '\\Draft',
  132. 'FLAGGED' => '\\Flagged',
  133. 'FORWARDED' => '$Forwarded',
  134. 'MDNSENT' => '$MDNSent',
  135. '*' => '\\*',
  136. );
  137. private $fp;
  138. private $host;
  139. private $logged = false;
  140. private $capability = array();
  141. private $capability_readed = false;
  142. private $prefs;
  143. private $cmd_tag;
  144. private $cmd_num = 0;
  145. private $resourceid;
  146. private $_debug = false;
  147. private $_debug_handler = false;
  148. const ERROR_OK = 0;
  149. const ERROR_NO = -1;
  150. const ERROR_BAD = -2;
  151. const ERROR_BYE = -3;
  152. const ERROR_UNKNOWN = -4;
  153. const ERROR_COMMAND = -5;
  154. const ERROR_READONLY = -6;
  155. const COMMAND_NORESPONSE = 1;
  156. const COMMAND_CAPABILITY = 2;
  157. const COMMAND_LASTLINE = 4;
  158. /**
  159. * Object constructor
  160. */
  161. function __construct()
  162. {
  163. }
  164. /**
  165. * Send simple (one line) command to the connection stream
  166. *
  167. * @param string $string Command string
  168. * @param bool $endln True if CRLF need to be added at the end of command
  169. *
  170. * @param int Number of bytes sent, False on error
  171. */
  172. function putLine($string, $endln=true)
  173. {
  174. if (!$this->fp)
  175. return false;
  176. if ($this->_debug) {
  177. $this->debug('C: '. rtrim($string));
  178. }
  179. $res = fwrite($this->fp, $string . ($endln ? "\r\n" : ''));
  180. if ($res === false) {
  181. @fclose($this->fp);
  182. $this->fp = null;
  183. }
  184. return $res;
  185. }
  186. /**
  187. * Send command to the connection stream with Command Continuation
  188. * Requests (RFC3501 7.5) and LITERAL+ (RFC2088) support
  189. *
  190. * @param string $string Command string
  191. * @param bool $endln True if CRLF need to be added at the end of command
  192. *
  193. * @return int|bool Number of bytes sent, False on error
  194. */
  195. function putLineC($string, $endln=true)
  196. {
  197. if (!$this->fp) {
  198. return false;
  199. }
  200. if ($endln) {
  201. $string .= "\r\n";
  202. }
  203. $res = 0;
  204. if ($parts = preg_split('/(\{[0-9]+\}\r\n)/m', $string, -1, PREG_SPLIT_DELIM_CAPTURE)) {
  205. for ($i=0, $cnt=count($parts); $i<$cnt; $i++) {
  206. if (preg_match('/^\{([0-9]+)\}\r\n$/', $parts[$i+1], $matches)) {
  207. // LITERAL+ support
  208. if ($this->prefs['literal+']) {
  209. $parts[$i+1] = sprintf("{%d+}\r\n", $matches[1]);
  210. }
  211. $bytes = $this->putLine($parts[$i].$parts[$i+1], false);
  212. if ($bytes === false)
  213. return false;
  214. $res += $bytes;
  215. // don't wait if server supports LITERAL+ capability
  216. if (!$this->prefs['literal+']) {
  217. $line = $this->readLine(1000);
  218. // handle error in command
  219. if ($line[0] != '+')
  220. return false;
  221. }
  222. $i++;
  223. }
  224. else {
  225. $bytes = $this->putLine($parts[$i], false);
  226. if ($bytes === false)
  227. return false;
  228. $res += $bytes;
  229. }
  230. }
  231. }
  232. return $res;
  233. }
  234. /**
  235. * Reads line from the connection stream
  236. *
  237. * @param int $size Buffer size
  238. *
  239. * @return string Line of text response
  240. */
  241. function readLine($size=1024)
  242. {
  243. $line = '';
  244. if (!$size) {
  245. $size = 1024;
  246. }
  247. do {
  248. if ($this->eof()) {
  249. return $line ? $line : NULL;
  250. }
  251. $buffer = fgets($this->fp, $size);
  252. if ($buffer === false) {
  253. $this->closeSocket();
  254. break;
  255. }
  256. if ($this->_debug) {
  257. $this->debug('S: '. rtrim($buffer));
  258. }
  259. $line .= $buffer;
  260. } while (substr($buffer, -1) != "\n");
  261. return $line;
  262. }
  263. /**
  264. * Reads more data from the connection stream when provided
  265. * data contain string literal
  266. *
  267. * @param string $line Response text
  268. * @param bool $escape Enables escaping
  269. *
  270. * @return string Line of text response
  271. */
  272. function multLine($line, $escape = false)
  273. {
  274. $line = rtrim($line);
  275. if (preg_match('/\{([0-9]+)\}$/', $line, $m)) {
  276. $out = '';
  277. $str = substr($line, 0, -strlen($m[0]));
  278. $bytes = $m[1];
  279. while (strlen($out) < $bytes) {
  280. $line = $this->readBytes($bytes);
  281. if ($line === NULL)
  282. break;
  283. $out .= $line;
  284. }
  285. $line = $str . ($escape ? $this->escape($out) : $out);
  286. }
  287. return $line;
  288. }
  289. /**
  290. * Reads specified number of bytes from the connection stream
  291. *
  292. * @param int $bytes Number of bytes to get
  293. *
  294. * @return string Response text
  295. */
  296. function readBytes($bytes)
  297. {
  298. $data = '';
  299. $len = 0;
  300. while ($len < $bytes && !$this->eof())
  301. {
  302. $d = fread($this->fp, $bytes-$len);
  303. if ($this->_debug) {
  304. $this->debug('S: '. $d);
  305. }
  306. $data .= $d;
  307. $data_len = strlen($data);
  308. if ($len == $data_len) {
  309. break; // nothing was read -> exit to avoid apache lockups
  310. }
  311. $len = $data_len;
  312. }
  313. return $data;
  314. }
  315. /**
  316. * Reads complete response to the IMAP command
  317. *
  318. * @param array $untagged Will be filled with untagged response lines
  319. *
  320. * @return string Response text
  321. */
  322. function readReply(&$untagged=null)
  323. {
  324. do {
  325. $line = trim($this->readLine(1024));
  326. // store untagged response lines
  327. if ($line[0] == '*')
  328. $untagged[] = $line;
  329. } while ($line[0] == '*');
  330. if ($untagged)
  331. $untagged = join("\n", $untagged);
  332. return $line;
  333. }
  334. /**
  335. * Response parser.
  336. *
  337. * @param string $string Response text
  338. * @param string $err_prefix Error message prefix
  339. *
  340. * @return int Response status
  341. */
  342. function parseResult($string, $err_prefix='')
  343. {
  344. if (preg_match('/^[a-z0-9*]+ (OK|NO|BAD|BYE)(.*)$/i', trim($string), $matches)) {
  345. $res = strtoupper($matches[1]);
  346. $str = trim($matches[2]);
  347. if ($res == 'OK') {
  348. $this->errornum = self::ERROR_OK;
  349. } else if ($res == 'NO') {
  350. $this->errornum = self::ERROR_NO;
  351. } else if ($res == 'BAD') {
  352. $this->errornum = self::ERROR_BAD;
  353. } else if ($res == 'BYE') {
  354. $this->closeSocket();
  355. $this->errornum = self::ERROR_BYE;
  356. }
  357. if ($str) {
  358. $str = trim($str);
  359. // get response string and code (RFC5530)
  360. if (preg_match("/^\[([a-z-]+)\]/i", $str, $m)) {
  361. $this->resultcode = strtoupper($m[1]);
  362. $str = trim(substr($str, strlen($m[1]) + 2));
  363. }
  364. else {
  365. $this->resultcode = null;
  366. // parse response for [APPENDUID 1204196876 3456]
  367. if (preg_match("/^\[APPENDUID [0-9]+ ([0-9,:*]+)\]/i", $str, $m)) {
  368. $this->data['APPENDUID'] = $m[1];
  369. }
  370. }
  371. $this->result = $str;
  372. if ($this->errornum != self::ERROR_OK) {
  373. $this->error = $err_prefix ? $err_prefix.$str : $str;
  374. }
  375. }
  376. return $this->errornum;
  377. }
  378. return self::ERROR_UNKNOWN;
  379. }
  380. /**
  381. * Checks connection stream state.
  382. *
  383. * @return bool True if connection is closed
  384. */
  385. private function eof()
  386. {
  387. if (!is_resource($this->fp)) {
  388. return true;
  389. }
  390. // If a connection opened by fsockopen() wasn't closed
  391. // by the server, feof() will hang.
  392. $start = microtime(true);
  393. if (feof($this->fp) ||
  394. ($this->prefs['timeout'] && (microtime(true) - $start > $this->prefs['timeout']))
  395. ) {
  396. $this->closeSocket();
  397. return true;
  398. }
  399. return false;
  400. }
  401. /**
  402. * Closes connection stream.
  403. */
  404. private function closeSocket()
  405. {
  406. @fclose($this->fp);
  407. $this->fp = null;
  408. }
  409. /**
  410. * Error code/message setter.
  411. */
  412. function setError($code, $msg='')
  413. {
  414. $this->errornum = $code;
  415. $this->error = $msg;
  416. }
  417. /**
  418. * Checks response status.
  419. * Checks if command response line starts with specified prefix (or * BYE/BAD)
  420. *
  421. * @param string $string Response text
  422. * @param string $match Prefix to match with (case-sensitive)
  423. * @param bool $error Enables BYE/BAD checking
  424. * @param bool $nonempty Enables empty response checking
  425. *
  426. * @return bool True any check is true or connection is closed.
  427. */
  428. function startsWith($string, $match, $error=false, $nonempty=false)
  429. {
  430. if (!$this->fp) {
  431. return true;
  432. }
  433. if (strncmp($string, $match, strlen($match)) == 0) {
  434. return true;
  435. }
  436. if ($error && preg_match('/^\* (BYE|BAD) /i', $string, $m)) {
  437. if (strtoupper($m[1]) == 'BYE') {
  438. $this->closeSocket();
  439. }
  440. return true;
  441. }
  442. if ($nonempty && !strlen($string)) {
  443. return true;
  444. }
  445. return false;
  446. }
  447. private function hasCapability($name)
  448. {
  449. if (empty($this->capability) || $name == '') {
  450. return false;
  451. }
  452. if (in_array($name, $this->capability)) {
  453. return true;
  454. }
  455. else if (strpos($name, '=')) {
  456. return false;
  457. }
  458. $result = array();
  459. foreach ($this->capability as $cap) {
  460. $entry = explode('=', $cap);
  461. if ($entry[0] == $name) {
  462. $result[] = $entry[1];
  463. }
  464. }
  465. return !empty($result) ? $result : false;
  466. }
  467. /**
  468. * Capabilities checker
  469. *
  470. * @param string $name Capability name
  471. *
  472. * @return mixed Capability values array for key=value pairs, true/false for others
  473. */
  474. function getCapability($name)
  475. {
  476. $result = $this->hasCapability($name);
  477. if (!empty($result)) {
  478. return $result;
  479. }
  480. else if ($this->capability_readed) {
  481. return false;
  482. }
  483. // get capabilities (only once) because initial
  484. // optional CAPABILITY response may differ
  485. $result = $this->execute('CAPABILITY');
  486. if ($result[0] == self::ERROR_OK) {
  487. $this->parseCapability($result[1]);
  488. }
  489. $this->capability_readed = true;
  490. return $this->hasCapability($name);
  491. }
  492. function clearCapability()
  493. {
  494. $this->capability = array();
  495. $this->capability_readed = false;
  496. }
  497. /**
  498. * DIGEST-MD5/CRAM-MD5/PLAIN Authentication
  499. *
  500. * @param string $user
  501. * @param string $pass
  502. * @param string $type Authentication type (PLAIN/CRAM-MD5/DIGEST-MD5)
  503. *
  504. * @return resource Connection resourse on success, error code on error
  505. */
  506. function authenticate($user, $pass, $type='PLAIN')
  507. {
  508. if ($type == 'CRAM-MD5' || $type == 'DIGEST-MD5') {
  509. if ($type == 'DIGEST-MD5' && !class_exists('Auth_SASL')) {
  510. $this->setError(self::ERROR_BYE,
  511. "The Auth_SASL package is required for DIGEST-MD5 authentication");
  512. return self::ERROR_BAD;
  513. }
  514. $this->putLine($this->nextTag() . " AUTHENTICATE $type");
  515. $line = trim($this->readReply());
  516. if ($line[0] == '+') {
  517. $challenge = substr($line, 2);
  518. }
  519. else {
  520. return $this->parseResult($line);
  521. }
  522. if ($type == 'CRAM-MD5') {
  523. // RFC2195: CRAM-MD5
  524. $ipad = '';
  525. $opad = '';
  526. // initialize ipad, opad
  527. for ($i=0; $i<64; $i++) {
  528. $ipad .= chr(0x36);
  529. $opad .= chr(0x5C);
  530. }
  531. // pad $pass so it's 64 bytes
  532. $padLen = 64 - strlen($pass);
  533. for ($i=0; $i<$padLen; $i++) {
  534. $pass .= chr(0);
  535. }
  536. // generate hash
  537. $hash = md5($this->_xor($pass, $opad) . pack("H*",
  538. md5($this->_xor($pass, $ipad) . base64_decode($challenge))));
  539. $reply = base64_encode($user . ' ' . $hash);
  540. // send result
  541. $this->putLine($reply);
  542. }
  543. else {
  544. // RFC2831: DIGEST-MD5
  545. // proxy authorization
  546. if (!empty($this->prefs['auth_cid'])) {
  547. $authc = $this->prefs['auth_cid'];
  548. $pass = $this->prefs['auth_pw'];
  549. }
  550. else {
  551. $authc = $user;
  552. $user = '';
  553. }
  554. $auth_sasl = Auth_SASL::factory('digestmd5');
  555. $reply = base64_encode($auth_sasl->getResponse($authc, $pass,
  556. base64_decode($challenge), $this->host, 'imap', $user));
  557. // send result
  558. $this->putLine($reply);
  559. $line = trim($this->readReply());
  560. if ($line[0] == '+') {
  561. $challenge = substr($line, 2);
  562. }
  563. else {
  564. return $this->parseResult($line);
  565. }
  566. // check response
  567. $challenge = base64_decode($challenge);
  568. if (strpos($challenge, 'rspauth=') === false) {
  569. $this->setError(self::ERROR_BAD,
  570. "Unexpected response from server to DIGEST-MD5 response");
  571. return self::ERROR_BAD;
  572. }
  573. $this->putLine('');
  574. }
  575. $line = $this->readReply();
  576. $result = $this->parseResult($line);
  577. }
  578. else { // PLAIN
  579. // proxy authorization
  580. if (!empty($this->prefs['auth_cid'])) {
  581. $authc = $this->prefs['auth_cid'];
  582. $pass = $this->prefs['auth_pw'];
  583. }
  584. else {
  585. $authc = $user;
  586. $user = '';
  587. }
  588. $reply = base64_encode($user . chr(0) . $authc . chr(0) . $pass);
  589. // RFC 4959 (SASL-IR): save one round trip
  590. if ($this->getCapability('SASL-IR')) {
  591. list($result, $line) = $this->execute("AUTHENTICATE PLAIN", array($reply),
  592. self::COMMAND_LASTLINE | self::COMMAND_CAPABILITY);
  593. }
  594. else {
  595. $this->putLine($this->nextTag() . " AUTHENTICATE PLAIN");
  596. $line = trim($this->readReply());
  597. if ($line[0] != '+') {
  598. return $this->parseResult($line);
  599. }
  600. // send result, get reply and process it
  601. $this->putLine($reply);
  602. $line = $this->readReply();
  603. $result = $this->parseResult($line);
  604. }
  605. }
  606. if ($result == self::ERROR_OK) {
  607. // optional CAPABILITY response
  608. if ($line && preg_match('/\[CAPABILITY ([^]]+)\]/i', $line, $matches)) {
  609. $this->parseCapability($matches[1], true);
  610. }
  611. return $this->fp;
  612. }
  613. else {
  614. $this->setError($result, "AUTHENTICATE $type: $line");
  615. }
  616. return $result;
  617. }
  618. /**
  619. * LOGIN Authentication
  620. *
  621. * @param string $user
  622. * @param string $pass
  623. *
  624. * @return resource Connection resourse on success, error code on error
  625. */
  626. function login($user, $password)
  627. {
  628. list($code, $response) = $this->execute('LOGIN', array(
  629. $this->escape($user), $this->escape($password)), self::COMMAND_CAPABILITY);
  630. // re-set capabilities list if untagged CAPABILITY response provided
  631. if (preg_match('/\* CAPABILITY (.+)/i', $response, $matches)) {
  632. $this->parseCapability($matches[1], true);
  633. }
  634. if ($code == self::ERROR_OK) {
  635. return $this->fp;
  636. }
  637. return $code;
  638. }
  639. /**
  640. * Detects hierarchy delimiter
  641. *
  642. * @return string The delimiter
  643. */
  644. function getHierarchyDelimiter()
  645. {
  646. if ($this->prefs['delimiter']) {
  647. return $this->prefs['delimiter'];
  648. }
  649. // try (LIST "" ""), should return delimiter (RFC2060 Sec 6.3.8)
  650. list($code, $response) = $this->execute('LIST',
  651. array($this->escape(''), $this->escape('')));
  652. if ($code == self::ERROR_OK) {
  653. $args = $this->tokenizeResponse($response, 4);
  654. $delimiter = $args[3];
  655. if (strlen($delimiter) > 0) {
  656. return ($this->prefs['delimiter'] = $delimiter);
  657. }
  658. }
  659. return NULL;
  660. }
  661. /**
  662. * NAMESPACE handler (RFC 2342)
  663. *
  664. * @return array Namespace data hash (personal, other, shared)
  665. */
  666. function getNamespace()
  667. {
  668. if (array_key_exists('namespace', $this->prefs)) {
  669. return $this->prefs['namespace'];
  670. }
  671. if (!$this->getCapability('NAMESPACE')) {
  672. return self::ERROR_BAD;
  673. }
  674. list($code, $response) = $this->execute('NAMESPACE');
  675. if ($code == self::ERROR_OK && preg_match('/^\* NAMESPACE /', $response)) {
  676. $data = $this->tokenizeResponse(substr($response, 11));
  677. }
  678. if (!is_array($data)) {
  679. return $code;
  680. }
  681. $this->prefs['namespace'] = array(
  682. 'personal' => $data[0],
  683. 'other' => $data[1],
  684. 'shared' => $data[2],
  685. );
  686. return $this->prefs['namespace'];
  687. }
  688. /**
  689. * Connects to IMAP server and authenticates.
  690. *
  691. * @param string $host Server hostname or IP
  692. * @param string $user User name
  693. * @param string $password Password
  694. * @param array $options Connection and class options
  695. *
  696. * @return bool True on success, False on failure
  697. */
  698. function connect($host, $user, $password, $options=null)
  699. {
  700. // set options
  701. if (is_array($options)) {
  702. $this->prefs = $options;
  703. }
  704. // set auth method
  705. if (!empty($this->prefs['auth_type'])) {
  706. $auth_method = strtoupper($this->prefs['auth_type']);
  707. } else {
  708. $auth_method = 'CHECK';
  709. }
  710. $result = false;
  711. // initialize connection
  712. $this->error = '';
  713. $this->errornum = self::ERROR_OK;
  714. $this->selected = null;
  715. $this->user = $user;
  716. $this->host = $host;
  717. $this->logged = false;
  718. // check input
  719. if (empty($host)) {
  720. $this->setError(self::ERROR_BAD, "Empty host");
  721. return false;
  722. }
  723. if (empty($user)) {
  724. $this->setError(self::ERROR_NO, "Empty user");
  725. return false;
  726. }
  727. if (empty($password)) {
  728. $this->setError(self::ERROR_NO, "Empty password");
  729. return false;
  730. }
  731. if (!$this->prefs['port']) {
  732. $this->prefs['port'] = 143;
  733. }
  734. // check for SSL
  735. if ($this->prefs['ssl_mode'] && $this->prefs['ssl_mode'] != 'tls') {
  736. $host = $this->prefs['ssl_mode'] . '://' . $host;
  737. }
  738. if ($this->prefs['timeout'] <= 0) {
  739. $this->prefs['timeout'] = ini_get('default_socket_timeout');
  740. }
  741. // Connect
  742. $this->fp = @fsockopen($host, $this->prefs['port'], $errno, $errstr, $this->prefs['timeout']);
  743. if (!$this->fp) {
  744. $this->setError(self::ERROR_BAD, sprintf("Could not connect to %s:%d: %s", $host, $this->prefs['port'], $errstr));
  745. return false;
  746. }
  747. if ($this->prefs['timeout'] > 0)
  748. stream_set_timeout($this->fp, $this->prefs['timeout']);
  749. $line = trim(fgets($this->fp, 8192));
  750. if ($this->_debug) {
  751. // set connection identifier for debug output
  752. preg_match('/#([0-9]+)/', (string)$this->fp, $m);
  753. $this->resourceid = strtoupper(substr(md5($m[1].$this->user.microtime()), 0, 4));
  754. if ($line)
  755. $this->debug('S: '. $line);
  756. }
  757. // Connected to wrong port or connection error?
  758. if (!preg_match('/^\* (OK|PREAUTH)/i', $line)) {
  759. if ($line)
  760. $error = sprintf("Wrong startup greeting (%s:%d): %s", $host, $this->prefs['port'], $line);
  761. else
  762. $error = sprintf("Empty startup greeting (%s:%d)", $host, $this->prefs['port']);
  763. $this->setError(self::ERROR_BAD, $error);
  764. $this->closeConnection();
  765. return false;
  766. }
  767. // RFC3501 [7.1] optional CAPABILITY response
  768. if (preg_match('/\[CAPABILITY ([^]]+)\]/i', $line, $matches)) {
  769. $this->parseCapability($matches[1], true);
  770. }
  771. // TLS connection
  772. if ($this->prefs['ssl_mode'] == 'tls' && $this->getCapability('STARTTLS')) {
  773. if (version_compare(PHP_VERSION, '5.1.0', '>=')) {
  774. $res = $this->execute('STARTTLS');
  775. if ($res[0] != self::ERROR_OK) {
  776. $this->closeConnection();
  777. return false;
  778. }
  779. if (!stream_socket_enable_crypto($this->fp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
  780. $this->setError(self::ERROR_BAD, "Unable to negotiate TLS");
  781. $this->closeConnection();
  782. return false;
  783. }
  784. // Now we're secure, capabilities need to be reread
  785. $this->clearCapability();
  786. }
  787. }
  788. // Send ID info
  789. if (!empty($this->prefs['ident']) && $this->getCapability('ID')) {
  790. $this->id($this->prefs['ident']);
  791. }
  792. $auth_methods = array();
  793. $result = null;
  794. // check for supported auth methods
  795. if ($auth_method == 'CHECK') {
  796. if ($auth_caps = $this->getCapability('AUTH')) {
  797. $auth_methods = $auth_caps;
  798. }
  799. // RFC 2595 (LOGINDISABLED) LOGIN disabled when connection is not secure
  800. $login_disabled = $this->getCapability('LOGINDISABLED');
  801. if (($key = array_search('LOGIN', $auth_methods)) !== false) {
  802. if ($login_disabled) {
  803. unset($auth_methods[$key]);
  804. }
  805. }
  806. else if (!$login_disabled) {
  807. $auth_methods[] = 'LOGIN';
  808. }
  809. // Use best (for security) supported authentication method
  810. foreach (array('DIGEST-MD5', 'CRAM-MD5', 'CRAM_MD5', 'PLAIN', 'LOGIN') as $auth_method) {
  811. if (in_array($auth_method, $auth_methods)) {
  812. break;
  813. }
  814. }
  815. }
  816. else {
  817. // Prevent from sending credentials in plain text when connection is not secure
  818. if ($auth_method == 'LOGIN' && $this->getCapability('LOGINDISABLED')) {
  819. $this->setError(self::ERROR_BAD, "Login disabled by IMAP server");
  820. $this->closeConnection();
  821. return false;
  822. }
  823. // replace AUTH with CRAM-MD5 for backward compat.
  824. if ($auth_method == 'AUTH') {
  825. $auth_method = 'CRAM-MD5';
  826. }
  827. }
  828. // pre-login capabilities can be not complete
  829. $this->capability_readed = false;
  830. // Authenticate
  831. switch ($auth_method) {
  832. case 'CRAM_MD5':
  833. $auth_method = 'CRAM-MD5';
  834. case 'CRAM-MD5':
  835. case 'DIGEST-MD5':
  836. case 'PLAIN':
  837. $result = $this->authenticate($user, $password, $auth_method);
  838. break;
  839. case 'LOGIN':
  840. $result = $this->login($user, $password);
  841. break;
  842. default:
  843. $this->setError(self::ERROR_BAD, "Configuration error. Unknown auth method: $auth_method");
  844. }
  845. // Connected and authenticated
  846. if (is_resource($result)) {
  847. if ($this->prefs['force_caps']) {
  848. $this->clearCapability();
  849. }
  850. $this->logged = true;
  851. return true;
  852. }
  853. $this->closeConnection();
  854. return false;
  855. }
  856. /**
  857. * Checks connection status
  858. *
  859. * @return bool True if connection is active and user is logged in, False otherwise.
  860. */
  861. function connected()
  862. {
  863. return ($this->fp && $this->logged) ? true : false;
  864. }
  865. /**
  866. * Closes connection with logout.
  867. */
  868. function closeConnection()
  869. {
  870. if ($this->putLine($this->nextTag() . ' LOGOUT')) {
  871. $this->readReply();
  872. }
  873. $this->closeSocket();
  874. }
  875. /**
  876. * Executes SELECT command (if mailbox is already not in selected state)
  877. *
  878. * @param string $mailbox Mailbox name
  879. * @param array $qresync_data QRESYNC data (RFC5162)
  880. *
  881. * @return boolean True on success, false on error
  882. */
  883. function select($mailbox, $qresync_data = null)
  884. {
  885. if (!strlen($mailbox)) {
  886. return false;
  887. }
  888. if ($this->selected === $mailbox) {
  889. return true;
  890. }
  891. /*
  892. Temporary commented out because Courier returns \Noselect for INBOX
  893. Requires more investigation
  894. if (is_array($this->data['LIST']) && is_array($opts = $this->data['LIST'][$mailbox])) {
  895. if (in_array('\\Noselect', $opts)) {
  896. return false;
  897. }
  898. }
  899. */
  900. $params = array($this->escape($mailbox));
  901. // QRESYNC data items
  902. // 0. the last known UIDVALIDITY,
  903. // 1. the last known modification sequence,
  904. // 2. the optional set of known UIDs, and
  905. // 3. an optional parenthesized list of known sequence ranges and their
  906. // corresponding UIDs.
  907. if (!empty($qresync_data)) {
  908. if (!empty($qresync_data[2]))
  909. $qresync_data[2] = self::compressMessageSet($qresync_data[2]);
  910. $params[] = array('QRESYNC', $qresync_data);
  911. }
  912. list($code, $response) = $this->execute('SELECT', $params);
  913. if ($code == self::ERROR_OK) {
  914. $response = explode("\r\n", $response);
  915. foreach ($response as $line) {
  916. if (preg_match('/^\* ([0-9]+) (EXISTS|RECENT)$/i', $line, $m)) {
  917. $this->data[strtoupper($m[2])] = (int) $m[1];
  918. }
  919. else if (preg_match('/^\* OK \[/i', $line, $match)) {
  920. $line = substr($line, 6);
  921. if (preg_match('/^(UIDNEXT|UIDVALIDITY|UNSEEN) ([0-9]+)/i', $line, $match)) {
  922. $this->data[strtoupper($match[1])] = (int) $match[2];
  923. }
  924. else if (preg_match('/^(HIGHESTMODSEQ) ([0-9]+)/i', $line, $match)) {
  925. $this->data[strtoupper($match[1])] = (string) $match[2];
  926. }
  927. else if (preg_match('/^(NOMODSEQ)/i', $line, $match)) {
  928. $this->data[strtoupper($match[1])] = true;
  929. }
  930. else if (preg_match('/^PERMANENTFLAGS \(([^\)]+)\)/iU', $line, $match)) {
  931. $this->data['PERMANENTFLAGS'] = explode(' ', $match[1]);
  932. }
  933. }
  934. // QRESYNC FETCH response (RFC5162)
  935. else if (preg_match('/^\* ([0-9+]) FETCH/i', $line, $match)) {
  936. $line = substr($line, strlen($match[0]));
  937. $fetch_data = $this->tokenizeResponse($line, 1);
  938. $data = array('id' => $match[1]);
  939. for ($i=0, $size=count($fetch_data); $i<$size; $i+=2) {
  940. $data[strtolower($fetch_data[$i])] = $fetch_data[$i+1];
  941. }
  942. $this->data['QRESYNC'][$data['uid']] = $data;
  943. }
  944. // QRESYNC VANISHED response (RFC5162)
  945. else if (preg_match('/^\* VANISHED [()EARLIER]*/i', $line, $match)) {
  946. $line = substr($line, strlen($match[0]));
  947. $v_data = $this->tokenizeResponse($line, 1);
  948. $this->data['VANISHED'] = $v_data;
  949. }
  950. }
  951. $this->data['READ-WRITE'] = $this->resultcode != 'READ-ONLY';
  952. $this->selected = $mailbox;
  953. return true;
  954. }
  955. return false;
  956. }
  957. /**
  958. * Executes STATUS command
  959. *
  960. * @param string $mailbox Mailbox name
  961. * @param array $items Additional requested item names. By default
  962. * MESSAGES and UNSEEN are requested. Other defined
  963. * in RFC3501: UIDNEXT, UIDVALIDITY, RECENT
  964. *
  965. * @return array Status item-value hash
  966. * @since 0.5-beta
  967. */
  968. function status($mailbox, $items=array())
  969. {
  970. if (!strlen($mailbox)) {
  971. return false;
  972. }
  973. if (!in_array('MESSAGES', $items)) {
  974. $items[] = 'MESSAGES';
  975. }
  976. if (!in_array('UNSEEN', $items)) {
  977. $items[] = 'UNSEEN';
  978. }
  979. list($code, $response) = $this->execute('STATUS', array($this->escape($mailbox),
  980. '(' . implode(' ', (array) $items) . ')'));
  981. if ($code == self::ERROR_OK && preg_match('/\* STATUS /i', $response)) {
  982. $result = array();
  983. $response = substr($response, 9); // remove prefix "* STATUS "
  984. list($mbox, $items) = $this->tokenizeResponse($response, 2);
  985. // Fix for #1487859. Some buggy server returns not quoted
  986. // folder name with spaces. Let's try to handle this situation
  987. if (!is_array($items) && ($pos = strpos($response, '(')) !== false) {
  988. $response = substr($response, $pos);
  989. $items = $this->tokenizeResponse($response, 1);
  990. if (!is_array($items)) {
  991. return $result;
  992. }
  993. }
  994. for ($i=0, $len=count($items); $i<$len; $i += 2) {
  995. $result[$items[$i]] = $items[$i+1];
  996. }
  997. $this->data['STATUS:'.$mailbox] = $result;
  998. return $result;
  999. }
  1000. return false;
  1001. }
  1002. /**
  1003. * Executes EXPUNGE command
  1004. *
  1005. * @param string $mailbox Mailbox name
  1006. * @param string $messages Message UIDs to expunge
  1007. *
  1008. * @return boolean True on success, False on error
  1009. */
  1010. function expunge($mailbox, $messages=NULL)
  1011. {
  1012. if (!$this->select($mailbox)) {
  1013. return false;
  1014. }
  1015. if (!$this->data['READ-WRITE']) {
  1016. $this->setError(self::ERROR_READONLY, "Mailbox is read-only", 'EXPUNGE');
  1017. return false;
  1018. }
  1019. // Clear internal status cache
  1020. unset($this->data['STATUS:'.$mailbox]);
  1021. if ($messages)
  1022. $result = $this->execute('UID EXPUNGE', array($messages), self::COMMAND_NORESPONSE);
  1023. else
  1024. $result = $this->execute('EXPUNGE', null, self::COMMAND_NORESPONSE);
  1025. if ($result == self::ERROR_OK) {
  1026. $this->selected = null; // state has changed, need to reselect
  1027. return true;
  1028. }
  1029. return false;
  1030. }
  1031. /**
  1032. * Executes CLOSE command
  1033. *
  1034. * @return boolean True on success, False on error
  1035. * @since 0.5
  1036. */
  1037. function close()
  1038. {
  1039. $result = $this->execute('CLOSE', NULL, self::COMMAND_NORESPONSE);
  1040. if ($result == self::ERROR_OK) {
  1041. $this->selected = null;
  1042. return true;
  1043. }
  1044. return false;
  1045. }
  1046. /**
  1047. * Folder subscription (SUBSCRIBE)
  1048. *
  1049. * @param string $mailbox Mailbox name
  1050. *
  1051. * @return boolean True on success, False on error
  1052. */
  1053. function subscribe($mailbox)
  1054. {
  1055. $result = $this->execute('SUBSCRIBE', array($this->escape($mailbox)),
  1056. self::COMMAND_NORESPONSE);
  1057. return ($result == self::ERROR_OK);
  1058. }
  1059. /**
  1060. * Folder unsubscription (UNSUBSCRIBE)
  1061. *
  1062. * @param string $mailbox Mailbox name
  1063. *
  1064. * @return boolean True on success, False on error
  1065. */
  1066. function unsubscribe($mailbox)
  1067. {
  1068. $result = $this->execute('UNSUBSCRIBE', array($this->escape($mailbox)),
  1069. self::COMMAND_NORESPONSE);
  1070. return ($result == self::ERROR_OK);
  1071. }
  1072. /**
  1073. * Folder creation (CREATE)
  1074. *
  1075. * @param string $mailbox Mailbox name
  1076. *
  1077. * @return bool True on success, False on error
  1078. */
  1079. function createFolder($mailbox)
  1080. {
  1081. $result = $this->execute('CREATE', array($this->escape($mailbox)),
  1082. self::COMMAND_NORESPONSE);
  1083. return ($result == self::ERROR_OK);
  1084. }
  1085. /**
  1086. * Folder renaming (RENAME)
  1087. *
  1088. * @param string $mailbox Mailbox name
  1089. *
  1090. * @return bool True on success, False on error
  1091. */
  1092. function renameFolder($from, $to)
  1093. {
  1094. $result = $this->execute('RENAME', array($this->escape($from), $this->escape($to)),
  1095. self::COMMAND_NORESPONSE);
  1096. return ($result == self::ERROR_OK);
  1097. }
  1098. /**
  1099. * Executes DELETE command
  1100. *
  1101. * @param string $mailbox Mailbox name
  1102. *
  1103. * @return boolean True on success, False on error
  1104. */
  1105. function deleteFolder($mailbox)
  1106. {
  1107. $result = $this->execute('DELETE', array($this->escape($mailbox)),
  1108. self::COMMAND_NORESPONSE);
  1109. return ($result == self::ERROR_OK);
  1110. }
  1111. /**
  1112. * Removes all messages in a folder
  1113. *
  1114. * @param string $mailbox Mailbox name
  1115. *
  1116. * @return boolean True on success, False on error
  1117. */
  1118. function clearFolder($mailbox)
  1119. {
  1120. $num_in_trash = $this->countMessages($mailbox);
  1121. if ($num_in_trash > 0) {
  1122. $res = $this->flag($mailbox, '1:*', 'DELETED');
  1123. }
  1124. if ($res) {
  1125. if ($this->selected === $mailbox)
  1126. $res = $this->close();
  1127. else
  1128. $res = $this->expunge($mailbox);
  1129. }
  1130. return $res;
  1131. }
  1132. /**
  1133. * Returns list of mailboxes
  1134. *
  1135. * @param string $ref Reference name
  1136. * @param string $mailbox Mailbox name
  1137. * @param array $status_opts (see self::_listMailboxes)
  1138. * @param array $select_opts (see self::_listMailboxes)
  1139. *
  1140. * @return array List of mailboxes or hash of options if $status_opts argument
  1141. * is non-empty.
  1142. */
  1143. function listMailboxes($ref, $mailbox, $status_opts=array(), $select_opts=array())
  1144. {
  1145. return $this->_listMailboxes($ref, $mailbox, false, $status_opts, $select_opts);
  1146. }
  1147. /**
  1148. * Returns list of subscribed mailboxes
  1149. *
  1150. * @param string $ref Reference name
  1151. * @param string $mailbox Mailbox name
  1152. * @param array $status_opts (see self::_listMailboxes)
  1153. *
  1154. * @return array List of mailboxes or hash of options if $status_opts argument
  1155. * is non-empty.
  1156. */
  1157. function listSubscribed($ref, $mailbox, $status_opts=array())
  1158. {
  1159. return $this->_listMailboxes($ref, $mailbox, true, $status_opts, NULL);
  1160. }
  1161. /**
  1162. * IMAP LIST/LSUB command
  1163. *
  1164. * @param string $ref Reference name
  1165. * @param string $mailbox Mailbox name
  1166. * @param bool $subscribed Enables returning subscribed mailboxes only
  1167. * @param array $status_opts List of STATUS options (RFC5819: LIST-STATUS)
  1168. * Possible: MESSAGES, RECENT, UIDNEXT, UIDVALIDITY, UNSEEN
  1169. * @param array $select_opts List of selection options (RFC5258: LIST-EXTENDED)
  1170. * Possible: SUBSCRIBED, RECURSIVEMATCH, REMOTE
  1171. *
  1172. * @return array List of mailboxes or hash of options if $status_ops argument
  1173. * is non-empty.
  1174. */
  1175. private function _listMailboxes($ref, $mailbox, $subscribed=false,
  1176. $status_opts=array(), $select_opts=array())
  1177. {
  1178. if (!strlen($mailbox)) {
  1179. $mailbox = '*';
  1180. }
  1181. $args = array();
  1182. if (!empty($select_opts) && $this->getCapability('LIST-EXTENDED')) {
  1183. $select_opts = (array) $select_opts;
  1184. $args[] = '(' . implode(' ', $select_opts) . ')';
  1185. }
  1186. $args[] = $this->escape($ref);
  1187. $args[] = $this->escape($mailbox);
  1188. if (!empty($status_opts) && $this->getCapability('LIST-STATUS')) {
  1189. $status_opts = (array) $status_opts;
  1190. $lstatus = true;
  1191. $args[] = 'RETURN (STATUS (' . implode(' ', $status_opts) . '))';
  1192. }
  1193. list($code, $response) = $this->execute($subscribed ? 'LSUB' : 'LIST', $args);
  1194. if ($code == self::ERROR_OK) {
  1195. $folders = array();
  1196. $last = 0;
  1197. $pos = 0;
  1198. $response .= "\r\n";
  1199. while ($pos = strpos($response, "\r\n", $pos+1)) {
  1200. // literal string, not real end-of-command-line
  1201. if ($response[$pos-1] == '}') {
  1202. continue;
  1203. }
  1204. $line = substr($response, $last, $pos - $last);
  1205. $last = $pos + 2;
  1206. if (!preg_match('/^\* (LIST|LSUB|STATUS) /i', $line, $m)) {
  1207. continue;
  1208. }
  1209. $cmd = strtoupper($m[1]);
  1210. $line = substr($line, strlen($m[0]));
  1211. // * LIST (<options>) <delimiter> <mailbox>
  1212. if ($cmd == 'LIST' || $cmd == 'LSUB') {
  1213. list($opts, $delim, $mailbox) = $this->tokenizeResponse($line, 3);
  1214. // Add to result array
  1215. if (!$lstatus) {
  1216. $folders[] = $mailbox;
  1217. }
  1218. else {
  1219. $folders[$mailbox] = array();
  1220. }
  1221. // store LSUB options only if not empty, this way
  1222. // we can detect a situation when LIST doesn't return specified folder
  1223. if (!empty($opts) || $cmd == 'LIST') {
  1224. // Add to options array
  1225. if (empty($this->data['LIST'][$mailbox]))
  1226. $this->data['LIST'][$mailbox] = $opts;
  1227. else if (!empty($opts))
  1228. $this->data['LIST'][$mailbox] = array_unique(array_merge(
  1229. $this->data['LIST'][$mailbox], $opts));
  1230. }
  1231. }
  1232. // * STATUS <mailbox> (<result>)
  1233. else if ($cmd == 'STATUS') {
  1234. list($mailbox, $status) = $this->tokenizeResponse($line, 2);
  1235. for ($i=0, $len=count($status); $i<$len; $i += 2) {
  1236. list($name, $value) = $this->tokenizeResponse($status, 2);
  1237. $folders[$mailbox][$name] = $value;
  1238. }
  1239. }
  1240. }
  1241. return $folders;
  1242. }
  1243. return false;
  1244. }
  1245. /**
  1246. * Returns count of all messages in a folder
  1247. *
  1248. * @param string $mailbox Mailbox name
  1249. *
  1250. * @return int Number of messages, False on error
  1251. */
  1252. function countMessages($mailbox, $refresh = false)
  1253. {
  1254. if ($refresh) {
  1255. $this->selected = null;
  1256. }
  1257. if ($this->selected === $mailbox) {
  1258. return $this->data['EXISTS'];
  1259. }
  1260. // Check internal cache
  1261. $cache = $this->data['STATUS:'.$mailbox];
  1262. if (!empty($cache) && isset($cache['MESSAGES'])) {
  1263. return (int) $cache['MESSAGES'];
  1264. }
  1265. // Try STATUS (should be faster than SELECT)
  1266. $counts = $this->status($mailbox);
  1267. if (is_array($counts)) {
  1268. return (int) $counts['MESSAGES'];
  1269. }
  1270. return false;
  1271. }
  1272. /**
  1273. * Returns count of messages with \Recent flag in a folder
  1274. *
  1275. * @param string $mailbox Mailbox name
  1276. *
  1277. * @return int Number of messages, False on error
  1278. */
  1279. function countRecent($mailbox)
  1280. {
  1281. if (!strlen($mailbox)) {
  1282. $mailbox = 'INBOX';
  1283. }
  1284. $this->select($mailbox);
  1285. if ($this->selected === $mailbox) {
  1286. return $this->data['RECENT'];
  1287. }
  1288. return false;
  1289. }
  1290. /**
  1291. * Returns count of messages without \Seen flag in a specified folder
  1292. *
  1293. * @param string $mailbox Mailbox name
  1294. *
  1295. * @return int Number of messages, False on error
  1296. */
  1297. function countUnseen($mailbox)
  1298. {
  1299. // Check internal cache
  1300. $cache = $this->data['STATUS:'.$mailbox];
  1301. if (!empty($cache) && isset($cache['UNSEEN'])) {
  1302. return (int) $cache['UNSEEN'];
  1303. }
  1304. // Try STATUS (should be faster than SELECT+SEARCH)
  1305. $counts = $this->status($mailbox);
  1306. if (is_array($counts)) {
  1307. return (int) $counts['UNSEEN'];
  1308. }
  1309. // Invoke SEARCH as a fallback
  1310. $index = $this->search($mailbox, 'ALL UNSEEN', false, array('COUNT'));
  1311. if (!$index->is_error()) {
  1312. return $index->count();
  1313. }
  1314. return false;
  1315. }
  1316. /**
  1317. * Executes ID command (RFC2971)
  1318. *
  1319. * @param array $items Client identification information key/value hash
  1320. *
  1321. * @return array Server identification information key/value hash
  1322. * @since 0.6
  1323. */
  1324. function id($items=array())
  1325. {
  1326. if (is_array($items) && !empty($items)) {
  1327. foreach ($items as $key => $value) {
  1328. $args[] = $this->escape($key, true);
  1329. $args[] = $this->escape($value, true);
  1330. }
  1331. }
  1332. list($code, $response) = $this->execute('ID', array(
  1333. !empty($args) ? '(' . implode(' ', (array) $args) . ')' : $this->escape(null)
  1334. ));
  1335. if ($code == self::ERROR_OK && preg_match('/\* ID /i', $response)) {
  1336. $response = substr($response, 5); // remove prefix "* ID "
  1337. $items = $this->tokenizeResponse($response, 1);
  1338. $result = null;
  1339. for ($i=0, $len=count($items); $i<$len; $i += 2) {
  1340. $result[$items[$i]] = $items[$i+1];
  1341. }
  1342. return $result;
  1343. }
  1344. return false;
  1345. }
  1346. /**
  1347. * Executes ENABLE command (RFC5161)
  1348. *
  1349. * @param mixed $extension Extension name to enable (or array of names)
  1350. *
  1351. * @return array|bool List of enabled extensions, False on error
  1352. * @since 0.6
  1353. */
  1354. function enable($extension)
  1355. {
  1356. if (empty($extension))
  1357. return false;
  1358. if (!$this->hasCapability('ENABLE'))
  1359. return false;
  1360. if (!is_array($extension))
  1361. $extension = array($extension);
  1362. list($code, $response) = $this->execute('ENABLE', $extension);
  1363. if ($code == self::ERROR_OK && preg_match('/\* ENABLED /i', $response)) {
  1364. $response = substr($response, 10); // remove prefix "* ENABLED "
  1365. $result = (array) $this->tokenizeResponse($response);
  1366. return $result;
  1367. }
  1368. return false;
  1369. }
  1370. /**
  1371. * Executes SORT command
  1372. *
  1373. * @param string $mailbox Mailbox name
  1374. * @param string $field Field to sort by (ARRIVAL, CC, DATE, FROM, SIZE, SUBJECT, TO)
  1375. * @param string $add Searching criteria
  1376. * @param bool $return_uid Enables UID SORT usage
  1377. * @param string $encoding Character set
  1378. *
  1379. * @return rcube_result_index Response data
  1380. */
  1381. function sort($mailbox, $field, $add='', $return_uid=false, $encoding = 'US-ASCII')
  1382. {
  1383. require_once dirname(__FILE__) . '/rcube_result_index.php';
  1384. $field = strtoupper($field);
  1385. if ($field == 'INTERNALDATE') {
  1386. $field = 'ARRIVAL';
  1387. }
  1388. $fields = array('ARRIVAL' => 1,'CC' => 1,'DATE' => 1,
  1389. 'FROM' => 1, 'SIZE' => 1, 'SUBJECT' => 1, 'TO' => 1);
  1390. if (!$fields[$field]) {
  1391. return new rcube_result_index($mailbox);
  1392. }
  1393. if (!$this->select($mailbox)) {
  1394. return new rcube_result_index($mailbox);
  1395. }
  1396. // RFC 5957: SORT=DISPLAY
  1397. if (($field == 'FROM' || $field == 'TO') && $this->getCapability('SORT=DISPLAY')) {
  1398. $field = 'DISPLAY' . $field;
  1399. }
  1400. // message IDs
  1401. if (!empty($add))
  1402. $add = $this->compressMessageSet($add);
  1403. list($code, $response) = $this->execute($return_uid ? 'UID SORT' : 'SORT',
  1404. array("($field)", $encoding, 'ALL' . (!empty($add) ? ' '.$add : '')));
  1405. if ($code != self::ERROR_OK) {
  1406. $response = null;
  1407. }
  1408. return new rcube_result_index($mailbox, $response);
  1409. }
  1410. /**
  1411. * Executes THREAD command
  1412. *
  1413. * @param string $mailbox Mailbox name
  1414. * @param string $algorithm Threading algorithm (ORDEREDSUBJECT, REFERENCES, REFS)
  1415. * @param string $criteria Searching criteria
  1416. * @param bool $return_uid Enables UIDs in result instead of sequence numbers
  1417. * @param string $encoding Character set
  1418. *
  1419. * @return rcube_result_thread Thread data
  1420. */
  1421. function thread($mailbox, $algorithm='REFERENCES', $criteria='', $return_uid=false, $encoding='US-ASCII')
  1422. {
  1423. require_once dirname(__FILE__) . '/rcube_result_thread.php';
  1424. $old_sel = $this->selected;
  1425. if (!$this->select($mailbox)) {
  1426. return new rcube_result_thread($mailbox);
  1427. }
  1428. // return empty result when folder is empty and we're just after SELECT
  1429. if ($old_sel != $mailbox && !$this->data['EXISTS']) {
  1430. return new rcube_result_thread($mailbox);
  1431. }
  1432. $encoding = $encoding ? trim($encoding) : 'US-ASCII';
  1433. $algorithm = $algorithm ? trim($algorithm) : 'REFERENCES';
  1434. $criteria = $criteria ? 'ALL '.trim($criteria) : 'ALL';
  1435. $data = '';
  1436. list($code, $response) = $this->execute($return_uid ? 'UID THREAD' : 'THREAD',
  1437. array($algorithm, $encoding, $criteria));
  1438. if ($code != self::ERROR_OK) {
  1439. $response = null;
  1440. }
  1441. return new rcube_result_thread($mailbox, $response);
  1442. }
  1443. /**
  1444. * Executes SEARCH command
  1445. *
  1446. * @param string $mailbox Mailbox name
  1447. * @param string $criteria Searching criteria
  1448. * @param bool $return_uid Enable UID in result instead of sequence ID
  1449. * @param array $items Return items (MIN, MAX, COUNT, ALL)
  1450. *
  1451. * @return rcube_result_index Result data
  1452. */
  1453. function search($mailbox, $criteria, $return_uid=false, $items=array())
  1454. {
  1455. require_once dirname(__FILE__) . '/rcube_result_index.php';
  1456. $old_sel = $this->selected;
  1457. if (!$this->select($mailbox)) {
  1458. return new rcube_result_index($mailbox);
  1459. }
  1460. // return empty result when folder is empty and we're just after SELECT
  1461. if ($old_sel != $mailbox && !$this->data['EXISTS']) {
  1462. return new rcube_result_index($mailbox, '* SEARCH');
  1463. }
  1464. // If ESEARCH is supported always use ALL
  1465. // but not when items are specified or using simple id2uid search
  1466. if (empty($items) && preg_match('/[^0-9]/', $criteria)) {
  1467. $items = array('ALL');
  1468. }
  1469. $esearch = empty($items) ? false : $this->getCapability('ESEARCH');
  1470. $criteria = trim($criteria);
  1471. $params = '';
  1472. // RFC4731: ESEARCH
  1473. if (!empty($items) && $esearch) {
  1474. $params .= 'RETURN (' . implode(' ', $items) . ')';
  1475. }
  1476. if (!empty($criteria)) {
  1477. $modseq = stripos($criteria, 'MODSEQ') !== false;
  1478. $params .= ($params ? ' ' : '') . $criteria;
  1479. }
  1480. else {
  1481. $params .= 'ALL';
  1482. }
  1483. list($code, $response) = $this->execute($return_uid ? 'UID SEARCH' : 'SEARCH',
  1484. array($params));
  1485. if ($code != self::ERROR_OK) {
  1486. $response = null;
  1487. }
  1488. return new rcube_result_index($mailbox, $response);
  1489. }
  1490. /**
  1491. * Simulates SORT command by using FETCH and sorting.
  1492. *
  1493. * @param string $mailbox Mailbox name
  1494. * @param string|array $message_set Searching criteria (list of messages to return)
  1495. * @param string $index_field Field to sort by (ARRIVAL, CC, DATE, FROM, SIZE, SUBJECT, TO)
  1496. * @param bool $skip_deleted Makes that DELETED messages will be skipped
  1497. * @param bool $uidfetch Enables UID FETCH usage
  1498. * @param bool $return_uid Enables returning UIDs instead of IDs
  1499. *
  1500. * @return rcube_result_index Response data
  1501. */
  1502. function index($mailbox, $message_set, $index_field='', $skip_deleted=true,
  1503. $uidfetch=false, $return_uid=false)
  1504. {
  1505. require_once dirname(__FILE__) . '/rcube_result_index.php';
  1506. $msg_index = $this->fetchHeaderIndex($mailbox, $message_set,
  1507. $index_field, $skip_deleted, $uidfetch, $return_uid);
  1508. if (!empty($msg_index)) {
  1509. asort($msg_index); // ASC
  1510. $msg_index = array_keys($msg_index);
  1511. $msg_index = '* SEARCH ' . implode(' ', $msg_index);
  1512. }
  1513. else {
  1514. $msg_index = is_array($msg_index) ? '* SEARCH' : null;
  1515. }
  1516. return new rcube_result_index($mailbox, $msg_index);
  1517. }
  1518. function fetchHeaderIndex($mailbox, $message_set, $index_field='', $skip_deleted=true,
  1519. $uidfetch=false, $return_uid=false)
  1520. {
  1521. if (is_array($message_set)) {
  1522. if (!($message_set = $this->compressMessageSet($message_set)))
  1523. return false;
  1524. } else {
  1525. list($from_idx, $to_idx) = explode(':', $message_set);
  1526. if (empty($message_set) ||
  1527. (isset($to_idx) && $to_idx != '*' && (int)$from_idx > (int)$to_idx)) {
  1528. return false;
  1529. }
  1530. }
  1531. $index_field = empty($index_field) ? 'DATE' : strtoupper($index_field);
  1532. $fields_a['DATE'] = 1;
  1533. $fields_a['INTERNALDATE'] = 4;
  1534. $fields_a['ARRIVAL'] = 4;
  1535. $fields_a['FROM'] = 1;
  1536. $fields_a['REPLY-TO'] = 1;
  1537. $fields_a['SENDER'] = 1;
  1538. $fields_a['TO'] = 1;
  1539. $fields_a['CC'] = 1;
  1540. $fields_a['SUBJECT'] = 1;
  1541. $fields_a['UID'] = 2;
  1542. $fields_a['SIZE'] = 2;
  1543. $fields_a['SEEN'] = 3;
  1544. $fields_a['RECENT'] = 3;
  1545. $fields_a['DELETED'] = 3;
  1546. if (!($mode = $fields_a[$index_field])) {
  1547. return false;
  1548. }
  1549. /* Do "SELECT" command */
  1550. if (!$this->select($mailbox)) {
  1551. return false;
  1552. }
  1553. // build FETCH command string
  1554. $key = $this->nextTag();
  1555. $cmd = $uidfetch ? 'UID FETCH' : 'FETCH';
  1556. $fields = array();
  1557. if ($return_uid)
  1558. $fields[] = 'UID';
  1559. if ($skip_deleted)
  1560. $fields[] = 'FLAGS';
  1561. if ($mode == 1) {
  1562. if ($index_field == 'DATE')
  1563. $fields[] = 'INTERNALDATE';
  1564. $fields[] = "BODY.PEEK[HEADER.FIELDS ($index_field)]";
  1565. }
  1566. else if ($mode == 2) {
  1567. if ($index_field == 'SIZE')
  1568. $fields[] = 'RFC822.SIZE';
  1569. else if (!$return_uid || $index_field != 'UID')
  1570. $fields[] = $index_field;
  1571. }
  1572. else if ($mode == 3 && !$skip_deleted)
  1573. $fields[] = 'FLAGS';
  1574. else if ($mode == 4)
  1575. $fields[] = 'INTERNALDATE';
  1576. $request = "$key $cmd $message_set (" . implode(' ', $fields) . ")";
  1577. if (!$this->putLine($request)) {
  1578. $this->setError(self::ERROR_COMMAND, "Unable to send command: $request");
  1579. return false;
  1580. }
  1581. $result = array();
  1582. do {
  1583. $line = rtrim($this->readLine(200));
  1584. $line = $this->multLine($line);
  1585. if (preg_match('/^\* ([0-9]+) FETCH/', $line, $m)) {
  1586. $id = $m[1];
  1587. $flags = NULL;
  1588. if ($return_uid) {
  1589. if (preg_match('/UID ([0-9]+)/', $line, $matches))
  1590. $id = (int) $matches[1];
  1591. else
  1592. continue;
  1593. }
  1594. if ($skip_deleted && preg_match('/FLAGS \(([^)]+)\)/', $line, $matches)) {
  1595. $flags = explode(' ', strtoupper($matches[1]));
  1596. if (in_array('\\DELETED', $flags)) {
  1597. $deleted[$id] = $id;
  1598. continue;
  1599. }
  1600. }
  1601. if ($mode == 1 && $index_field == 'DATE') {
  1602. if (preg_match('/BODY\[HEADER\.FIELDS \("*DATE"*\)\] (.*)/', $line, $matches)) {
  1603. $value = preg_replace(array('/^"*[a-z]+:/i'), '', $matches[1]);
  1604. $value = trim($value);
  1605. $result[$id] = $this->strToTime($value);
  1606. }
  1607. // non-existent/empty Date: header, use INTERNALDATE
  1608. if (empty($result[$id])) {
  1609. if (preg_match('/INTERNALDATE "([^"]+)"/', $line, $matches))
  1610. $result[$id] = $this->strToTime($matches[1]);
  1611. else
  1612. $result[$id] = 0;
  1613. }
  1614. } else if ($mode == 1) {
  1615. if (preg_match('/BODY\[HEADER\.FIELDS \("?(FROM|REPLY-TO|SENDER|TO|SUBJECT)"?\)\] (.*)/', $line, $matches)) {
  1616. $value = preg_replace(array('/^"*[a-z]+:/i', '/\s+$/sm'), array('', ''), $matches[2]);
  1617. $result[$id] = trim($value);
  1618. } else {
  1619. $result[$id] = '';
  1620. }
  1621. } else if ($mode == 2) {
  1622. if (preg_match('/(UID|RFC822\.SIZE) ([0-9]+)/', $line, $matches)) {
  1623. $result[$id] = trim($matches[2]);
  1624. } else {
  1625. $result[$id] = 0;
  1626. }
  1627. } else if ($mode == 3) {
  1628. if (!$flags && preg_match('/FLAGS \(([^)]+)\)/', $line, $matches)) {
  1629. $flags = explode(' ', $matches[1]);
  1630. }
  1631. $result[$id] = in_array('\\'.$index_field, $flags) ? 1 : 0;
  1632. } else if ($mode == 4) {
  1633. if (preg_match('/INTERNALDATE "([^"]+)"/', $line, $matches)) {
  1634. $result[$id] = $this->strToTime($matches[1]);
  1635. } else {
  1636. $result[$id] = 0;
  1637. }
  1638. }
  1639. }
  1640. } while (!$this->startsWith($line, $key, true, true));
  1641. return $result;
  1642. }
  1643. /**
  1644. * Returns message sequence identifier
  1645. *
  1646. * @param string $mailbox Mailbox name
  1647. * @param int $uid Message unique identifier (UID)
  1648. *
  1649. * @return int Message sequence identifier
  1650. */
  1651. function UID2ID($mailbox, $uid)
  1652. {
  1653. if ($uid > 0) {
  1654. $index = $this->search($mailbox, "UID $uid");
  1655. if ($index->count() == 1) {
  1656. $arr = $index->get();
  1657. return (int) $arr[0];
  1658. }
  1659. }
  1660. return null;
  1661. }
  1662. /**
  1663. * Returns message unique identifier (UID)
  1664. *
  1665. * @param string $mailbox Mailbox name
  1666. * @param int $uid Message sequence identifier
  1667. *
  1668. * @return int Message unique identifier
  1669. */
  1670. function ID2UID($mailbox, $id)
  1671. {
  1672. if (empty($id) || $id < 0) {
  1673. return null;
  1674. }
  1675. if (!$this->select($mailbox)) {
  1676. return null;
  1677. }
  1678. $index = $this->search($mailbox, $id, true);
  1679. if ($index->count() == 1) {
  1680. $arr = $index->get();
  1681. return (int) $arr[0];
  1682. }
  1683. return null;
  1684. }
  1685. /**
  1686. * Sets flag of the message(s)
  1687. *
  1688. * @param string $mailbox Mailbox name
  1689. * @param string|array $messages Message UID(s)
  1690. * @param string $flag Flag name
  1691. *
  1692. * @return bool True on success, False on failure
  1693. */
  1694. function flag($mailbox, $messages, $flag) {
  1695. return $this->modFlag($mailbox, $messages, $flag, '+');
  1696. }
  1697. /**
  1698. * Unsets flag of the message(s)
  1699. *
  1700. * @param string $mailbox Mailbox name
  1701. * @param string|array $messages Message UID(s)
  1702. * @param string $flag Flag name
  1703. *
  1704. * @return bool True on success, False on failure
  1705. */
  1706. function unflag($mailbox, $messages, $flag) {
  1707. return $this->modFlag($mailbox, $messages, $flag, '-');
  1708. }
  1709. /**
  1710. * Changes flag of the message(s)
  1711. *
  1712. * @param string $mailbox Mailbox name
  1713. * @param string|array $messages Message UID(s)
  1714. * @param string $flag Flag name
  1715. * @param string $mod Modifier [+|-]. Default: "+".
  1716. *
  1717. * @return bool True on success, False on failure
  1718. */
  1719. private function modFlag($mailbox, $messages, $flag, $mod = '+')
  1720. {
  1721. if ($mod != '+' && $mod != '-') {
  1722. $mod = '+';
  1723. }
  1724. if (!$this->select($mailbox)) {
  1725. return false;
  1726. }
  1727. if (!$this->data['READ-WRITE']) {
  1728. $this->setError(self::ERROR_READONLY, "Mailbox is read-only", 'STORE');
  1729. return false;
  1730. }
  1731. // Clear internal status cache
  1732. if ($flag == 'SEEN') {
  1733. unset($this->data['STATUS:'.$mailbox]['UNSEEN']);
  1734. }
  1735. $flag = $this->flags[strtoupper($flag)];
  1736. $result = $this->execute('UID STORE', array(
  1737. $this->compressMessageSet($messages), $mod . 'FLAGS.SILENT', "($flag)"),
  1738. self::COMMAND_NORESPONSE);
  1739. return ($result == self::ERROR_OK);
  1740. }
  1741. /**
  1742. * Copies message(s) from one folder to another
  1743. *
  1744. * @param string|array $messages Message UID(s)
  1745. * @param string $from Mailbox name
  1746. * @param string $to Destination mailbox name
  1747. *
  1748. * @return bool True on success, False on failure
  1749. */
  1750. function copy($messages, $from, $to)
  1751. {
  1752. if (!$this->select($from)) {
  1753. return false;
  1754. }
  1755. // Clear internal status cache
  1756. unset($this->data['STATUS:'.$to]);
  1757. $result = $this->execute('UID COPY', array(
  1758. $this->compressMessageSet($messages), $this->escape($to)),
  1759. self::COMMAND_NORESPONSE);
  1760. return ($result == self::ERROR_OK);
  1761. }
  1762. /**
  1763. * Moves message(s) from one folder to another.
  1764. * Original message(s) will be marked as deleted.
  1765. *
  1766. * @param string|array $messages Message UID(s)
  1767. * @param string $from Mailbox name
  1768. * @param string $to Destination mailbox name
  1769. *
  1770. * @return bool True on success, False on failure
  1771. */
  1772. function move($messages, $from, $to)
  1773. {
  1774. if (!$this->select($from)) {
  1775. return false;
  1776. }
  1777. if (!$this->data['READ-WRITE']) {
  1778. $this->setError(self::ERROR_READONLY, "Mailbox is read-only", 'STORE');
  1779. return false;
  1780. }
  1781. $r = $this->copy($messages, $from, $to);
  1782. if ($r) {
  1783. // Clear internal status cache
  1784. unset($this->data['STATUS:'.$from]);
  1785. return $this->flag($from, $messages, 'DELETED');
  1786. }
  1787. return $r;
  1788. }
  1789. /**
  1790. * FETCH command (RFC3501)
  1791. *
  1792. * @param string $mailbox Mailbox name
  1793. * @param mixed $message_set Message(s) sequence identifier(s) or UID(s)
  1794. * @param bool $is_uid True if $message_set contains UIDs
  1795. * @param array $query_items FETCH command data items
  1796. * @param string $mod_seq Modification sequence for CHANGEDSINCE (RFC4551) query
  1797. * @param bool $vanished Enables VANISHED parameter (RFC5162) for CHANGEDSINCE query
  1798. *
  1799. * @return array List of rcube_mail_header elements, False on error
  1800. * @since 0.6
  1801. */
  1802. function fetch($mailbox, $message_set, $is_uid = false, $query_items = array(),
  1803. $mod_seq = null, $vanished = false)
  1804. {
  1805. if (!$this->select($mailbox)) {
  1806. return false;
  1807. }
  1808. $message_set = $this->compressMessageSet($message_set);
  1809. $result = array();
  1810. $key = $this->nextTag();
  1811. $request = $key . ($is_uid ? ' UID' : '') . " FETCH $message_set ";
  1812. $request .= "(" . implode(' ', $query_items) . ")";
  1813. if ($mod_seq !== null && $this->hasCapability('CONDSTORE')) {
  1814. $request .= " (CHANGEDSINCE $mod_seq" . ($vanished ? " VANISHED" : '') .")";
  1815. }
  1816. if (!$this->putLine($request)) {
  1817. $this->setError(self::ERROR_COMMAND, "Unable to send command: $request");
  1818. return false;
  1819. }
  1820. do {
  1821. $line = $this->readLine(4096);
  1822. if (!$line)
  1823. break;
  1824. // Sample reply line:
  1825. // * 321 FETCH (UID 2417 RFC822.SIZE 2730 FLAGS (\Seen)
  1826. // INTERNALDATE "16-Nov-2008 21:08:46 +0100" BODYSTRUCTURE (...)
  1827. // BODY[HEADER.FIELDS ...
  1828. if (preg_match('/^\* ([0-9]+) FETCH/', $line, $m)) {
  1829. $id = intval($m[1]);
  1830. $result[$id] = new rcube_mail_header;
  1831. $result[$id]->id = $id;
  1832. $result[$id]->subject = '';
  1833. $result[$id]->messageID = 'mid:' . $id;
  1834. $headers = null;
  1835. $lines = array();
  1836. $line = substr($line, strlen($m[0]) + 2);
  1837. $ln = 0;
  1838. // get complete entry
  1839. while (preg_match('/\{([0-9]+)\}\r\n$/', $line, $m)) {
  1840. $bytes = $m[1];
  1841. $out = '';
  1842. while (strlen($out) < $bytes) {
  1843. $out = $this->readBytes($bytes);
  1844. if ($out === NULL)
  1845. break;
  1846. $line .= $out;
  1847. }
  1848. $str = $this->readLine(4096);
  1849. if ($str === false)
  1850. break;
  1851. $line .= $str;
  1852. }
  1853. // Tokenize response and assign to object properties
  1854. while (list($name, $value) = $this->tokenizeResponse($line, 2)) {
  1855. if ($name == 'UID') {
  1856. $result[$id]->uid = intval($value);
  1857. }
  1858. else if ($name == 'RFC822.SIZE') {
  1859. $result[$id]->size = intval($value);
  1860. }
  1861. else if ($name == 'RFC822.TEXT') {
  1862. $result[$id]->body = $value;
  1863. }
  1864. else if ($name == 'INTERNALDATE') {
  1865. $result[$id]->internaldate = $value;
  1866. $result[$id]->date = $value;
  1867. $result[$id]->timestamp = $this->StrToTime($value);
  1868. }
  1869. else if ($name == 'FLAGS') {
  1870. if (!empty($value)) {
  1871. foreach ((array)$value as $flag) {
  1872. $flag = str_replace(array('$', '\\'), '', $flag);
  1873. $flag = strtoupper($flag);
  1874. $result[$id]->flags[$flag] = true;
  1875. }
  1876. }
  1877. }
  1878. else if ($name == 'MODSEQ') {
  1879. $result[$id]->modseq = $value[0];
  1880. }
  1881. else if ($name == 'ENVELOPE') {
  1882. $result[$id]->envelope = $value;
  1883. }
  1884. else if ($name == 'BODYSTRUCTURE' || ($name == 'BODY' && count($value) > 2)) {
  1885. if (!is_array($value[0]) && (strtolower($value[0]) == 'message' && strtolower($value[1]) == 'rfc822')) {
  1886. $value = array($value);
  1887. }
  1888. $result[$id]->bodystructure = $value;
  1889. }
  1890. else if ($name == 'RFC822') {
  1891. $result[$id]->body = $value;
  1892. }
  1893. else if ($name == 'BODY') {
  1894. $body = $this->tokenizeResponse($line, 1);
  1895. if ($value[0] == 'HEADER.FIELDS')
  1896. $headers = $body;
  1897. else if (!empty($value))
  1898. $result[$id]->bodypart[$value[0]] = $body;
  1899. else
  1900. $result[$id]->body = $body;
  1901. }
  1902. }
  1903. // create array with header field:data
  1904. if (!empty($headers)) {
  1905. $headers = explode("\n", trim($headers));
  1906. foreach ($headers as $hid => $resln) {
  1907. if (ord($resln[0]) <= 32) {
  1908. $lines[$ln] .= (empty($lines[$ln]) ? '' : "\n") . trim($resln);
  1909. } else {
  1910. $lines[++$ln] = trim($resln);
  1911. }
  1912. }
  1913. while (list($lines_key, $str) = each($lines)) {
  1914. list($field, $string) = explode(':', $str, 2);
  1915. $field = strtolower($field);
  1916. $string = preg_replace('/\n[\t\s]*/', ' ', trim($string));
  1917. switch ($field) {
  1918. case 'date';
  1919. $result[$id]->date = $string;
  1920. $result[$id]->timestamp = $this->strToTime($string);
  1921. break;
  1922. case 'from':
  1923. $result[$id]->from = $string;
  1924. break;
  1925. case 'to':
  1926. $result[$id]->to = preg_replace('/undisclosed-recipients:[;,]*/', '', $string);
  1927. break;
  1928. case 'subject':
  1929. $result[$id]->subject = $string;
  1930. break;
  1931. case 'reply-to':
  1932. $result[$id]->replyto = $string;
  1933. break;
  1934. case 'cc':
  1935. $result[$id]->cc = $string;
  1936. break;
  1937. case 'bcc':
  1938. $result[$id]->bcc = $string;
  1939. break;
  1940. case 'content-transfer-encoding':
  1941. $result[$id]->encoding = $string;
  1942. break;
  1943. case 'content-type':
  1944. $ctype_parts = preg_split('/[; ]/', $string);
  1945. $result[$id]->ctype = strtolower(array_shift($ctype_parts));
  1946. if (preg_match('/charset\s*=\s*"?([a-z0-9\-\.\_]+)"?/i', $string, $regs)) {
  1947. $result[$id]->charset = $regs[1];
  1948. }
  1949. break;
  1950. case 'in-reply-to':
  1951. $result[$id]->in_reply_to = str_replace(array("\n", '<', '>'), '', $string);
  1952. break;
  1953. case 'references':
  1954. $result[$id]->references = $string;
  1955. break;
  1956. case 'return-receipt-to':
  1957. case 'disposition-notification-to':
  1958. case 'x-confirm-reading-to':
  1959. $result[$id]->mdn_to = $string;
  1960. break;
  1961. case 'message-id':
  1962. $result[$id]->messageID = $string;
  1963. break;
  1964. case 'x-priority':
  1965. if (preg_match('/^(\d+)/', $string, $matches)) {
  1966. $result[$id]->priority = intval($matches[1]);
  1967. }
  1968. break;
  1969. default:
  1970. if (strlen($field) > 2) {
  1971. $result[$id]->others[$field] = $string;
  1972. }
  1973. break;
  1974. }
  1975. }
  1976. }
  1977. }
  1978. // VANISHED response (QRESYNC RFC5162)
  1979. // Sample: * VANISHED (EARLIER) 300:310,405,411
  1980. else if (preg_match('/^\* VANISHED [()EARLIER]*/i', $line, $match)) {
  1981. $line = substr($line, strlen($match[0]));
  1982. $v_data = $this->tokenizeResponse($line, 1);
  1983. $this->data['VANISHED'] = $v_data;
  1984. }
  1985. } while (!$this->startsWith($line, $key, true));
  1986. return $result;
  1987. }
  1988. function fetchHeaders($mailbox, $message_set, $is_uid = false, $bodystr = false, $add = '')
  1989. {
  1990. $query_items = array('UID', 'RFC822.SIZE', 'FLAGS', 'INTERNALDATE');
  1991. if ($bodystr)
  1992. $query_items[] = 'BODYSTRUCTURE';
  1993. $query_items[] = 'BODY.PEEK[HEADER.FIELDS ('
  1994. . 'DATE FROM TO SUBJECT CONTENT-TYPE CC REPLY-TO LIST-POST DISPOSITION-NOTIFICATION-TO X-PRIORITY'
  1995. . ($add ? ' ' . trim($add) : '')
  1996. . ')]';
  1997. $result = $this->fetch($mailbox, $message_set, $is_uid, $query_items);
  1998. return $result;
  1999. }
  2000. function fetchHeader($mailbox, $id, $uidfetch=false, $bodystr=false, $add='')
  2001. {
  2002. $a = $this->fetchHeaders($mailbox, $id, $uidfetch, $bodystr, $add);
  2003. if (is_array($a)) {
  2004. return array_shift($a);
  2005. }
  2006. return false;
  2007. }
  2008. function sortHeaders($a, $field, $flag)
  2009. {
  2010. if (empty($field)) {
  2011. $field = 'uid';
  2012. }
  2013. else {
  2014. $field = strtolower($field);
  2015. }
  2016. if ($field == 'date' || $field == 'internaldate') {
  2017. $field = 'timestamp';
  2018. }
  2019. if (empty($flag)) {
  2020. $flag = 'ASC';
  2021. } else {
  2022. $flag = strtoupper($flag);
  2023. }
  2024. $c = count($a);
  2025. if ($c > 0) {
  2026. // Strategy:
  2027. // First, we'll create an "index" array.
  2028. // Then, we'll use sort() on that array,
  2029. // and use that to sort the main array.
  2030. // create "index" array
  2031. $index = array();
  2032. reset($a);
  2033. while (list($key, $val) = each($a)) {
  2034. if ($field == 'timestamp') {
  2035. $data = $this->strToTime($val->date);
  2036. if (!$data) {
  2037. $data = $val->timestamp;
  2038. }
  2039. } else {
  2040. $data = $val->$field;
  2041. if (is_string($data)) {
  2042. $data = str_replace('"', '', $data);
  2043. if ($field == 'subject') {
  2044. $data = preg_replace('/^(Re: \s*|Fwd:\s*|Fw:\s*)+/i', '', $data);
  2045. }
  2046. $data = strtoupper($data);
  2047. }
  2048. }
  2049. $index[$key] = $data;
  2050. }
  2051. // sort index
  2052. if ($flag == 'ASC') {
  2053. asort($index);
  2054. } else {
  2055. arsort($index);
  2056. }
  2057. // form new array based on index
  2058. $result = array();
  2059. reset($index);
  2060. while (list($key, $val) = each($index)) {
  2061. $result[$key] = $a[$key];
  2062. }
  2063. }
  2064. return $result;
  2065. }
  2066. function fetchMIMEHeaders($mailbox, $uid, $parts, $mime=true)
  2067. {
  2068. if (!$this->select($mailbox)) {
  2069. return false;
  2070. }
  2071. $result = false;
  2072. $parts = (array) $parts;
  2073. $key = $this->nextTag();
  2074. $peeks = array();
  2075. $type = $mime ? 'MIME' : 'HEADER';
  2076. // format request
  2077. foreach ($parts as $part) {
  2078. $peeks[] = "BODY.PEEK[$part.$type]";
  2079. }
  2080. $request = "$key UID FETCH $uid (" . implode(' ', $peeks) . ')';
  2081. // send request
  2082. if (!$this->putLine($request)) {
  2083. $this->setError(self::ERROR_COMMAND, "Unable to send command: $request");
  2084. return false;
  2085. }
  2086. do {
  2087. $line = $this->readLine(1024);
  2088. if (preg_match('/^\* [0-9]+ FETCH [0-9UID( ]+BODY\[([0-9\.]+)\.'.$type.'\]/', $line, $matches)) {
  2089. $idx = $matches[1];
  2090. $headers = '';
  2091. // get complete entry
  2092. if (preg_match('/\{([0-9]+)\}\r\n$/', $line, $m)) {
  2093. $bytes = $m[1];
  2094. $out = '';
  2095. while (strlen($out) < $bytes) {
  2096. $out = $this->readBytes($bytes);
  2097. if ($out === null)
  2098. break;
  2099. $headers .= $out;
  2100. }
  2101. }
  2102. $result[$idx] = trim($headers);
  2103. }
  2104. } while (!$this->startsWith($line, $key, true));
  2105. return $result;
  2106. }
  2107. function fetchPartHeader($mailbox, $id, $is_uid=false, $part=NULL)
  2108. {
  2109. $part = empty($part) ? 'HEADER' : $part.'.MIME';
  2110. return $this->handlePartBody($mailbox, $id, $is_uid, $part);
  2111. }
  2112. function handlePartBody($mailbox, $id, $is_uid=false, $part='', $encoding=NULL, $print=NULL, $file=NULL, $formatted=true)
  2113. {
  2114. if (!$this->select($mailbox)) {
  2115. return false;
  2116. }
  2117. switch ($encoding) {
  2118. case 'base64':
  2119. $mode = 1;
  2120. break;
  2121. case 'quoted-printable':
  2122. $mode = 2;
  2123. break;
  2124. case 'x-uuencode':
  2125. case 'x-uue':
  2126. case 'uue':
  2127. case 'uuencode':
  2128. $mode = 3;
  2129. break;
  2130. default:
  2131. $mode = 0;
  2132. }
  2133. // format request
  2134. $reply_key = '* ' . $id;
  2135. $key = $this->nextTag();
  2136. $request = $key . ($is_uid ? ' UID' : '') . " FETCH $id (BODY.PEEK[$part])";
  2137. // send request
  2138. if (!$this->putLine($request)) {
  2139. $this->setError(self::ERROR_COMMAND, "Unable to send command: $request");
  2140. return false;
  2141. }
  2142. // receive reply line
  2143. do {
  2144. $line = rtrim($this->readLine(1024));
  2145. $a = explode(' ', $line);
  2146. } while (!($end = $this->startsWith($line, $key, true)) && $a[2] != 'FETCH');
  2147. $len = strlen($line);
  2148. $result = false;
  2149. if ($a[2] != 'FETCH') {
  2150. }
  2151. // handle empty "* X FETCH ()" response
  2152. else if ($line[$len-1] == ')' && $line[$len-2] != '(') {
  2153. // one line response, get everything between first and last quotes
  2154. if (substr($line, -4, 3) == 'NIL') {
  2155. // NIL response
  2156. $result = '';
  2157. } else {
  2158. $from = strpos($line, '"') + 1;
  2159. $to = strrpos($line, '"');
  2160. $len = $to - $from;
  2161. $result = substr($line, $from, $len);
  2162. }
  2163. if ($mode == 1) {
  2164. $result = base64_decode($result);
  2165. }
  2166. else if ($mode == 2) {
  2167. $result = quoted_printable_decode($result);
  2168. }
  2169. else if ($mode == 3) {
  2170. $result = convert_uudecode($result);
  2171. }
  2172. } else if ($line[$len-1] == '}') {
  2173. // multi-line request, find sizes of content and receive that many bytes
  2174. $from = strpos($line, '{') + 1;
  2175. $to = strrpos($line, '}');
  2176. $len = $to - $from;
  2177. $sizeStr = substr($line, $from, $len);
  2178. $bytes = (int)$sizeStr;
  2179. $prev = '';
  2180. while ($bytes > 0) {
  2181. $line = $this->readLine(4096);
  2182. if ($line === NULL) {
  2183. break;
  2184. }
  2185. $len = strlen($line);
  2186. if ($len > $bytes) {
  2187. $line = substr($line, 0, $bytes);
  2188. $len = strlen($line);
  2189. }
  2190. $bytes -= $len;
  2191. // BASE64
  2192. if ($mode == 1) {
  2193. $line = rtrim($line, "\t\r\n\0\x0B");
  2194. // create chunks with proper length for base64 decoding
  2195. $line = $prev.$line;
  2196. $length = strlen($line);
  2197. if ($length % 4) {
  2198. $length = floor($length / 4) * 4;
  2199. $prev = substr($line, $length);
  2200. $line = substr($line, 0, $length);
  2201. }
  2202. else
  2203. $prev = '';
  2204. $line = base64_decode($line);
  2205. // QUOTED-PRINTABLE
  2206. } else if ($mode == 2) {
  2207. $line = rtrim($line, "\t\r\0\x0B");
  2208. $line = quoted_printable_decode($line);
  2209. // UUENCODE
  2210. } else if ($mode == 3) {
  2211. $line = rtrim($line, "\t\r\n\0\x0B");
  2212. if ($line == 'end' || preg_match('/^begin\s+[0-7]+\s+.+$/', $line))
  2213. continue;
  2214. $line = convert_uudecode($line);
  2215. // default
  2216. } else if ($formatted) {
  2217. $line = rtrim($line, "\t\r\n\0\x0B") . "\n";
  2218. }
  2219. if ($file)
  2220. fwrite($file, $line);
  2221. else if ($print)
  2222. echo $line;
  2223. else
  2224. $result .= $line;
  2225. }
  2226. }
  2227. // read in anything up until last line
  2228. if (!$end)
  2229. do {
  2230. $line = $this->readLine(1024);
  2231. } while (!$this->startsWith($line, $key, true));
  2232. if ($result !== false) {
  2233. if ($file) {
  2234. fwrite($file, $result);
  2235. } else if ($print) {
  2236. echo $result;
  2237. } else
  2238. return $result;
  2239. return true;
  2240. }
  2241. return false;
  2242. }
  2243. /**
  2244. * Handler for IMAP APPEND command
  2245. *
  2246. * @param string $mailbox Mailbox name
  2247. * @param string $message Message content
  2248. *
  2249. * @return string|bool On success APPENDUID response (if available) or True, False on failure
  2250. */
  2251. function append($mailbox, &$message)
  2252. {
  2253. unset($this->data['APPENDUID']);
  2254. if (!$mailbox) {
  2255. return false;
  2256. }
  2257. $message = str_replace("\r", '', $message);
  2258. $message = str_replace("\n", "\r\n", $message);
  2259. $len = strlen($message);
  2260. if (!$len) {
  2261. return false;
  2262. }
  2263. $key = $this->nextTag();
  2264. $request = sprintf("$key APPEND %s (\\Seen) {%d%s}", $this->escape($mailbox),
  2265. $len, ($this->prefs['literal+'] ? '+' : ''));
  2266. if ($this->putLine($request)) {
  2267. // Don't wait when LITERAL+ is supported
  2268. if (!$this->prefs['literal+']) {
  2269. $line = $this->readReply();
  2270. if ($line[0] != '+') {
  2271. $this->parseResult($line, 'APPEND: ');
  2272. return false;
  2273. }
  2274. }
  2275. if (!$this->putLine($message)) {
  2276. return false;
  2277. }
  2278. do {
  2279. $line = $this->readLine();
  2280. } while (!$this->startsWith($line, $key, true, true));
  2281. // Clear internal status cache
  2282. unset($this->data['STATUS:'.$mailbox]);
  2283. if ($this->parseResult($line, 'APPEND: ') != self::ERROR_OK)
  2284. return false;
  2285. else if (!empty($this->data['APPENDUID']))
  2286. return $this->data['APPENDUID'];
  2287. else
  2288. return true;
  2289. }
  2290. else {
  2291. $this->setError(self::ERROR_COMMAND, "Unable to send command: $request");
  2292. }
  2293. return false;
  2294. }
  2295. /**
  2296. * Handler for IMAP APPEND command.
  2297. *
  2298. * @param string $mailbox Mailbox name
  2299. * @param string $path Path to the file with message body
  2300. * @param string $headers Message headers
  2301. *
  2302. * @return string|bool On success APPENDUID response (if available) or True, False on failure
  2303. */
  2304. function appendFromFile($mailbox, $path, $headers=null)
  2305. {
  2306. unset($this->data['APPENDUID']);
  2307. if (!$mailbox) {
  2308. return false;
  2309. }
  2310. // open message file
  2311. $in_fp = false;
  2312. if (file_exists(realpath($path))) {
  2313. $in_fp = fopen($path, 'r');
  2314. }
  2315. if (!$in_fp) {
  2316. $this->setError(self::ERROR_UNKNOWN, "Couldn't open $path for reading");
  2317. return false;
  2318. }
  2319. $body_separator = "\r\n\r\n";
  2320. $len = filesize($path);
  2321. if (!$len) {
  2322. return false;
  2323. }
  2324. if ($headers) {
  2325. $headers = preg_replace('/[\r\n]+$/', '', $headers);
  2326. $len += strlen($headers) + strlen($body_separator);
  2327. }
  2328. // send APPEND command
  2329. $key = $this->nextTag();
  2330. $request = sprintf("$key APPEND %s (\\Seen) {%d%s}", $this->escape($mailbox),
  2331. $len, ($this->prefs['literal+'] ? '+' : ''));
  2332. if ($this->putLine($request)) {
  2333. // Don't wait when LITERAL+ is supported
  2334. if (!$this->prefs['literal+']) {
  2335. $line = $this->readReply();
  2336. if ($line[0] != '+') {
  2337. $this->parseResult($line, 'APPEND: ');
  2338. return false;
  2339. }
  2340. }
  2341. // send headers with body separator
  2342. if ($headers) {
  2343. $this->putLine($headers . $body_separator, false);
  2344. }
  2345. // send file
  2346. while (!feof($in_fp) && $this->fp) {
  2347. $buffer = fgets($in_fp, 4096);
  2348. $this->putLine($buffer, false);
  2349. }
  2350. fclose($in_fp);
  2351. if (!$this->putLine('')) { // \r\n
  2352. return false;
  2353. }
  2354. // read response
  2355. do {
  2356. $line = $this->readLine();
  2357. } while (!$this->startsWith($line, $key, true, true));
  2358. // Clear internal status cache
  2359. unset($this->data['STATUS:'.$mailbox]);
  2360. if ($this->parseResult($line, 'APPEND: ') != self::ERROR_OK)
  2361. return false;
  2362. else if (!empty($this->data['APPENDUID']))
  2363. return $this->data['APPENDUID'];
  2364. else
  2365. return true;
  2366. }
  2367. else {
  2368. $this->setError(self::ERROR_COMMAND, "Unable to send command: $request");
  2369. }
  2370. return false;
  2371. }
  2372. /**
  2373. * Returns QUOTA information
  2374. *
  2375. * @return array Quota information
  2376. */
  2377. function getQuota()
  2378. {
  2379. /*
  2380. * GETQUOTAROOT "INBOX"
  2381. * QUOTAROOT INBOX user/rchijiiwa1
  2382. * QUOTA user/rchijiiwa1 (STORAGE 654 9765)
  2383. * OK Completed
  2384. */
  2385. $result = false;
  2386. $quota_lines = array();
  2387. $key = $this->nextTag();
  2388. $command = $key . ' GETQUOTAROOT INBOX';
  2389. // get line(s) containing quota info
  2390. if ($this->putLine($command)) {
  2391. do {
  2392. $line = rtrim($this->readLine(5000));
  2393. if (preg_match('/^\* QUOTA /', $line)) {
  2394. $quota_lines[] = $line;
  2395. }
  2396. } while (!$this->startsWith($line, $key, true, true));
  2397. }
  2398. else {
  2399. $this->setError(self::ERROR_COMMAND, "Unable to send command: $command");
  2400. }
  2401. // return false if not found, parse if found
  2402. $min_free = PHP_INT_MAX;
  2403. foreach ($quota_lines as $key => $quota_line) {
  2404. $quota_line = str_replace(array('(', ')'), '', $quota_line);
  2405. $parts = explode(' ', $quota_line);
  2406. $storage_part = array_search('STORAGE', $parts);
  2407. if (!$storage_part) {
  2408. continue;
  2409. }
  2410. $used = intval($parts[$storage_part+1]);
  2411. $total = intval($parts[$storage_part+2]);
  2412. $free = $total - $used;
  2413. // return lowest available space from all quotas
  2414. if ($free < $min_free) {
  2415. $min_free = $free;
  2416. $result['used'] = $used;
  2417. $result['total'] = $total;
  2418. $result['percent'] = min(100, round(($used/max(1,$total))*100));
  2419. $result['free'] = 100 - $result['percent'];
  2420. }
  2421. }
  2422. return $result;
  2423. }
  2424. /**
  2425. * Send the SETACL command (RFC4314)
  2426. *
  2427. * @param string $mailbox Mailbox name
  2428. * @param string $user User name
  2429. * @param mixed $acl ACL string or array
  2430. *
  2431. * @return boolean True on success, False on failure
  2432. *
  2433. * @since 0.5-beta
  2434. */
  2435. function setACL($mailbox, $user, $acl)
  2436. {
  2437. if (is_array($acl)) {
  2438. $acl = implode('', $acl);
  2439. }
  2440. $result = $this->execute('SETACL', array(
  2441. $this->escape($mailbox), $this->escape($user), strtolower($acl)),
  2442. self::COMMAND_NORESPONSE);
  2443. return ($result == self::ERROR_OK);
  2444. }
  2445. /**
  2446. * Send the DELETEACL command (RFC4314)
  2447. *
  2448. * @param string $mailbox Mailbox name
  2449. * @param string $user User name
  2450. *
  2451. * @return boolean True on success, False on failure
  2452. *
  2453. * @since 0.5-beta
  2454. */
  2455. function deleteACL($mailbox, $user)
  2456. {
  2457. $result = $this->execute('DELETEACL', array(
  2458. $this->escape($mailbox), $this->escape($user)),
  2459. self::COMMAND_NORESPONSE);
  2460. return ($result == self::ERROR_OK);
  2461. }
  2462. /**
  2463. * Send the GETACL command (RFC4314)
  2464. *
  2465. * @param string $mailbox Mailbox name
  2466. *
  2467. * @return array User-rights array on success, NULL on error
  2468. * @since 0.5-beta
  2469. */
  2470. function getACL($mailbox)
  2471. {
  2472. list($code, $response) = $this->execute('GETACL', array($this->escape($mailbox)));
  2473. if ($code == self::ERROR_OK && preg_match('/^\* ACL /i', $response)) {
  2474. // Parse server response (remove "* ACL ")
  2475. $response = substr($response, 6);
  2476. $ret = $this->tokenizeResponse($response);
  2477. $mbox = array_shift($ret);
  2478. $size = count($ret);
  2479. // Create user-rights hash array
  2480. // @TODO: consider implementing fixACL() method according to RFC4314.2.1.1
  2481. // so we could return only standard rights defined in RFC4314,
  2482. // excluding 'c' and 'd' defined in RFC2086.
  2483. if ($size % 2 == 0) {
  2484. for ($i=0; $i<$size; $i++) {
  2485. $ret[$ret[$i]] = str_split($ret[++$i]);
  2486. unset($ret[$i-1]);
  2487. unset($ret[$i]);
  2488. }
  2489. return $ret;
  2490. }
  2491. $this->setError(self::ERROR_COMMAND, "Incomplete ACL response");
  2492. return NULL;
  2493. }
  2494. return NULL;
  2495. }
  2496. /**
  2497. * Send the LISTRIGHTS command (RFC4314)
  2498. *
  2499. * @param string $mailbox Mailbox name
  2500. * @param string $user User name
  2501. *
  2502. * @return array List of user rights
  2503. * @since 0.5-beta
  2504. */
  2505. function listRights($mailbox, $user)
  2506. {
  2507. list($code, $response) = $this->execute('LISTRIGHTS', array(
  2508. $this->escape($mailbox), $this->escape($user)));
  2509. if ($code == self::ERROR_OK && preg_match('/^\* LISTRIGHTS /i', $response)) {
  2510. // Parse server response (remove "* LISTRIGHTS ")
  2511. $response = substr($response, 13);
  2512. $ret_mbox = $this->tokenizeResponse($response, 1);
  2513. $ret_user = $this->tokenizeResponse($response, 1);
  2514. $granted = $this->tokenizeResponse($response, 1);
  2515. $optional = trim($response);
  2516. return array(
  2517. 'granted' => str_split($granted),
  2518. 'optional' => explode(' ', $optional),
  2519. );
  2520. }
  2521. return NULL;
  2522. }
  2523. /**
  2524. * Send the MYRIGHTS command (RFC4314)
  2525. *
  2526. * @param string $mailbox Mailbox name
  2527. *
  2528. * @return array MYRIGHTS response on success, NULL on error
  2529. * @since 0.5-beta
  2530. */
  2531. function myRights($mailbox)
  2532. {
  2533. list($code, $response) = $this->execute('MYRIGHTS', array($this->escape($mailbox)));
  2534. if ($code == self::ERROR_OK && preg_match('/^\* MYRIGHTS /i', $response)) {
  2535. // Parse server response (remove "* MYRIGHTS ")
  2536. $response = substr($response, 11);
  2537. $ret_mbox = $this->tokenizeResponse($response, 1);
  2538. $rights = $this->tokenizeResponse($response, 1);
  2539. return str_split($rights);
  2540. }
  2541. return NULL;
  2542. }
  2543. /**
  2544. * Send the SETMETADATA command (RFC5464)
  2545. *
  2546. * @param string $mailbox Mailbox name
  2547. * @param array $entries Entry-value array (use NULL value as NIL)
  2548. *
  2549. * @return boolean True on success, False on failure
  2550. * @since 0.5-beta
  2551. */
  2552. function setMetadata($mailbox, $entries)
  2553. {
  2554. if (!is_array($entries) || empty($entries)) {
  2555. $this->setError(self::ERROR_COMMAND, "Wrong argument for SETMETADATA command");
  2556. return false;
  2557. }
  2558. foreach ($entries as $name => $value) {
  2559. $entries[$name] = $this->escape($name) . ' ' . $this->escape($value);
  2560. }
  2561. $entries = implode(' ', $entries);
  2562. $result = $this->execute('SETMETADATA', array(
  2563. $this->escape($mailbox), '(' . $entries . ')'),
  2564. self::COMMAND_NORESPONSE);
  2565. return ($result == self::ERROR_OK);
  2566. }
  2567. /**
  2568. * Send the SETMETADATA command with NIL values (RFC5464)
  2569. *
  2570. * @param string $mailbox Mailbox name
  2571. * @param array $entries Entry names array
  2572. *
  2573. * @return boolean True on success, False on failure
  2574. *
  2575. * @since 0.5-beta
  2576. */
  2577. function deleteMetadata($mailbox, $entries)
  2578. {
  2579. if (!is_array($entries) && !empty($entries)) {
  2580. $entries = explode(' ', $entries);
  2581. }
  2582. if (empty($entries)) {
  2583. $this->setError(self::ERROR_COMMAND, "Wrong argument for SETMETADATA command");
  2584. return false;
  2585. }
  2586. foreach ($entries as $entry) {
  2587. $data[$entry] = NULL;
  2588. }
  2589. return $this->setMetadata($mailbox, $data);
  2590. }
  2591. /**
  2592. * Send the GETMETADATA command (RFC5464)
  2593. *
  2594. * @param string $mailbox Mailbox name
  2595. * @param array $entries Entries
  2596. * @param array $options Command options (with MAXSIZE and DEPTH keys)
  2597. *
  2598. * @return array GETMETADATA result on success, NULL on error
  2599. *
  2600. * @since 0.5-beta
  2601. */
  2602. function getMetadata($mailbox, $entries, $options=array())
  2603. {
  2604. if (!is_array($entries)) {
  2605. $entries = array($entries);
  2606. }
  2607. // create entries string
  2608. foreach ($entries as $idx => $name) {
  2609. $entries[$idx] = $this->escape($name);
  2610. }
  2611. $optlist = '';
  2612. $entlist = '(' . implode(' ', $entries) . ')';
  2613. // create options string
  2614. if (is_array($options)) {
  2615. $options = array_change_key_case($options, CASE_UPPER);
  2616. $opts = array();
  2617. if (!empty($options['MAXSIZE'])) {
  2618. $opts[] = 'MAXSIZE '.intval($options['MAXSIZE']);
  2619. }
  2620. if (!empty($options['DEPTH'])) {
  2621. $opts[] = 'DEPTH '.intval($options['DEPTH']);
  2622. }
  2623. if ($opts) {
  2624. $optlist = '(' . implode(' ', $opts) . ')';
  2625. }
  2626. }
  2627. $optlist .= ($optlist ? ' ' : '') . $entlist;
  2628. list($code, $response) = $this->execute('GETMETADATA', array(
  2629. $this->escape($mailbox), $optlist));
  2630. if ($code == self::ERROR_OK) {
  2631. $result = array();
  2632. $data = $this->tokenizeResponse($response);
  2633. // The METADATA response can contain multiple entries in a single
  2634. // response or multiple responses for each entry or group of entries
  2635. if (!empty($data) && ($size = count($data))) {
  2636. for ($i=0; $i<$size; $i++) {
  2637. if (isset($mbox) && is_array($data[$i])) {
  2638. $size_sub = count($data[$i]);
  2639. for ($x=0; $x<$size_sub; $x++) {
  2640. $result[$mbox][$data[$i][$x]] = $data[$i][++$x];
  2641. }
  2642. unset($data[$i]);
  2643. }
  2644. else if ($data[$i] == '*') {
  2645. if ($data[$i+1] == 'METADATA') {
  2646. $mbox = $data[$i+2];
  2647. unset($data[$i]); // "*"
  2648. unset($data[++$i]); // "METADATA"
  2649. unset($data[++$i]); // Mailbox
  2650. }
  2651. // get rid of other untagged responses
  2652. else {
  2653. unset($mbox);
  2654. unset($data[$i]);
  2655. }
  2656. }
  2657. else if (isset($mbox)) {
  2658. $result[$mbox][$data[$i]] = $data[++$i];
  2659. unset($data[$i]);
  2660. unset($data[$i-1]);
  2661. }
  2662. else {
  2663. unset($data[$i]);
  2664. }
  2665. }
  2666. }
  2667. return $result;
  2668. }
  2669. return NULL;
  2670. }
  2671. /**
  2672. * Send the SETANNOTATION command (draft-daboo-imap-annotatemore)
  2673. *
  2674. * @param string $mailbox Mailbox name
  2675. * @param array $data Data array where each item is an array with
  2676. * three elements: entry name, attribute name, value
  2677. *
  2678. * @return boolean True on success, False on failure
  2679. * @since 0.5-beta
  2680. */
  2681. function setAnnotation($mailbox, $data)
  2682. {
  2683. if (!is_array($data) || empty($data)) {
  2684. $this->setError(self::ERROR_COMMAND, "Wrong argument for SETANNOTATION command");
  2685. return false;
  2686. }
  2687. foreach ($data as $entry) {
  2688. // ANNOTATEMORE drafts before version 08 require quoted parameters
  2689. $entries[] = sprintf('%s (%s %s)', $this->escape($entry[0], true),
  2690. $this->escape($entry[1], true), $this->escape($entry[2], true));
  2691. }
  2692. $entries = implode(' ', $entries);
  2693. $result = $this->execute('SETANNOTATION', array(
  2694. $this->escape($mailbox), $entries), self::COMMAND_NORESPONSE);
  2695. return ($result == self::ERROR_OK);
  2696. }
  2697. /**
  2698. * Send the SETANNOTATION command with NIL values (draft-daboo-imap-annotatemore)
  2699. *
  2700. * @param string $mailbox Mailbox name
  2701. * @param array $data Data array where each item is an array with
  2702. * two elements: entry name and attribute name
  2703. *
  2704. * @return boolean True on success, False on failure
  2705. *
  2706. * @since 0.5-beta
  2707. */
  2708. function deleteAnnotation($mailbox, $data)
  2709. {
  2710. if (!is_array($data) || empty($data)) {
  2711. $this->setError(self::ERROR_COMMAND, "Wrong argument for SETANNOTATION command");
  2712. return false;
  2713. }
  2714. return $this->setAnnotation($mailbox, $data);
  2715. }
  2716. /**
  2717. * Send the GETANNOTATION command (draft-daboo-imap-annotatemore)
  2718. *
  2719. * @param string $mailbox Mailbox name
  2720. * @param array $entries Entries names
  2721. * @param array $attribs Attribs names
  2722. *
  2723. * @return array Annotations result on success, NULL on error
  2724. *
  2725. * @since 0.5-beta
  2726. */
  2727. function getAnnotation($mailbox, $entries, $attribs)
  2728. {
  2729. if (!is_array($entries)) {
  2730. $entries = array($entries);
  2731. }
  2732. // create entries string
  2733. // ANNOTATEMORE drafts before version 08 require quoted parameters
  2734. foreach ($entries as $idx => $name) {
  2735. $entries[$idx] = $this->escape($name, true);
  2736. }
  2737. $entries = '(' . implode(' ', $entries) . ')';
  2738. if (!is_array($attribs)) {
  2739. $attribs = array($attribs);
  2740. }
  2741. // create entries string
  2742. foreach ($attribs as $idx => $name) {
  2743. $attribs[$idx] = $this->escape($name, true);
  2744. }
  2745. $attribs = '(' . implode(' ', $attribs) . ')';
  2746. list($code, $response) = $this->execute('GETANNOTATION', array(
  2747. $this->escape($mailbox), $entries, $attribs));
  2748. if ($code == self::ERROR_OK) {
  2749. $result = array();
  2750. $data = $this->tokenizeResponse($response);
  2751. // Here we returns only data compatible with METADATA result format
  2752. if (!empty($data) && ($size = count($data))) {
  2753. for ($i=0; $i<$size; $i++) {
  2754. $entry = $data[$i];
  2755. if (isset($mbox) && is_array($entry)) {
  2756. $attribs = $entry;
  2757. $entry = $last_entry;
  2758. }
  2759. else if ($entry == '*') {
  2760. if ($data[$i+1] == 'ANNOTATION') {
  2761. $mbox = $data[$i+2];
  2762. unset($data[$i]); // "*"
  2763. unset($data[++$i]); // "ANNOTATION"
  2764. unset($data[++$i]); // Mailbox
  2765. }
  2766. // get rid of other untagged responses
  2767. else {
  2768. unset($mbox);
  2769. unset($data[$i]);
  2770. }
  2771. continue;
  2772. }
  2773. else if (isset($mbox)) {
  2774. $attribs = $data[++$i];
  2775. }
  2776. else {
  2777. unset($data[$i]);
  2778. continue;
  2779. }
  2780. if (!empty($attribs)) {
  2781. for ($x=0, $len=count($attribs); $x<$len;) {
  2782. $attr = $attribs[$x++];
  2783. $value = $attribs[$x++];
  2784. if ($attr == 'value.priv') {
  2785. $result[$mbox]['/private' . $entry] = $value;
  2786. }
  2787. else if ($attr == 'value.shared') {
  2788. $result[$mbox]['/shared' . $entry] = $value;
  2789. }
  2790. }
  2791. }
  2792. $last_entry = $entry;
  2793. unset($data[$i]);
  2794. }
  2795. }
  2796. return $result;
  2797. }
  2798. return NULL;
  2799. }
  2800. /**
  2801. * Returns BODYSTRUCTURE for the specified message.
  2802. *
  2803. * @param string $mailbox Folder name
  2804. * @param int $id Message sequence number or UID
  2805. * @param bool $is_uid True if $id is an UID
  2806. *
  2807. * @return array/bool Body structure array or False on error.
  2808. * @since 0.6
  2809. */
  2810. function getStructure($mailbox, $id, $is_uid = false)
  2811. {
  2812. $result = $this->fetch($mailbox, $id, $is_uid, array('BODYSTRUCTURE'));
  2813. if (is_array($result)) {
  2814. $result = array_shift($result);
  2815. return $result->bodystructure;
  2816. }
  2817. return false;
  2818. }
  2819. /**
  2820. * Returns data of a message part according to specified structure.
  2821. *
  2822. * @param array $structure Message structure (getStructure() result)
  2823. * @param string $part Message part identifier
  2824. *
  2825. * @return array Part data as hash array (type, encoding, charset, size)
  2826. */
  2827. static function getStructurePartData($structure, $part)
  2828. {
  2829. $part_a = self::getStructurePartArray($structure, $part);
  2830. $data = array();
  2831. if (empty($part_a)) {
  2832. return $data;
  2833. }
  2834. // content-type
  2835. if (is_array($part_a[0])) {
  2836. $data['type'] = 'multipart';
  2837. }
  2838. else {
  2839. $data['type'] = strtolower($part_a[0]);
  2840. // encoding
  2841. $data['encoding'] = strtolower($part_a[5]);
  2842. // charset
  2843. if (is_array($part_a[2])) {
  2844. while (list($key, $val) = each($part_a[2])) {
  2845. if (strcasecmp($val, 'charset') == 0) {
  2846. $data['charset'] = $part_a[2][$key+1];
  2847. break;
  2848. }
  2849. }
  2850. }
  2851. }
  2852. // size
  2853. $data['size'] = intval($part_a[6]);
  2854. return $data;
  2855. }
  2856. static function getStructurePartArray($a, $part)
  2857. {
  2858. if (!is_array($a)) {
  2859. return false;
  2860. }
  2861. if (empty($part)) {
  2862. return $a;
  2863. }
  2864. $ctype = is_string($a[0]) && is_string($a[1]) ? $a[0] . '/' . $a[1] : '';
  2865. if (strcasecmp($ctype, 'message/rfc822') == 0) {
  2866. $a = $a[8];
  2867. }
  2868. if (strpos($part, '.') > 0) {
  2869. $orig_part = $part;
  2870. $pos = strpos($part, '.');
  2871. $rest = substr($orig_part, $pos+1);
  2872. $part = substr($orig_part, 0, $pos);
  2873. return self::getStructurePartArray($a[$part-1], $rest);
  2874. }
  2875. else if ($part > 0) {
  2876. if (is_array($a[$part-1]))
  2877. return $a[$part-1];
  2878. else
  2879. return $a;
  2880. }
  2881. }
  2882. /**
  2883. * Creates next command identifier (tag)
  2884. *
  2885. * @return string Command identifier
  2886. * @since 0.5-beta
  2887. */
  2888. function nextTag()
  2889. {
  2890. $this->cmd_num++;
  2891. $this->cmd_tag = sprintf('A%04d', $this->cmd_num);
  2892. return $this->cmd_tag;
  2893. }
  2894. /**
  2895. * Sends IMAP command and parses result
  2896. *
  2897. * @param string $command IMAP command
  2898. * @param array $arguments Command arguments
  2899. * @param int $options Execution options
  2900. *
  2901. * @return mixed Response code or list of response code and data
  2902. * @since 0.5-beta
  2903. */
  2904. function execute($command, $arguments=array(), $options=0)
  2905. {
  2906. $tag = $this->nextTag();
  2907. $query = $tag . ' ' . $command;
  2908. $noresp = ($options & self::COMMAND_NORESPONSE);
  2909. $response = $noresp ? null : '';
  2910. if (!empty($arguments)) {
  2911. foreach ($arguments as $arg) {
  2912. $query .= ' ' . self::r_implode($arg);
  2913. }
  2914. }
  2915. // Send command
  2916. if (!$this->putLineC($query)) {
  2917. $this->setError(self::ERROR_COMMAND, "Unable to send command: $query");
  2918. return $noresp ? self::ERROR_COMMAND : array(self::ERROR_COMMAND, '');
  2919. }
  2920. // Parse response
  2921. do {
  2922. $line = $this->readLine(4096);
  2923. if ($response !== null) {
  2924. $response .= $line;
  2925. }
  2926. } while (!$this->startsWith($line, $tag . ' ', true, true));
  2927. $code = $this->parseResult($line, $command . ': ');
  2928. // Remove last line from response
  2929. if ($response) {
  2930. $line_len = min(strlen($response), strlen($line) + 2);
  2931. $response = substr($response, 0, -$line_len);
  2932. }
  2933. // optional CAPABILITY response
  2934. if (($options & self::COMMAND_CAPABILITY) && $code == self::ERROR_OK
  2935. && preg_match('/\[CAPABILITY ([^]]+)\]/i', $line, $matches)
  2936. ) {
  2937. $this->parseCapability($matches[1], true);
  2938. }
  2939. // return last line only (without command tag, result and response code)
  2940. if ($line && ($options & self::COMMAND_LASTLINE)) {
  2941. $response = preg_replace("/^$tag (OK|NO|BAD|BYE|PREAUTH)?\s*(\[[a-z-]+\])?\s*/i", '', trim($line));
  2942. }
  2943. return $noresp ? $code : array($code, $response);
  2944. }
  2945. /**
  2946. * Splits IMAP response into string tokens
  2947. *
  2948. * @param string &$str The IMAP's server response
  2949. * @param int $num Number of tokens to return
  2950. *
  2951. * @return mixed Tokens array or string if $num=1
  2952. * @since 0.5-beta
  2953. */
  2954. static function tokenizeResponse(&$str, $num=0)
  2955. {
  2956. $result = array();
  2957. while (!$num || count($result) < $num) {
  2958. // remove spaces from the beginning of the string
  2959. $str = ltrim($str);
  2960. switch ($str[0]) {
  2961. // String literal
  2962. case '{':
  2963. if (($epos = strpos($str, "}\r\n", 1)) == false) {
  2964. // error
  2965. }
  2966. if (!is_numeric(($bytes = substr($str, 1, $epos - 1)))) {
  2967. // error
  2968. }
  2969. $result[] = $bytes ? substr($str, $epos + 3, $bytes) : '';
  2970. // Advance the string
  2971. $str = substr($str, $epos + 3 + $bytes);
  2972. break;
  2973. // Quoted string
  2974. case '"':
  2975. $len = strlen($str);
  2976. for ($pos=1; $pos<$len; $pos++) {
  2977. if ($str[$pos] == '"') {
  2978. break;
  2979. }
  2980. if ($str[$pos] == "\\") {
  2981. if ($str[$pos + 1] == '"' || $str[$pos + 1] == "\\") {
  2982. $pos++;
  2983. }
  2984. }
  2985. }
  2986. if ($str[$pos] != '"') {
  2987. // error
  2988. }
  2989. // we need to strip slashes for a quoted string
  2990. $result[] = stripslashes(substr($str, 1, $pos - 1));
  2991. $str = substr($str, $pos + 1);
  2992. break;
  2993. // Parenthesized list
  2994. case '(':
  2995. case '[':
  2996. $str = substr($str, 1);
  2997. $result[] = self::tokenizeResponse($str);
  2998. break;
  2999. case ')':
  3000. case ']':
  3001. $str = substr($str, 1);
  3002. return $result;
  3003. break;
  3004. // String atom, number, NIL, *, %
  3005. default:
  3006. // empty string
  3007. if ($str === '' || $str === null) {
  3008. break 2;
  3009. }
  3010. // excluded chars: SP, CTL, ), [, ]
  3011. if (preg_match('/^([^\x00-\x20\x29\x5B\x5D\x7F]+)/', $str, $m)) {
  3012. $result[] = $m[1] == 'NIL' ? NULL : $m[1];
  3013. $str = substr($str, strlen($m[1]));
  3014. }
  3015. break;
  3016. }
  3017. }
  3018. return $num == 1 ? $result[0] : $result;
  3019. }
  3020. static function r_implode($element)
  3021. {
  3022. $string = '';
  3023. if (is_array($element)) {
  3024. reset($element);
  3025. while (list($key, $value) = each($element)) {
  3026. $string .= ' ' . self::r_implode($value);
  3027. }
  3028. }
  3029. else {
  3030. return $element;
  3031. }
  3032. return '(' . trim($string) . ')';
  3033. }
  3034. /**
  3035. * Converts message identifiers array into sequence-set syntax
  3036. *
  3037. * @param array $messages Message identifiers
  3038. * @param bool $force Forces compression of any size
  3039. *
  3040. * @return string Compressed sequence-set
  3041. */
  3042. static function compressMessageSet($messages, $force=false)
  3043. {
  3044. // given a comma delimited list of independent mid's,
  3045. // compresses by grouping sequences together
  3046. if (!is_array($messages)) {
  3047. // if less than 255 bytes long, let's not bother
  3048. if (!$force && strlen($messages)<255) {
  3049. return $messages;
  3050. }
  3051. // see if it's already been compressed
  3052. if (strpos($messages, ':') !== false) {
  3053. return $messages;
  3054. }
  3055. // separate, then sort
  3056. $messages = explode(',', $messages);
  3057. }
  3058. sort($messages);
  3059. $result = array();
  3060. $start = $prev = $messages[0];
  3061. foreach ($messages as $id) {
  3062. $incr = $id - $prev;
  3063. if ($incr > 1) { // found a gap
  3064. if ($start == $prev) {
  3065. $result[] = $prev; // push single id
  3066. } else {
  3067. $result[] = $start . ':' . $prev; // push sequence as start_id:end_id
  3068. }
  3069. $start = $id; // start of new sequence
  3070. }
  3071. $prev = $id;
  3072. }
  3073. // handle the last sequence/id
  3074. if ($start == $prev) {
  3075. $result[] = $prev;
  3076. } else {
  3077. $result[] = $start.':'.$prev;
  3078. }
  3079. // return as comma separated string
  3080. return implode(',', $result);
  3081. }
  3082. /**
  3083. * Converts message sequence-set into array
  3084. *
  3085. * @param string $messages Message identifiers
  3086. *
  3087. * @return array List of message identifiers
  3088. */
  3089. static function uncompressMessageSet($messages)
  3090. {
  3091. if (empty($messages)) {
  3092. return array();
  3093. }
  3094. $result = array();
  3095. $messages = explode(',', $messages);
  3096. foreach ($messages as $idx => $part) {
  3097. $items = explode(':', $part);
  3098. $max = max($items[0], $items[1]);
  3099. for ($x=$items[0]; $x<=$max; $x++) {
  3100. $result[] = (int)$x;
  3101. }
  3102. unset($messages[$idx]);
  3103. }
  3104. return $result;
  3105. }
  3106. private function _xor($string, $string2)
  3107. {
  3108. $result = '';
  3109. $size = strlen($string);
  3110. for ($i=0; $i<$size; $i++) {
  3111. $result .= chr(ord($string[$i]) ^ ord($string2[$i]));
  3112. }
  3113. return $result;
  3114. }
  3115. /**
  3116. * Converts datetime string into unix timestamp
  3117. *
  3118. * @param string $date Date string
  3119. *
  3120. * @return int Unix timestamp
  3121. */
  3122. static function strToTime($date)
  3123. {
  3124. // support non-standard "GMTXXXX" literal
  3125. $date = preg_replace('/GMT\s*([+-][0-9]+)/', '\\1', $date);
  3126. // if date parsing fails, we have a date in non-rfc format
  3127. // remove token from the end and try again
  3128. while (($ts = intval(@strtotime($date))) <= 0) {
  3129. $d = explode(' ', $date);
  3130. array_pop($d);
  3131. if (empty($d)) {
  3132. break;
  3133. }
  3134. $date = implode(' ', $d);
  3135. }
  3136. return $ts < 0 ? 0 : $ts;
  3137. }
  3138. /**
  3139. * CAPABILITY response parser
  3140. */
  3141. private function parseCapability($str, $trusted=false)
  3142. {
  3143. $str = preg_replace('/^\* CAPABILITY /i', '', $str);
  3144. $this->capability = explode(' ', strtoupper($str));
  3145. if (!isset($this->prefs['literal+']) && in_array('LITERAL+', $this->capability)) {
  3146. $this->prefs['literal+'] = true;
  3147. }
  3148. if ($trusted) {
  3149. $this->capability_readed = true;
  3150. }
  3151. }
  3152. /**
  3153. * Escapes a string when it contains special characters (RFC3501)
  3154. *
  3155. * @param string $string IMAP string
  3156. * @param boolean $force_quotes Forces string quoting (for atoms)
  3157. *
  3158. * @return string String atom, quoted-string or string literal
  3159. * @todo lists
  3160. */
  3161. static function escape($string, $force_quotes=false)
  3162. {
  3163. if ($string === null) {
  3164. return 'NIL';
  3165. }
  3166. if ($string === '') {
  3167. return '""';
  3168. }
  3169. // atom-string (only safe characters)
  3170. if (!$force_quotes && !preg_match('/[\x00-\x20\x22\x25\x28-\x2A\x5B-\x5D\x7B\x7D\x80-\xFF]/', $string)) {
  3171. return $string;
  3172. }
  3173. // quoted-string
  3174. if (!preg_match('/[\r\n\x00\x80-\xFF]/', $string)) {
  3175. return '"' . addcslashes($string, '\\"') . '"';
  3176. }
  3177. // literal-string
  3178. return sprintf("{%d}\r\n%s", strlen($string), $string);
  3179. }
  3180. /**
  3181. * Unescapes quoted-string
  3182. *
  3183. * @param string $string IMAP string
  3184. *
  3185. * @return string String
  3186. */
  3187. static function unEscape($string)
  3188. {
  3189. return stripslashes($string);
  3190. }
  3191. /**
  3192. * Set the value of the debugging flag.
  3193. *
  3194. * @param boolean $debug New value for the debugging flag.
  3195. *
  3196. * @since 0.5-stable
  3197. */
  3198. function setDebug($debug, $handler = null)
  3199. {
  3200. $this->_debug = $debug;
  3201. $this->_debug_handler = $handler;
  3202. }
  3203. /**
  3204. * Write the given debug text to the current debug output handler.
  3205. *
  3206. * @param string $message Debug mesage text.
  3207. *
  3208. * @since 0.5-stable
  3209. */
  3210. private function debug($message)
  3211. {
  3212. if ($this->resourceid) {
  3213. $message = sprintf('[%s] %s', $this->resourceid, $message);
  3214. }
  3215. if ($this->_debug_handler) {
  3216. call_user_func_array($this->_debug_handler, array(&$this, $message));
  3217. } else {
  3218. echo "DEBUG: $message\n";
  3219. }
  3220. }
  3221. }