RoundCube Webmail
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

4977 lines
159 KiB

20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
15 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
18 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
15 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
20 years ago
  1. <?php
  2. /*
  3. +-----------------------------------------------------------------------+
  4. | program/include/rcube_imap.php |
  5. | |
  6. | This file is part of the Roundcube Webmail client |
  7. | Copyright (C) 2005-2010, The Roundcube Dev Team |
  8. | Licensed under the GNU GPL |
  9. | |
  10. | PURPOSE: |
  11. | IMAP Engine |
  12. | |
  13. +-----------------------------------------------------------------------+
  14. | Author: Thomas Bruederli <roundcube@gmail.com> |
  15. | Author: Aleksander Machniak <alec@alec.pl> |
  16. +-----------------------------------------------------------------------+
  17. $Id$
  18. */
  19. /**
  20. * Interface class for accessing an IMAP server
  21. *
  22. * @package Mail
  23. * @author Thomas Bruederli <roundcube@gmail.com>
  24. * @author Aleksander Machniak <alec@alec.pl>
  25. * @version 2.0
  26. */
  27. class rcube_imap
  28. {
  29. public $debug_level = 1;
  30. public $skip_deleted = false;
  31. public $page_size = 10;
  32. public $list_page = 1;
  33. public $threading = false;
  34. public $fetch_add_headers = '';
  35. public $get_all_headers = false;
  36. /**
  37. * Instance of rcube_imap_generic
  38. *
  39. * @var rcube_imap_generic
  40. */
  41. public $conn;
  42. /**
  43. * Instance of rcube_mdb2
  44. *
  45. * @var rcube_mdb2
  46. */
  47. private $db;
  48. /**
  49. * Instance of rcube_cache
  50. *
  51. * @var rcube_cache
  52. */
  53. private $cache;
  54. private $mailbox = 'INBOX';
  55. private $delimiter = NULL;
  56. private $namespace = NULL;
  57. private $sort_field = '';
  58. private $sort_order = 'DESC';
  59. private $default_charset = 'ISO-8859-1';
  60. private $struct_charset = NULL;
  61. private $default_folders = array('INBOX');
  62. private $messages_caching = false;
  63. private $icache = array();
  64. private $uid_id_map = array();
  65. private $msg_headers = array();
  66. public $search_set = NULL;
  67. public $search_string = '';
  68. private $search_charset = '';
  69. private $search_sort_field = '';
  70. private $search_threads = false;
  71. private $search_sorted = false;
  72. private $db_header_fields = array('idx', 'uid', 'subject', 'from', 'to', 'cc', 'date', 'size');
  73. private $options = array('auth_method' => 'check');
  74. private $host, $user, $pass, $port, $ssl;
  75. private $caching = false;
  76. /**
  77. * All (additional) headers used (in any way) by Roundcube
  78. * Not listed here: DATE, FROM, TO, CC, REPLY-TO, SUBJECT, CONTENT-TYPE, LIST-POST
  79. * (used for messages listing) are hardcoded in rcube_imap_generic::fetchHeaders()
  80. *
  81. * @var array
  82. * @see rcube_imap::fetch_add_headers
  83. */
  84. private $all_headers = array(
  85. 'IN-REPLY-TO',
  86. 'BCC',
  87. 'MESSAGE-ID',
  88. 'CONTENT-TRANSFER-ENCODING',
  89. 'REFERENCES',
  90. 'X-PRIORITY',
  91. 'X-DRAFT-INFO',
  92. 'MAIL-FOLLOWUP-TO',
  93. 'MAIL-REPLY-TO',
  94. 'RETURN-PATH',
  95. );
  96. const UNKNOWN = 0;
  97. const NOPERM = 1;
  98. const READONLY = 2;
  99. const TRYCREATE = 3;
  100. const INUSE = 4;
  101. const OVERQUOTA = 5;
  102. const ALREADYEXISTS = 6;
  103. const NONEXISTENT = 7;
  104. const CONTACTADMIN = 8;
  105. /**
  106. * Object constructor.
  107. */
  108. function __construct()
  109. {
  110. $this->conn = new rcube_imap_generic();
  111. // Set namespace and delimiter from session,
  112. // so some methods would work before connection
  113. if (isset($_SESSION['imap_namespace']))
  114. $this->namespace = $_SESSION['imap_namespace'];
  115. if (isset($_SESSION['imap_delimiter']))
  116. $this->delimiter = $_SESSION['imap_delimiter'];
  117. }
  118. /**
  119. * Connect to an IMAP server
  120. *
  121. * @param string $host Host to connect
  122. * @param string $user Username for IMAP account
  123. * @param string $pass Password for IMAP account
  124. * @param integer $port Port to connect to
  125. * @param string $use_ssl SSL schema (either ssl or tls) or null if plain connection
  126. * @return boolean TRUE on success, FALSE on failure
  127. * @access public
  128. */
  129. function connect($host, $user, $pass, $port=143, $use_ssl=null)
  130. {
  131. // check for OpenSSL support in PHP build
  132. if ($use_ssl && extension_loaded('openssl'))
  133. $this->options['ssl_mode'] = $use_ssl == 'imaps' ? 'ssl' : $use_ssl;
  134. else if ($use_ssl) {
  135. raise_error(array('code' => 403, 'type' => 'imap',
  136. 'file' => __FILE__, 'line' => __LINE__,
  137. 'message' => "OpenSSL not available"), true, false);
  138. $port = 143;
  139. }
  140. $this->options['port'] = $port;
  141. if ($this->options['debug']) {
  142. $this->conn->setDebug(true, array($this, 'debug_handler'));
  143. $this->options['ident'] = array(
  144. 'name' => 'Roundcube Webmail',
  145. 'version' => RCMAIL_VERSION,
  146. 'php' => PHP_VERSION,
  147. 'os' => PHP_OS,
  148. 'command' => $_SERVER['REQUEST_URI'],
  149. );
  150. }
  151. $attempt = 0;
  152. do {
  153. $data = rcmail::get_instance()->plugins->exec_hook('imap_connect',
  154. array('host' => $host, 'user' => $user, 'attempt' => ++$attempt));
  155. if (!empty($data['pass']))
  156. $pass = $data['pass'];
  157. $this->conn->connect($data['host'], $data['user'], $pass, $this->options);
  158. } while(!$this->conn->connected() && $data['retry']);
  159. $this->host = $data['host'];
  160. $this->user = $data['user'];
  161. $this->pass = $pass;
  162. $this->port = $port;
  163. $this->ssl = $use_ssl;
  164. if ($this->conn->connected()) {
  165. // get namespace and delimiter
  166. $this->set_env();
  167. return true;
  168. }
  169. // write error log
  170. else if ($this->conn->error) {
  171. if ($pass && $user) {
  172. $message = sprintf("Login failed for %s from %s. %s",
  173. $user, rcmail_remote_ip(), $this->conn->error);
  174. raise_error(array('code' => 403, 'type' => 'imap',
  175. 'file' => __FILE__, 'line' => __LINE__,
  176. 'message' => $message), true, false);
  177. }
  178. }
  179. return false;
  180. }
  181. /**
  182. * Close IMAP connection
  183. * Usually done on script shutdown
  184. *
  185. * @access public
  186. */
  187. function close()
  188. {
  189. $this->conn->closeConnection();
  190. }
  191. /**
  192. * Close IMAP connection and re-connect
  193. * This is used to avoid some strange socket errors when talking to Courier IMAP
  194. *
  195. * @access public
  196. */
  197. function reconnect()
  198. {
  199. $this->conn->closeConnection();
  200. $connected = $this->connect($this->host, $this->user, $this->pass, $this->port, $this->ssl);
  201. // issue SELECT command to restore connection status
  202. if ($connected && strlen($this->mailbox))
  203. $this->conn->select($this->mailbox);
  204. }
  205. /**
  206. * Returns code of last error
  207. *
  208. * @return int Error code
  209. */
  210. function get_error_code()
  211. {
  212. return $this->conn->errornum;
  213. }
  214. /**
  215. * Returns message of last error
  216. *
  217. * @return string Error message
  218. */
  219. function get_error_str()
  220. {
  221. return $this->conn->error;
  222. }
  223. /**
  224. * Returns code of last command response
  225. *
  226. * @return int Response code
  227. */
  228. function get_response_code()
  229. {
  230. switch ($this->conn->resultcode) {
  231. case 'NOPERM':
  232. return self::NOPERM;
  233. case 'READ-ONLY':
  234. return self::READONLY;
  235. case 'TRYCREATE':
  236. return self::TRYCREATE;
  237. case 'INUSE':
  238. return self::INUSE;
  239. case 'OVERQUOTA':
  240. return self::OVERQUOTA;
  241. case 'ALREADYEXISTS':
  242. return self::ALREADYEXISTS;
  243. case 'NONEXISTENT':
  244. return self::NONEXISTENT;
  245. case 'CONTACTADMIN':
  246. return self::CONTACTADMIN;
  247. default:
  248. return self::UNKNOWN;
  249. }
  250. }
  251. /**
  252. * Returns last command response
  253. *
  254. * @return string Response
  255. */
  256. function get_response_str()
  257. {
  258. return $this->conn->result;
  259. }
  260. /**
  261. * Set options to be used in rcube_imap_generic::connect()
  262. *
  263. * @param array $opt Options array
  264. */
  265. function set_options($opt)
  266. {
  267. $this->options = array_merge($this->options, (array)$opt);
  268. }
  269. /**
  270. * Set default message charset
  271. *
  272. * This will be used for message decoding if a charset specification is not available
  273. *
  274. * @param string $cs Charset string
  275. * @access public
  276. */
  277. function set_charset($cs)
  278. {
  279. $this->default_charset = $cs;
  280. }
  281. /**
  282. * This list of folders will be listed above all other folders
  283. *
  284. * @param array $arr Indexed list of folder names
  285. * @access public
  286. */
  287. function set_default_mailboxes($arr)
  288. {
  289. if (is_array($arr)) {
  290. $this->default_folders = $arr;
  291. // add inbox if not included
  292. if (!in_array('INBOX', $this->default_folders))
  293. array_unshift($this->default_folders, 'INBOX');
  294. }
  295. }
  296. /**
  297. * Set internal mailbox reference.
  298. *
  299. * All operations will be perfomed on this mailbox/folder
  300. *
  301. * @param string $mailbox Mailbox/Folder name
  302. * @access public
  303. */
  304. function set_mailbox($mailbox)
  305. {
  306. if ($this->mailbox == $mailbox)
  307. return;
  308. $this->mailbox = $mailbox;
  309. // clear messagecount cache for this mailbox
  310. $this->_clear_messagecount($mailbox);
  311. }
  312. /**
  313. * Forces selection of a mailbox
  314. *
  315. * @param string $mailbox Mailbox/Folder name
  316. * @access public
  317. */
  318. function select_mailbox($mailbox=null)
  319. {
  320. if (!strlen($mailbox)) {
  321. $mailbox = $this->mailbox;
  322. }
  323. $selected = $this->conn->select($mailbox);
  324. if ($selected && $this->mailbox != $mailbox) {
  325. // clear messagecount cache for this mailbox
  326. $this->_clear_messagecount($mailbox);
  327. $this->mailbox = $mailbox;
  328. }
  329. }
  330. /**
  331. * Set internal list page
  332. *
  333. * @param number $page Page number to list
  334. * @access public
  335. */
  336. function set_page($page)
  337. {
  338. $this->list_page = (int)$page;
  339. }
  340. /**
  341. * Set internal page size
  342. *
  343. * @param number $size Number of messages to display on one page
  344. * @access public
  345. */
  346. function set_pagesize($size)
  347. {
  348. $this->page_size = (int)$size;
  349. }
  350. /**
  351. * Save a set of message ids for future message listing methods
  352. *
  353. * @param string IMAP Search query
  354. * @param array List of message ids or NULL if empty
  355. * @param string Charset of search string
  356. * @param string Sorting field
  357. * @param string True if set is sorted (SORT was used for searching)
  358. */
  359. function set_search_set($str=null, $msgs=null, $charset=null, $sort_field=null, $threads=false, $sorted=false)
  360. {
  361. if (is_array($str) && $msgs == null)
  362. list($str, $msgs, $charset, $sort_field, $threads) = $str;
  363. if ($msgs === false)
  364. $msgs = array();
  365. else if ($msgs != null && !is_array($msgs))
  366. $msgs = explode(',', $msgs);
  367. $this->search_string = $str;
  368. $this->search_set = $msgs;
  369. $this->search_charset = $charset;
  370. $this->search_sort_field = $sort_field;
  371. $this->search_threads = $threads;
  372. $this->search_sorted = $sorted;
  373. }
  374. /**
  375. * Return the saved search set as hash array
  376. * @return array Search set
  377. */
  378. function get_search_set()
  379. {
  380. return array($this->search_string,
  381. $this->search_set,
  382. $this->search_charset,
  383. $this->search_sort_field,
  384. $this->search_threads,
  385. $this->search_sorted,
  386. );
  387. }
  388. /**
  389. * Returns the currently used mailbox name
  390. *
  391. * @return string Name of the mailbox/folder
  392. * @access public
  393. */
  394. function get_mailbox_name()
  395. {
  396. return $this->conn->connected() ? $this->mailbox : '';
  397. }
  398. /**
  399. * Returns the IMAP server's capability
  400. *
  401. * @param string $cap Capability name
  402. * @return mixed Capability value or TRUE if supported, FALSE if not
  403. * @access public
  404. */
  405. function get_capability($cap)
  406. {
  407. return $this->conn->getCapability(strtoupper($cap));
  408. }
  409. /**
  410. * Sets threading flag to the best supported THREAD algorithm
  411. *
  412. * @param boolean $enable TRUE to enable and FALSE
  413. * @return string Algorithm or false if THREAD is not supported
  414. * @access public
  415. */
  416. function set_threading($enable=false)
  417. {
  418. $this->threading = false;
  419. if ($enable && ($caps = $this->get_capability('THREAD'))) {
  420. if (in_array('REFS', $caps))
  421. $this->threading = 'REFS';
  422. else if (in_array('REFERENCES', $caps))
  423. $this->threading = 'REFERENCES';
  424. else if (in_array('ORDEREDSUBJECT', $caps))
  425. $this->threading = 'ORDEREDSUBJECT';
  426. }
  427. return $this->threading;
  428. }
  429. /**
  430. * Checks the PERMANENTFLAGS capability of the current mailbox
  431. * and returns true if the given flag is supported by the IMAP server
  432. *
  433. * @param string $flag Permanentflag name
  434. * @return boolean True if this flag is supported
  435. * @access public
  436. */
  437. function check_permflag($flag)
  438. {
  439. $flag = strtoupper($flag);
  440. $imap_flag = $this->conn->flags[$flag];
  441. return (in_array_nocase($imap_flag, $this->conn->data['PERMANENTFLAGS']));
  442. }
  443. /**
  444. * Returns the delimiter that is used by the IMAP server for folder separation
  445. *
  446. * @return string Delimiter string
  447. * @access public
  448. */
  449. function get_hierarchy_delimiter()
  450. {
  451. return $this->delimiter;
  452. }
  453. /**
  454. * Get namespace
  455. *
  456. * @param string $name Namespace array index: personal, other, shared, prefix
  457. *
  458. * @return array Namespace data
  459. * @access public
  460. */
  461. function get_namespace($name=null)
  462. {
  463. $ns = $this->namespace;
  464. if ($name) {
  465. return isset($ns[$name]) ? $ns[$name] : null;
  466. }
  467. unset($ns['prefix']);
  468. return $ns;
  469. }
  470. /**
  471. * Sets delimiter and namespaces
  472. *
  473. * @access private
  474. */
  475. private function set_env()
  476. {
  477. if ($this->delimiter !== null && $this->namespace !== null) {
  478. return;
  479. }
  480. $config = rcmail::get_instance()->config;
  481. $imap_personal = $config->get('imap_ns_personal');
  482. $imap_other = $config->get('imap_ns_other');
  483. $imap_shared = $config->get('imap_ns_shared');
  484. $imap_delimiter = $config->get('imap_delimiter');
  485. if (!$this->conn->connected())
  486. return;
  487. $ns = $this->conn->getNamespace();
  488. // Set namespaces (NAMESPACE supported)
  489. if (is_array($ns)) {
  490. $this->namespace = $ns;
  491. }
  492. else {
  493. $this->namespace = array(
  494. 'personal' => NULL,
  495. 'other' => NULL,
  496. 'shared' => NULL,
  497. );
  498. }
  499. if ($imap_delimiter) {
  500. $this->delimiter = $imap_delimiter;
  501. }
  502. if (empty($this->delimiter)) {
  503. $this->delimiter = $this->namespace['personal'][0][1];
  504. }
  505. if (empty($this->delimiter)) {
  506. $this->delimiter = $this->conn->getHierarchyDelimiter();
  507. }
  508. if (empty($this->delimiter)) {
  509. $this->delimiter = '/';
  510. }
  511. // Overwrite namespaces
  512. if ($imap_personal !== null) {
  513. $this->namespace['personal'] = NULL;
  514. foreach ((array)$imap_personal as $dir) {
  515. $this->namespace['personal'][] = array($dir, $this->delimiter);
  516. }
  517. }
  518. if ($imap_other !== null) {
  519. $this->namespace['other'] = NULL;
  520. foreach ((array)$imap_other as $dir) {
  521. if ($dir) {
  522. $this->namespace['other'][] = array($dir, $this->delimiter);
  523. }
  524. }
  525. }
  526. if ($imap_shared !== null) {
  527. $this->namespace['shared'] = NULL;
  528. foreach ((array)$imap_shared as $dir) {
  529. if ($dir) {
  530. $this->namespace['shared'][] = array($dir, $this->delimiter);
  531. }
  532. }
  533. }
  534. // Find personal namespace prefix for mod_mailbox()
  535. // Prefix can be removed when there is only one personal namespace
  536. if (is_array($this->namespace['personal']) && count($this->namespace['personal']) == 1) {
  537. $this->namespace['prefix'] = $this->namespace['personal'][0][0];
  538. }
  539. $_SESSION['imap_namespace'] = $this->namespace;
  540. $_SESSION['imap_delimiter'] = $this->delimiter;
  541. }
  542. /**
  543. * Get message count for a specific mailbox
  544. *
  545. * @param string $mailbox Mailbox/folder name
  546. * @param string $mode Mode for count [ALL|THREADS|UNSEEN|RECENT]
  547. * @param boolean $force Force reading from server and update cache
  548. * @param boolean $status Enables storing folder status info (max UID/count),
  549. * required for mailbox_status()
  550. * @return int Number of messages
  551. * @access public
  552. */
  553. function messagecount($mailbox='', $mode='ALL', $force=false, $status=true)
  554. {
  555. if (!strlen($mailbox)) {
  556. $mailbox = $this->mailbox;
  557. }
  558. return $this->_messagecount($mailbox, $mode, $force, $status);
  559. }
  560. /**
  561. * Private method for getting nr of messages
  562. *
  563. * @param string $mailbox Mailbox name
  564. * @param string $mode Mode for count [ALL|THREADS|UNSEEN|RECENT]
  565. * @param boolean $force Force reading from server and update cache
  566. * @param boolean $status Enables storing folder status info (max UID/count),
  567. * required for mailbox_status()
  568. * @return int Number of messages
  569. * @access private
  570. * @see rcube_imap::messagecount()
  571. */
  572. private function _messagecount($mailbox, $mode='ALL', $force=false, $status=true)
  573. {
  574. $mode = strtoupper($mode);
  575. // count search set
  576. if ($this->search_string && $mailbox == $this->mailbox && ($mode == 'ALL' || $mode == 'THREADS') && !$force) {
  577. if ($this->search_threads)
  578. return $mode == 'ALL' ? count((array)$this->search_set['depth']) : count((array)$this->search_set['tree']);
  579. else
  580. return count((array)$this->search_set);
  581. }
  582. $a_mailbox_cache = $this->get_cache('messagecount');
  583. // return cached value
  584. if (!$force && is_array($a_mailbox_cache[$mailbox]) && isset($a_mailbox_cache[$mailbox][$mode]))
  585. return $a_mailbox_cache[$mailbox][$mode];
  586. if (!is_array($a_mailbox_cache[$mailbox]))
  587. $a_mailbox_cache[$mailbox] = array();
  588. if ($mode == 'THREADS') {
  589. $res = $this->_threadcount($mailbox, $msg_count);
  590. $count = $res['count'];
  591. if ($status) {
  592. $this->set_folder_stats($mailbox, 'cnt', $res['msgcount']);
  593. $this->set_folder_stats($mailbox, 'maxuid', $res['maxuid'] ? $this->_id2uid($res['maxuid'], $mailbox) : 0);
  594. }
  595. }
  596. // RECENT count is fetched a bit different
  597. else if ($mode == 'RECENT') {
  598. $count = $this->conn->countRecent($mailbox);
  599. }
  600. // use SEARCH for message counting
  601. else if ($this->skip_deleted) {
  602. $search_str = "ALL UNDELETED";
  603. $keys = array('COUNT');
  604. $need_uid = false;
  605. if ($mode == 'UNSEEN') {
  606. $search_str .= " UNSEEN";
  607. }
  608. else {
  609. if ($this->messages_caching) {
  610. $keys[] = 'ALL';
  611. }
  612. if ($status) {
  613. $keys[] = 'MAX';
  614. $need_uid = true;
  615. }
  616. }
  617. // get message count using (E)SEARCH
  618. // not very performant but more precise (using UNDELETED)
  619. $index = $this->conn->search($mailbox, $search_str, $need_uid, $keys);
  620. $count = is_array($index) ? $index['COUNT'] : 0;
  621. if ($mode == 'ALL') {
  622. if ($need_uid && $this->messages_caching) {
  623. // Save messages index for check_cache_status()
  624. $this->icache['all_undeleted_idx'] = $index['ALL'];
  625. }
  626. if ($status) {
  627. $this->set_folder_stats($mailbox, 'cnt', $count);
  628. $this->set_folder_stats($mailbox, 'maxuid', is_array($index) ? $index['MAX'] : 0);
  629. }
  630. }
  631. }
  632. else {
  633. if ($mode == 'UNSEEN')
  634. $count = $this->conn->countUnseen($mailbox);
  635. else {
  636. $count = $this->conn->countMessages($mailbox);
  637. if ($status) {
  638. $this->set_folder_stats($mailbox,'cnt', $count);
  639. $this->set_folder_stats($mailbox, 'maxuid', $count ? $this->_id2uid($count, $mailbox) : 0);
  640. }
  641. }
  642. }
  643. $a_mailbox_cache[$mailbox][$mode] = (int)$count;
  644. // write back to cache
  645. $this->update_cache('messagecount', $a_mailbox_cache);
  646. return (int)$count;
  647. }
  648. /**
  649. * Private method for getting nr of threads
  650. *
  651. * @param string $mailbox Folder name
  652. *
  653. * @returns array Array containing items: 'count' - threads count,
  654. * 'msgcount' = messages count, 'maxuid' = max. UID in the set
  655. * @access private
  656. */
  657. private function _threadcount($mailbox)
  658. {
  659. $result = array();
  660. if (!empty($this->icache['threads'])) {
  661. $dcount = count($this->icache['threads']['depth']);
  662. $result = array(
  663. 'count' => count($this->icache['threads']['tree']),
  664. 'msgcount' => $dcount,
  665. 'maxuid' => $dcount ? max(array_keys($this->icache['threads']['depth'])) : 0,
  666. );
  667. }
  668. else if (is_array($result = $this->_fetch_threads($mailbox))) {
  669. $dcount = count($result[1]);
  670. $result = array(
  671. 'count' => count($result[0]),
  672. 'msgcount' => $dcount,
  673. 'maxuid' => $dcount ? max(array_keys($result[1])) : 0,
  674. );
  675. }
  676. return $result;
  677. }
  678. /**
  679. * Public method for listing headers
  680. * convert mailbox name with root dir first
  681. *
  682. * @param string $mailbox Mailbox/folder name
  683. * @param int $page Current page to list
  684. * @param string $sort_field Header field to sort by
  685. * @param string $sort_order Sort order [ASC|DESC]
  686. * @param int $slice Number of slice items to extract from result array
  687. * @return array Indexed array with message header objects
  688. * @access public
  689. */
  690. function list_headers($mailbox='', $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
  691. {
  692. if (!strlen($mailbox)) {
  693. $mailbox = $this->mailbox;
  694. }
  695. return $this->_list_headers($mailbox, $page, $sort_field, $sort_order, false, $slice);
  696. }
  697. /**
  698. * Private method for listing message headers
  699. *
  700. * @param string $mailbox Mailbox name
  701. * @param int $page Current page to list
  702. * @param string $sort_field Header field to sort by
  703. * @param string $sort_order Sort order [ASC|DESC]
  704. * @param int $slice Number of slice items to extract from result array
  705. * @return array Indexed array with message header objects
  706. * @access private
  707. * @see rcube_imap::list_headers
  708. */
  709. private function _list_headers($mailbox='', $page=NULL, $sort_field=NULL, $sort_order=NULL, $recursive=false, $slice=0)
  710. {
  711. if (!strlen($mailbox))
  712. return array();
  713. // use saved message set
  714. if ($this->search_string && $mailbox == $this->mailbox)
  715. return $this->_list_header_set($mailbox, $page, $sort_field, $sort_order, $slice);
  716. if ($this->threading)
  717. return $this->_list_thread_headers($mailbox, $page, $sort_field, $sort_order, $recursive, $slice);
  718. $this->_set_sort_order($sort_field, $sort_order);
  719. $page = $page ? $page : $this->list_page;
  720. $cache_key = $mailbox.'.msg';
  721. if ($this->messages_caching) {
  722. // cache is OK, we can get messages from local cache
  723. // (assume cache is in sync when in recursive mode)
  724. if ($recursive || $this->check_cache_status($mailbox, $cache_key)>0) {
  725. $start_msg = ($page-1) * $this->page_size;
  726. $a_msg_headers = $this->get_message_cache($cache_key, $start_msg,
  727. $start_msg+$this->page_size, $this->sort_field, $this->sort_order);
  728. $result = array_values($a_msg_headers);
  729. if ($slice)
  730. $result = array_slice($result, -$slice, $slice);
  731. return $result;
  732. }
  733. // cache is incomplete, sync it (all messages in the folder)
  734. else if (!$recursive) {
  735. $this->sync_header_index($mailbox);
  736. return $this->_list_headers($mailbox, $page, $this->sort_field, $this->sort_order, true, $slice);
  737. }
  738. }
  739. // retrieve headers from IMAP
  740. $a_msg_headers = array();
  741. // use message index sort as default sorting (for better performance)
  742. if (!$this->sort_field) {
  743. if ($this->skip_deleted) {
  744. // @TODO: this could be cached
  745. if ($msg_index = $this->_search_index($mailbox, 'ALL UNDELETED')) {
  746. $max = max($msg_index);
  747. list($begin, $end) = $this->_get_message_range(count($msg_index), $page);
  748. $msg_index = array_slice($msg_index, $begin, $end-$begin);
  749. }
  750. }
  751. else if ($max = $this->conn->countMessages($mailbox)) {
  752. list($begin, $end) = $this->_get_message_range($max, $page);
  753. $msg_index = range($begin+1, $end);
  754. }
  755. else
  756. $msg_index = array();
  757. if ($slice && $msg_index)
  758. $msg_index = array_slice($msg_index, ($this->sort_order == 'DESC' ? 0 : -$slice), $slice);
  759. // fetch reqested headers from server
  760. if ($msg_index)
  761. $this->_fetch_headers($mailbox, join(",", $msg_index), $a_msg_headers, $cache_key);
  762. }
  763. // use SORT command
  764. else if ($this->get_capability('SORT') &&
  765. // Courier-IMAP provides SORT capability but allows to disable it by admin (#1486959)
  766. ($msg_index = $this->conn->sort($mailbox, $this->sort_field, $this->skip_deleted ? 'UNDELETED' : '')) !== false
  767. ) {
  768. if (!empty($msg_index)) {
  769. list($begin, $end) = $this->_get_message_range(count($msg_index), $page);
  770. $max = max($msg_index);
  771. $msg_index = array_slice($msg_index, $begin, $end-$begin);
  772. if ($slice)
  773. $msg_index = array_slice($msg_index, ($this->sort_order == 'DESC' ? 0 : -$slice), $slice);
  774. // fetch reqested headers from server
  775. $this->_fetch_headers($mailbox, join(',', $msg_index), $a_msg_headers, $cache_key);
  776. }
  777. }
  778. // fetch specified header for all messages and sort
  779. else if ($a_index = $this->conn->fetchHeaderIndex($mailbox, "1:*", $this->sort_field, $this->skip_deleted)) {
  780. asort($a_index); // ASC
  781. $msg_index = array_keys($a_index);
  782. $max = max($msg_index);
  783. list($begin, $end) = $this->_get_message_range(count($msg_index), $page);
  784. $msg_index = array_slice($msg_index, $begin, $end-$begin);
  785. if ($slice)
  786. $msg_index = array_slice($msg_index, ($this->sort_order == 'DESC' ? 0 : -$slice), $slice);
  787. // fetch reqested headers from server
  788. $this->_fetch_headers($mailbox, join(",", $msg_index), $a_msg_headers, $cache_key);
  789. }
  790. // delete cached messages with a higher index than $max+1
  791. // Changed $max to $max+1 to fix this bug : #1484295
  792. $this->clear_message_cache($cache_key, $max + 1);
  793. // kick child process to sync cache
  794. // ...
  795. // return empty array if no messages found
  796. if (!is_array($a_msg_headers) || empty($a_msg_headers))
  797. return array();
  798. // use this class for message sorting
  799. $sorter = new rcube_header_sorter();
  800. $sorter->set_sequence_numbers($msg_index);
  801. $sorter->sort_headers($a_msg_headers);
  802. if ($this->sort_order == 'DESC')
  803. $a_msg_headers = array_reverse($a_msg_headers);
  804. return array_values($a_msg_headers);
  805. }
  806. /**
  807. * Private method for listing message headers using threads
  808. *
  809. * @param string $mailbox Mailbox/folder name
  810. * @param int $page Current page to list
  811. * @param string $sort_field Header field to sort by
  812. * @param string $sort_order Sort order [ASC|DESC]
  813. * @param boolean $recursive True if called recursively
  814. * @param int $slice Number of slice items to extract from result array
  815. * @return array Indexed array with message header objects
  816. * @access private
  817. * @see rcube_imap::list_headers
  818. */
  819. private function _list_thread_headers($mailbox, $page=NULL, $sort_field=NULL, $sort_order=NULL, $recursive=false, $slice=0)
  820. {
  821. $this->_set_sort_order($sort_field, $sort_order);
  822. $page = $page ? $page : $this->list_page;
  823. // $cache_key = $mailbox.'.msg';
  824. // $cache_status = $this->check_cache_status($mailbox, $cache_key);
  825. // get all threads (default sort order)
  826. list ($thread_tree, $msg_depth, $has_children) = $this->_fetch_threads($mailbox);
  827. if (empty($thread_tree))
  828. return array();
  829. $msg_index = $this->_sort_threads($mailbox, $thread_tree);
  830. return $this->_fetch_thread_headers($mailbox,
  831. $thread_tree, $msg_depth, $has_children, $msg_index, $page, $slice);
  832. }
  833. /**
  834. * Private method for fetching threads data
  835. *
  836. * @param string $mailbox Mailbox/folder name
  837. * @return array Array with thread data
  838. * @access private
  839. */
  840. private function _fetch_threads($mailbox)
  841. {
  842. if (empty($this->icache['threads'])) {
  843. // get all threads
  844. $result = $this->conn->thread($mailbox, $this->threading,
  845. $this->skip_deleted ? 'UNDELETED' : '');
  846. // add to internal (fast) cache
  847. $this->icache['threads'] = array();
  848. $this->icache['threads']['tree'] = is_array($result) ? $result[0] : array();
  849. $this->icache['threads']['depth'] = is_array($result) ? $result[1] : array();
  850. $this->icache['threads']['has_children'] = is_array($result) ? $result[2] : array();
  851. }
  852. return array(
  853. $this->icache['threads']['tree'],
  854. $this->icache['threads']['depth'],
  855. $this->icache['threads']['has_children'],
  856. );
  857. }
  858. /**
  859. * Private method for fetching threaded messages headers
  860. *
  861. * @param string $mailbox Mailbox name
  862. * @param array $thread_tree Thread tree data
  863. * @param array $msg_depth Thread depth data
  864. * @param array $has_children Thread children data
  865. * @param array $msg_index Messages index
  866. * @param int $page List page number
  867. * @param int $slice Number of threads to slice
  868. * @return array Messages headers
  869. * @access private
  870. */
  871. private function _fetch_thread_headers($mailbox, $thread_tree, $msg_depth, $has_children, $msg_index, $page, $slice=0)
  872. {
  873. $cache_key = $mailbox.'.msg';
  874. // now get IDs for current page
  875. list($begin, $end) = $this->_get_message_range(count($msg_index), $page);
  876. $msg_index = array_slice($msg_index, $begin, $end-$begin);
  877. if ($slice)
  878. $msg_index = array_slice($msg_index, ($this->sort_order == 'DESC' ? 0 : -$slice), $slice);
  879. if ($this->sort_order == 'DESC')
  880. $msg_index = array_reverse($msg_index);
  881. // flatten threads array
  882. // @TODO: fetch children only in expanded mode (?)
  883. $all_ids = array();
  884. foreach ($msg_index as $root) {
  885. $all_ids[] = $root;
  886. if (!empty($thread_tree[$root]))
  887. $all_ids = array_merge($all_ids, array_keys_recursive($thread_tree[$root]));
  888. }
  889. // fetch reqested headers from server
  890. $this->_fetch_headers($mailbox, $all_ids, $a_msg_headers, $cache_key);
  891. // return empty array if no messages found
  892. if (!is_array($a_msg_headers) || empty($a_msg_headers))
  893. return array();
  894. // use this class for message sorting
  895. $sorter = new rcube_header_sorter();
  896. $sorter->set_sequence_numbers($all_ids);
  897. $sorter->sort_headers($a_msg_headers);
  898. // Set depth, has_children and unread_children fields in headers
  899. $this->_set_thread_flags($a_msg_headers, $msg_depth, $has_children);
  900. return array_values($a_msg_headers);
  901. }
  902. /**
  903. * Private method for setting threaded messages flags:
  904. * depth, has_children and unread_children
  905. *
  906. * @param array $headers Reference to headers array indexed by message ID
  907. * @param array $msg_depth Array of messages depth indexed by message ID
  908. * @param array $msg_children Array of messages children flags indexed by message ID
  909. * @return array Message headers array indexed by message ID
  910. * @access private
  911. */
  912. private function _set_thread_flags(&$headers, $msg_depth, $msg_children)
  913. {
  914. $parents = array();
  915. foreach ($headers as $idx => $header) {
  916. $id = $header->id;
  917. $depth = $msg_depth[$id];
  918. $parents = array_slice($parents, 0, $depth);
  919. if (!empty($parents)) {
  920. $headers[$idx]->parent_uid = end($parents);
  921. if (!$header->seen)
  922. $headers[$parents[0]]->unread_children++;
  923. }
  924. array_push($parents, $header->uid);
  925. $headers[$idx]->depth = $depth;
  926. $headers[$idx]->has_children = $msg_children[$id];
  927. }
  928. }
  929. /**
  930. * Private method for listing a set of message headers (search results)
  931. *
  932. * @param string $mailbox Mailbox/folder name
  933. * @param int $page Current page to list
  934. * @param string $sort_field Header field to sort by
  935. * @param string $sort_order Sort order [ASC|DESC]
  936. * @param int $slice Number of slice items to extract from result array
  937. * @return array Indexed array with message header objects
  938. * @access private
  939. * @see rcube_imap::list_header_set()
  940. */
  941. private function _list_header_set($mailbox, $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
  942. {
  943. if (!strlen($mailbox) || empty($this->search_set))
  944. return array();
  945. // use saved messages from searching
  946. if ($this->threading)
  947. return $this->_list_thread_header_set($mailbox, $page, $sort_field, $sort_order, $slice);
  948. // search set is threaded, we need a new one
  949. if ($this->search_threads) {
  950. if (empty($this->search_set['tree']))
  951. return array();
  952. $this->search('', $this->search_string, $this->search_charset, $sort_field);
  953. }
  954. $msgs = $this->search_set;
  955. $a_msg_headers = array();
  956. $page = $page ? $page : $this->list_page;
  957. $start_msg = ($page-1) * $this->page_size;
  958. $this->_set_sort_order($sort_field, $sort_order);
  959. // quickest method (default sorting)
  960. if (!$this->search_sort_field && !$this->sort_field) {
  961. if ($sort_order == 'DESC')
  962. $msgs = array_reverse($msgs);
  963. // get messages uids for one page
  964. $msgs = array_slice(array_values($msgs), $start_msg, min(count($msgs)-$start_msg, $this->page_size));
  965. if ($slice)
  966. $msgs = array_slice($msgs, -$slice, $slice);
  967. // fetch headers
  968. $this->_fetch_headers($mailbox, join(',',$msgs), $a_msg_headers, NULL);
  969. // I didn't found in RFC that FETCH always returns messages sorted by index
  970. $sorter = new rcube_header_sorter();
  971. $sorter->set_sequence_numbers($msgs);
  972. $sorter->sort_headers($a_msg_headers);
  973. return array_values($a_msg_headers);
  974. }
  975. // sorted messages, so we can first slice array and then fetch only wanted headers
  976. if ($this->search_sorted) { // SORT searching result
  977. // reset search set if sorting field has been changed
  978. if ($this->sort_field && $this->search_sort_field != $this->sort_field)
  979. $msgs = $this->search('', $this->search_string, $this->search_charset, $this->sort_field);
  980. // return empty array if no messages found
  981. if (empty($msgs))
  982. return array();
  983. if ($sort_order == 'DESC')
  984. $msgs = array_reverse($msgs);
  985. // get messages uids for one page
  986. $msgs = array_slice(array_values($msgs), $start_msg, min(count($msgs)-$start_msg, $this->page_size));
  987. if ($slice)
  988. $msgs = array_slice($msgs, -$slice, $slice);
  989. // fetch headers
  990. $this->_fetch_headers($mailbox, join(',',$msgs), $a_msg_headers, NULL);
  991. $sorter = new rcube_header_sorter();
  992. $sorter->set_sequence_numbers($msgs);
  993. $sorter->sort_headers($a_msg_headers);
  994. return array_values($a_msg_headers);
  995. }
  996. else { // SEARCH result, need sorting
  997. $cnt = count($msgs);
  998. // 300: experimantal value for best result
  999. if (($cnt > 300 && $cnt > $this->page_size) || !$this->sort_field) {
  1000. // use memory less expensive (and quick) method for big result set
  1001. $a_index = $this->message_index('', $this->sort_field, $this->sort_order);
  1002. // get messages uids for one page...
  1003. $msgs = array_slice($a_index, $start_msg, min($cnt-$start_msg, $this->page_size));
  1004. if ($slice)
  1005. $msgs = array_slice($msgs, -$slice, $slice);
  1006. // ...and fetch headers
  1007. $this->_fetch_headers($mailbox, join(',', $msgs), $a_msg_headers, NULL);
  1008. // return empty array if no messages found
  1009. if (!is_array($a_msg_headers) || empty($a_msg_headers))
  1010. return array();
  1011. $sorter = new rcube_header_sorter();
  1012. $sorter->set_sequence_numbers($msgs);
  1013. $sorter->sort_headers($a_msg_headers);
  1014. return array_values($a_msg_headers);
  1015. }
  1016. else {
  1017. // for small result set we can fetch all messages headers
  1018. $this->_fetch_headers($mailbox, join(',', $msgs), $a_msg_headers, NULL);
  1019. // return empty array if no messages found
  1020. if (!is_array($a_msg_headers) || empty($a_msg_headers))
  1021. return array();
  1022. // if not already sorted
  1023. $a_msg_headers = $this->conn->sortHeaders(
  1024. $a_msg_headers, $this->sort_field, $this->sort_order);
  1025. // only return the requested part of the set
  1026. $a_msg_headers = array_slice(array_values($a_msg_headers),
  1027. $start_msg, min($cnt-$start_msg, $this->page_size));
  1028. if ($slice)
  1029. $a_msg_headers = array_slice($a_msg_headers, -$slice, $slice);
  1030. return $a_msg_headers;
  1031. }
  1032. }
  1033. }
  1034. /**
  1035. * Private method for listing a set of threaded message headers (search results)
  1036. *
  1037. * @param string $mailbox Mailbox/folder name
  1038. * @param int $page Current page to list
  1039. * @param string $sort_field Header field to sort by
  1040. * @param string $sort_order Sort order [ASC|DESC]
  1041. * @param int $slice Number of slice items to extract from result array
  1042. * @return array Indexed array with message header objects
  1043. * @access private
  1044. * @see rcube_imap::list_header_set()
  1045. */
  1046. private function _list_thread_header_set($mailbox, $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
  1047. {
  1048. // update search_set if previous data was fetched with disabled threading
  1049. if (!$this->search_threads) {
  1050. if (empty($this->search_set))
  1051. return array();
  1052. $this->search('', $this->search_string, $this->search_charset, $sort_field);
  1053. }
  1054. // empty result
  1055. if (empty($this->search_set['tree']))
  1056. return array();
  1057. $thread_tree = $this->search_set['tree'];
  1058. $msg_depth = $this->search_set['depth'];
  1059. $has_children = $this->search_set['children'];
  1060. $a_msg_headers = array();
  1061. $page = $page ? $page : $this->list_page;
  1062. $start_msg = ($page-1) * $this->page_size;
  1063. $this->_set_sort_order($sort_field, $sort_order);
  1064. $msg_index = $this->_sort_threads($mailbox, $thread_tree, array_keys($msg_depth));
  1065. return $this->_fetch_thread_headers($mailbox,
  1066. $thread_tree, $msg_depth, $has_children, $msg_index, $page, $slice=0);
  1067. }
  1068. /**
  1069. * Helper function to get first and last index of the requested set
  1070. *
  1071. * @param int $max Messages count
  1072. * @param mixed $page Page number to show, or string 'all'
  1073. * @return array Array with two values: first index, last index
  1074. * @access private
  1075. */
  1076. private function _get_message_range($max, $page)
  1077. {
  1078. $start_msg = ($page-1) * $this->page_size;
  1079. if ($page=='all') {
  1080. $begin = 0;
  1081. $end = $max;
  1082. }
  1083. else if ($this->sort_order=='DESC') {
  1084. $begin = $max - $this->page_size - $start_msg;
  1085. $end = $max - $start_msg;
  1086. }
  1087. else {
  1088. $begin = $start_msg;
  1089. $end = $start_msg + $this->page_size;
  1090. }
  1091. if ($begin < 0) $begin = 0;
  1092. if ($end < 0) $end = $max;
  1093. if ($end > $max) $end = $max;
  1094. return array($begin, $end);
  1095. }
  1096. /**
  1097. * Fetches message headers (used for loop)
  1098. *
  1099. * @param string $mailbox Mailbox name
  1100. * @param string $msgs Message index to fetch
  1101. * @param array $a_msg_headers Reference to message headers array
  1102. * @param string $cache_key Cache index key
  1103. * @return int Messages count
  1104. * @access private
  1105. */
  1106. private function _fetch_headers($mailbox, $msgs, &$a_msg_headers, $cache_key)
  1107. {
  1108. // fetch reqested headers from server
  1109. $a_header_index = $this->conn->fetchHeaders(
  1110. $mailbox, $msgs, false, false, $this->get_fetch_headers());
  1111. if (empty($a_header_index))
  1112. return 0;
  1113. foreach ($a_header_index as $i => $headers) {
  1114. $a_msg_headers[$headers->uid] = $headers;
  1115. }
  1116. // Update cache
  1117. if ($this->messages_caching && $cache_key) {
  1118. // cache is incomplete?
  1119. $cache_index = $this->get_message_cache_index($cache_key);
  1120. foreach ($a_header_index as $headers) {
  1121. // message in cache
  1122. if ($cache_index[$headers->id] == $headers->uid) {
  1123. unset($cache_index[$headers->id]);
  1124. continue;
  1125. }
  1126. // wrong UID at this position
  1127. if ($cache_index[$headers->id]) {
  1128. $for_remove[] = $cache_index[$headers->id];
  1129. unset($cache_index[$headers->id]);
  1130. }
  1131. // message UID in cache but at wrong position
  1132. if (is_int($key = array_search($headers->uid, $cache_index))) {
  1133. $for_remove[] = $cache_index[$key];
  1134. unset($cache_index[$key]);
  1135. }
  1136. $for_create[] = $headers->uid;
  1137. }
  1138. if ($for_remove)
  1139. $this->remove_message_cache($cache_key, $for_remove);
  1140. // add messages to cache
  1141. foreach ((array)$for_create as $uid) {
  1142. $headers = $a_msg_headers[$uid];
  1143. $this->add_message_cache($cache_key, $headers->id, $headers, NULL, true);
  1144. }
  1145. }
  1146. return count($a_msg_headers);
  1147. }
  1148. /**
  1149. * Returns current status of mailbox
  1150. *
  1151. * We compare the maximum UID to determine the number of
  1152. * new messages because the RECENT flag is not reliable.
  1153. *
  1154. * @param string $mailbox Mailbox/folder name
  1155. * @return int Folder status
  1156. */
  1157. function mailbox_status($mailbox = null)
  1158. {
  1159. if (!strlen($mailbox)) {
  1160. $mailbox = $this->mailbox;
  1161. }
  1162. $old = $this->get_folder_stats($mailbox);
  1163. // refresh message count -> will update
  1164. $this->_messagecount($mailbox, 'ALL', true);
  1165. $result = 0;
  1166. $new = $this->get_folder_stats($mailbox);
  1167. // got new messages
  1168. if ($new['maxuid'] > $old['maxuid'])
  1169. $result += 1;
  1170. // some messages has been deleted
  1171. if ($new['cnt'] < $old['cnt'])
  1172. $result += 2;
  1173. // @TODO: optional checking for messages flags changes (?)
  1174. // @TODO: UIDVALIDITY checking
  1175. return $result;
  1176. }
  1177. /**
  1178. * Stores folder statistic data in session
  1179. * @TODO: move to separate DB table (cache?)
  1180. *
  1181. * @param string $mailbox Mailbox name
  1182. * @param string $name Data name
  1183. * @param mixed $data Data value
  1184. */
  1185. private function set_folder_stats($mailbox, $name, $data)
  1186. {
  1187. $_SESSION['folders'][$mailbox][$name] = $data;
  1188. }
  1189. /**
  1190. * Gets folder statistic data
  1191. *
  1192. * @param string $mailbox Mailbox name
  1193. *
  1194. * @return array Stats data
  1195. */
  1196. private function get_folder_stats($mailbox)
  1197. {
  1198. if ($_SESSION['folders'][$mailbox])
  1199. return (array) $_SESSION['folders'][$mailbox];
  1200. else
  1201. return array();
  1202. }
  1203. /**
  1204. * Return sorted array of message IDs (not UIDs)
  1205. *
  1206. * @param string $mailbox Mailbox to get index from
  1207. * @param string $sort_field Sort column
  1208. * @param string $sort_order Sort order [ASC, DESC]
  1209. * @return array Indexed array with message IDs
  1210. */
  1211. function message_index($mailbox='', $sort_field=NULL, $sort_order=NULL)
  1212. {
  1213. if ($this->threading)
  1214. return $this->thread_index($mailbox, $sort_field, $sort_order);
  1215. $this->_set_sort_order($sort_field, $sort_order);
  1216. if (!strlen($mailbox)) {
  1217. $mailbox = $this->mailbox;
  1218. }
  1219. $key = "{$mailbox}:{$this->sort_field}:{$this->sort_order}:{$this->search_string}.msgi";
  1220. // we have a saved search result, get index from there
  1221. if (!isset($this->icache[$key]) && $this->search_string
  1222. && !$this->search_threads && $mailbox == $this->mailbox) {
  1223. // use message index sort as default sorting
  1224. if (!$this->sort_field) {
  1225. $msgs = $this->search_set;
  1226. if ($this->search_sort_field != 'date')
  1227. sort($msgs);
  1228. if ($this->sort_order == 'DESC')
  1229. $this->icache[$key] = array_reverse($msgs);
  1230. else
  1231. $this->icache[$key] = $msgs;
  1232. }
  1233. // sort with SORT command
  1234. else if ($this->search_sorted) {
  1235. if ($this->sort_field && $this->search_sort_field != $this->sort_field)
  1236. $this->search('', $this->search_string, $this->search_charset, $this->sort_field);
  1237. if ($this->sort_order == 'DESC')
  1238. $this->icache[$key] = array_reverse($this->search_set);
  1239. else
  1240. $this->icache[$key] = $this->search_set;
  1241. }
  1242. else {
  1243. $a_index = $this->conn->fetchHeaderIndex($mailbox,
  1244. join(',', $this->search_set), $this->sort_field, $this->skip_deleted);
  1245. if (is_array($a_index)) {
  1246. if ($this->sort_order=="ASC")
  1247. asort($a_index);
  1248. else if ($this->sort_order=="DESC")
  1249. arsort($a_index);
  1250. $this->icache[$key] = array_keys($a_index);
  1251. }
  1252. else {
  1253. $this->icache[$key] = array();
  1254. }
  1255. }
  1256. }
  1257. // have stored it in RAM
  1258. if (isset($this->icache[$key]))
  1259. return $this->icache[$key];
  1260. // check local cache
  1261. $cache_key = $mailbox.'.msg';
  1262. $cache_status = $this->check_cache_status($mailbox, $cache_key);
  1263. // cache is OK
  1264. if ($cache_status>0) {
  1265. $a_index = $this->get_message_cache_index($cache_key,
  1266. $this->sort_field, $this->sort_order);
  1267. return array_keys($a_index);
  1268. }
  1269. // use message index sort as default sorting
  1270. if (!$this->sort_field) {
  1271. if ($this->skip_deleted) {
  1272. $a_index = $this->_search_index($mailbox, 'ALL');
  1273. } else if ($max = $this->_messagecount($mailbox)) {
  1274. $a_index = range(1, $max);
  1275. }
  1276. if ($a_index !== false && $this->sort_order == 'DESC')
  1277. $a_index = array_reverse($a_index);
  1278. $this->icache[$key] = $a_index;
  1279. }
  1280. // fetch complete message index
  1281. else if ($this->get_capability('SORT') &&
  1282. ($a_index = $this->conn->sort($mailbox,
  1283. $this->sort_field, $this->skip_deleted ? 'UNDELETED' : '')) !== false
  1284. ) {
  1285. if ($this->sort_order == 'DESC')
  1286. $a_index = array_reverse($a_index);
  1287. $this->icache[$key] = $a_index;
  1288. }
  1289. else if ($a_index = $this->conn->fetchHeaderIndex(
  1290. $mailbox, "1:*", $this->sort_field, $this->skip_deleted)) {
  1291. if ($this->sort_order=="ASC")
  1292. asort($a_index);
  1293. else if ($this->sort_order=="DESC")
  1294. arsort($a_index);
  1295. $this->icache[$key] = array_keys($a_index);
  1296. }
  1297. return $this->icache[$key] !== false ? $this->icache[$key] : array();
  1298. }
  1299. /**
  1300. * Return sorted array of threaded message IDs (not UIDs)
  1301. *
  1302. * @param string $mailbox Mailbox to get index from
  1303. * @param string $sort_field Sort column
  1304. * @param string $sort_order Sort order [ASC, DESC]
  1305. * @return array Indexed array with message IDs
  1306. */
  1307. function thread_index($mailbox='', $sort_field=NULL, $sort_order=NULL)
  1308. {
  1309. $this->_set_sort_order($sort_field, $sort_order);
  1310. if (!strlen($mailbox)) {
  1311. $mailbox = $this->mailbox;
  1312. }
  1313. $key = "{$mailbox}:{$this->sort_field}:{$this->sort_order}:{$this->search_string}.thi";
  1314. // we have a saved search result, get index from there
  1315. if (!isset($this->icache[$key]) && $this->search_string
  1316. && $this->search_threads && $mailbox == $this->mailbox) {
  1317. // use message IDs for better performance
  1318. $ids = array_keys_recursive($this->search_set['tree']);
  1319. $this->icache[$key] = $this->_flatten_threads($mailbox, $this->search_set['tree'], $ids);
  1320. }
  1321. // have stored it in RAM
  1322. if (isset($this->icache[$key]))
  1323. return $this->icache[$key];
  1324. /*
  1325. // check local cache
  1326. $cache_key = $mailbox.'.msg';
  1327. $cache_status = $this->check_cache_status($mailbox, $cache_key);
  1328. // cache is OK
  1329. if ($cache_status>0) {
  1330. $a_index = $this->get_message_cache_index($cache_key, $this->sort_field, $this->sort_order);
  1331. return array_keys($a_index);
  1332. }
  1333. */
  1334. // get all threads (default sort order)
  1335. list ($thread_tree) = $this->_fetch_threads($mailbox);
  1336. $this->icache[$key] = $this->_flatten_threads($mailbox, $thread_tree);
  1337. return $this->icache[$key];
  1338. }
  1339. /**
  1340. * Return array of threaded messages (all, not only roots)
  1341. *
  1342. * @param string $mailbox Mailbox to get index from
  1343. * @param array $thread_tree Threaded messages array (see _fetch_threads())
  1344. * @param array $ids Message IDs if we know what we need (e.g. search result)
  1345. * for better performance
  1346. * @return array Indexed array with message IDs
  1347. *
  1348. * @access private
  1349. */
  1350. private function _flatten_threads($mailbox, $thread_tree, $ids=null)
  1351. {
  1352. if (empty($thread_tree))
  1353. return array();
  1354. $msg_index = $this->_sort_threads($mailbox, $thread_tree, $ids);
  1355. if ($this->sort_order == 'DESC')
  1356. $msg_index = array_reverse($msg_index);
  1357. // flatten threads array
  1358. $all_ids = array();
  1359. foreach ($msg_index as $root) {
  1360. $all_ids[] = $root;
  1361. if (!empty($thread_tree[$root])) {
  1362. foreach (array_keys_recursive($thread_tree[$root]) as $val)
  1363. $all_ids[] = $val;
  1364. }
  1365. }
  1366. return $all_ids;
  1367. }
  1368. /**
  1369. * @param string $mailbox Mailbox name
  1370. * @access private
  1371. */
  1372. private function sync_header_index($mailbox)
  1373. {
  1374. $cache_key = $mailbox.'.msg';
  1375. $cache_index = $this->get_message_cache_index($cache_key);
  1376. $chunk_size = 1000;
  1377. // cache is empty, get all messages
  1378. if (is_array($cache_index) && empty($cache_index)) {
  1379. $max = $this->_messagecount($mailbox);
  1380. // syncing a big folder maybe slow
  1381. @set_time_limit(0);
  1382. $start = 1;
  1383. $end = min($chunk_size, $max);
  1384. while (true) {
  1385. // do this in loop to save memory (1000 msgs ~= 10 MB)
  1386. if ($headers = $this->conn->fetchHeaders($mailbox,
  1387. "$start:$end", false, false, $this->get_fetch_headers())
  1388. ) {
  1389. foreach ($headers as $header) {
  1390. $this->add_message_cache($cache_key, $header->id, $header, NULL, true);
  1391. }
  1392. }
  1393. if ($end - $start < $chunk_size - 1)
  1394. break;
  1395. $end = min($end+$chunk_size, $max);
  1396. $start += $chunk_size;
  1397. }
  1398. return;
  1399. }
  1400. // fetch complete message index
  1401. if (isset($this->icache['folder_index']))
  1402. $a_message_index = &$this->icache['folder_index'];
  1403. else
  1404. $a_message_index = $this->conn->fetchHeaderIndex($mailbox, "1:*", 'UID', $this->skip_deleted);
  1405. if ($a_message_index === false || $cache_index === null)
  1406. return;
  1407. // compare cache index with real index
  1408. foreach ($a_message_index as $id => $uid) {
  1409. // message in cache at correct position
  1410. if ($cache_index[$id] == $uid) {
  1411. unset($cache_index[$id]);
  1412. continue;
  1413. }
  1414. // other message at this position
  1415. if (isset($cache_index[$id])) {
  1416. $for_remove[] = $cache_index[$id];
  1417. unset($cache_index[$id]);
  1418. }
  1419. // message in cache but at wrong position
  1420. if (is_int($key = array_search($uid, $cache_index))) {
  1421. $for_remove[] = $uid;
  1422. unset($cache_index[$key]);
  1423. }
  1424. $for_update[] = $id;
  1425. }
  1426. // remove messages at wrong positions and those deleted that are still in cache_index
  1427. if (!empty($for_remove))
  1428. $cache_index = array_merge($cache_index, $for_remove);
  1429. if (!empty($cache_index))
  1430. $this->remove_message_cache($cache_key, $cache_index);
  1431. // fetch complete headers and add to cache
  1432. if (!empty($for_update)) {
  1433. // syncing a big folder maybe slow
  1434. @set_time_limit(0);
  1435. // To save memory do this in chunks
  1436. $for_update = array_chunk($for_update, $chunk_size);
  1437. foreach ($for_update as $uids) {
  1438. if ($headers = $this->conn->fetchHeaders($mailbox,
  1439. $uids, false, false, $this->get_fetch_headers())
  1440. ) {
  1441. foreach ($headers as $header) {
  1442. $this->add_message_cache($cache_key, $header->id, $header, NULL, true);
  1443. }
  1444. }
  1445. }
  1446. }
  1447. }
  1448. /**
  1449. * Invoke search request to IMAP server
  1450. *
  1451. * @param string $mailbox Mailbox name to search in
  1452. * @param string $str Search criteria
  1453. * @param string $charset Search charset
  1454. * @param string $sort_field Header field to sort by
  1455. * @return array search results as list of message IDs
  1456. * @access public
  1457. */
  1458. function search($mailbox='', $str=NULL, $charset=NULL, $sort_field=NULL)
  1459. {
  1460. if (!$str)
  1461. return false;
  1462. if (!strlen($mailbox)) {
  1463. $mailbox = $this->mailbox;
  1464. }
  1465. $results = $this->_search_index($mailbox, $str, $charset, $sort_field);
  1466. $this->set_search_set($str, $results, $charset, $sort_field, (bool)$this->threading,
  1467. $this->threading || $this->search_sorted ? true : false);
  1468. return $results;
  1469. }
  1470. /**
  1471. * Private search method
  1472. *
  1473. * @param string $mailbox Mailbox name
  1474. * @param string $criteria Search criteria
  1475. * @param string $charset Charset
  1476. * @param string $sort_field Sorting field
  1477. * @return array search results as list of message ids
  1478. * @access private
  1479. * @see rcube_imap::search()
  1480. */
  1481. private function _search_index($mailbox, $criteria='ALL', $charset=NULL, $sort_field=NULL)
  1482. {
  1483. $orig_criteria = $criteria;
  1484. if ($this->skip_deleted && !preg_match('/UNDELETED/', $criteria))
  1485. $criteria = 'UNDELETED '.$criteria;
  1486. if ($this->threading) {
  1487. $a_messages = $this->conn->thread($mailbox, $this->threading, $criteria, $charset);
  1488. // Error, try with US-ASCII (RFC5256: SORT/THREAD must support US-ASCII and UTF-8,
  1489. // but I've seen that Courier doesn't support UTF-8)
  1490. if ($a_messages === false && $charset && $charset != 'US-ASCII')
  1491. $a_messages = $this->conn->thread($mailbox, $this->threading,
  1492. $this->convert_criteria($criteria, $charset), 'US-ASCII');
  1493. if ($a_messages !== false) {
  1494. list ($thread_tree, $msg_depth, $has_children) = $a_messages;
  1495. $a_messages = array(
  1496. 'tree' => $thread_tree,
  1497. 'depth' => $msg_depth,
  1498. 'children' => $has_children
  1499. );
  1500. }
  1501. return $a_messages;
  1502. }
  1503. if ($sort_field && $this->get_capability('SORT')) {
  1504. $charset = $charset ? $charset : $this->default_charset;
  1505. $a_messages = $this->conn->sort($mailbox, $sort_field, $criteria, false, $charset);
  1506. // Error, try with US-ASCII (RFC5256: SORT/THREAD must support US-ASCII and UTF-8,
  1507. // but I've seen that Courier doesn't support UTF-8)
  1508. if ($a_messages === false && $charset && $charset != 'US-ASCII')
  1509. $a_messages = $this->conn->sort($mailbox, $sort_field,
  1510. $this->convert_criteria($criteria, $charset), false, 'US-ASCII');
  1511. if ($a_messages !== false) {
  1512. $this->search_sorted = true;
  1513. return $a_messages;
  1514. }
  1515. }
  1516. if ($orig_criteria == 'ALL') {
  1517. $max = $this->_messagecount($mailbox);
  1518. $a_messages = $max ? range(1, $max) : array();
  1519. }
  1520. else {
  1521. $a_messages = $this->conn->search($mailbox,
  1522. ($charset ? "CHARSET $charset " : '') . $criteria);
  1523. // Error, try with US-ASCII (some servers may support only US-ASCII)
  1524. if ($a_messages === false && $charset && $charset != 'US-ASCII')
  1525. $a_messages = $this->conn->search($mailbox,
  1526. 'CHARSET US-ASCII ' . $this->convert_criteria($criteria, $charset));
  1527. // I didn't found that SEARCH should return sorted IDs
  1528. if (is_array($a_messages) && !$this->sort_field)
  1529. sort($a_messages);
  1530. }
  1531. $this->search_sorted = false;
  1532. return $a_messages;
  1533. }
  1534. /**
  1535. * Direct (real and simple) SEARCH request to IMAP server,
  1536. * without result sorting and caching
  1537. *
  1538. * @param string $mailbox Mailbox name to search in
  1539. * @param string $str Search string
  1540. * @param boolean $ret_uid True if UIDs should be returned
  1541. * @return array Search results as list of message IDs or UIDs
  1542. * @access public
  1543. */
  1544. function search_once($mailbox='', $str=NULL, $ret_uid=false)
  1545. {
  1546. if (!$str)
  1547. return false;
  1548. if (!strlen($mailbox)) {
  1549. $mailbox = $this->mailbox;
  1550. }
  1551. return $this->conn->search($mailbox, $str, $ret_uid);
  1552. }
  1553. /**
  1554. * Converts charset of search criteria string
  1555. *
  1556. * @param string $str Search string
  1557. * @param string $charset Original charset
  1558. * @param string $dest_charset Destination charset (default US-ASCII)
  1559. * @return string Search string
  1560. * @access private
  1561. */
  1562. private function convert_criteria($str, $charset, $dest_charset='US-ASCII')
  1563. {
  1564. // convert strings to US_ASCII
  1565. if (preg_match_all('/\{([0-9]+)\}\r\n/', $str, $matches, PREG_OFFSET_CAPTURE)) {
  1566. $last = 0; $res = '';
  1567. foreach ($matches[1] as $m) {
  1568. $string_offset = $m[1] + strlen($m[0]) + 4; // {}\r\n
  1569. $string = substr($str, $string_offset - 1, $m[0]);
  1570. $string = rcube_charset_convert($string, $charset, $dest_charset);
  1571. if (!$string)
  1572. continue;
  1573. $res .= sprintf("%s{%d}\r\n%s", substr($str, $last, $m[1] - $last - 1), strlen($string), $string);
  1574. $last = $m[0] + $string_offset - 1;
  1575. }
  1576. if ($last < strlen($str))
  1577. $res .= substr($str, $last, strlen($str)-$last);
  1578. }
  1579. else // strings for conversion not found
  1580. $res = $str;
  1581. return $res;
  1582. }
  1583. /**
  1584. * Sort thread
  1585. *
  1586. * @param string $mailbox Mailbox name
  1587. * @param array $thread_tree Unsorted thread tree (rcube_imap_generic::thread() result)
  1588. * @param array $ids Message IDs if we know what we need (e.g. search result)
  1589. * @return array Sorted roots IDs
  1590. * @access private
  1591. */
  1592. private function _sort_threads($mailbox, $thread_tree, $ids=NULL)
  1593. {
  1594. // THREAD=ORDEREDSUBJECT: sorting by sent date of root message
  1595. // THREAD=REFERENCES: sorting by sent date of root message
  1596. // THREAD=REFS: sorting by the most recent date in each thread
  1597. // default sorting
  1598. if (!$this->sort_field || ($this->sort_field == 'date' && $this->threading == 'REFS')) {
  1599. return array_keys((array)$thread_tree);
  1600. }
  1601. // here we'll implement REFS sorting, for performance reason
  1602. else { // ($sort_field == 'date' && $this->threading != 'REFS')
  1603. // use SORT command
  1604. if ($this->get_capability('SORT') &&
  1605. ($a_index = $this->conn->sort($mailbox, $this->sort_field,
  1606. !empty($ids) ? $ids : ($this->skip_deleted ? 'UNDELETED' : ''))) !== false
  1607. ) {
  1608. // return unsorted tree if we've got no index data
  1609. if (!$a_index)
  1610. return array_keys((array)$thread_tree);
  1611. }
  1612. else {
  1613. // fetch specified headers for all messages and sort them
  1614. $a_index = $this->conn->fetchHeaderIndex($mailbox, !empty($ids) ? $ids : "1:*",
  1615. $this->sort_field, $this->skip_deleted);
  1616. // return unsorted tree if we've got no index data
  1617. if (!$a_index)
  1618. return array_keys((array)$thread_tree);
  1619. asort($a_index); // ASC
  1620. $a_index = array_values($a_index);
  1621. }
  1622. return $this->_sort_thread_refs($thread_tree, $a_index);
  1623. }
  1624. }
  1625. /**
  1626. * THREAD=REFS sorting implementation
  1627. *
  1628. * @param array $tree Thread tree array (message identifiers as keys)
  1629. * @param array $index Array of sorted message identifiers
  1630. * @return array Array of sorted roots messages
  1631. * @access private
  1632. */
  1633. private function _sort_thread_refs($tree, $index)
  1634. {
  1635. if (empty($tree))
  1636. return array();
  1637. $index = array_combine(array_values($index), $index);
  1638. // assign roots
  1639. foreach ($tree as $idx => $val) {
  1640. $index[$idx] = $idx;
  1641. if (!empty($val)) {
  1642. $idx_arr = array_keys_recursive($tree[$idx]);
  1643. foreach ($idx_arr as $subidx)
  1644. $index[$subidx] = $idx;
  1645. }
  1646. }
  1647. $index = array_values($index);
  1648. // create sorted array of roots
  1649. $msg_index = array();
  1650. if ($this->sort_order != 'DESC') {
  1651. foreach ($index as $idx)
  1652. if (!isset($msg_index[$idx]))
  1653. $msg_index[$idx] = $idx;
  1654. $msg_index = array_values($msg_index);
  1655. }
  1656. else {
  1657. for ($x=count($index)-1; $x>=0; $x--)
  1658. if (!isset($msg_index[$index[$x]]))
  1659. $msg_index[$index[$x]] = $index[$x];
  1660. $msg_index = array_reverse($msg_index);
  1661. }
  1662. return $msg_index;
  1663. }
  1664. /**
  1665. * Refresh saved search set
  1666. *
  1667. * @return array Current search set
  1668. */
  1669. function refresh_search()
  1670. {
  1671. if (!empty($this->search_string))
  1672. $this->search_set = $this->search('', $this->search_string, $this->search_charset,
  1673. $this->search_sort_field, $this->search_threads, $this->search_sorted);
  1674. return $this->get_search_set();
  1675. }
  1676. /**
  1677. * Check if the given message ID is part of the current search set
  1678. *
  1679. * @param string $msgid Message id
  1680. * @return boolean True on match or if no search request is stored
  1681. */
  1682. function in_searchset($msgid)
  1683. {
  1684. if (!empty($this->search_string)) {
  1685. if ($this->search_threads)
  1686. return isset($this->search_set['depth']["$msgid"]);
  1687. else
  1688. return in_array("$msgid", (array)$this->search_set, true);
  1689. }
  1690. else
  1691. return true;
  1692. }
  1693. /**
  1694. * Return message headers object of a specific message
  1695. *
  1696. * @param int $id Message ID
  1697. * @param string $mailbox Mailbox to read from
  1698. * @param boolean $is_uid True if $id is the message UID
  1699. * @param boolean $bodystr True if we need also BODYSTRUCTURE in headers
  1700. * @return object Message headers representation
  1701. */
  1702. function get_headers($id, $mailbox=null, $is_uid=true, $bodystr=false)
  1703. {
  1704. if (!strlen($mailbox)) {
  1705. $mailbox = $this->mailbox;
  1706. }
  1707. $uid = $is_uid ? $id : $this->_id2uid($id, $mailbox);
  1708. // get cached headers
  1709. if ($uid && ($headers = &$this->get_cached_message($mailbox.'.msg', $uid)))
  1710. return $headers;
  1711. $headers = $this->conn->fetchHeader(
  1712. $mailbox, $id, $is_uid, $bodystr, $this->get_fetch_headers());
  1713. // write headers cache
  1714. if ($headers) {
  1715. if ($headers->uid && $headers->id)
  1716. $this->uid_id_map[$mailbox][$headers->uid] = $headers->id;
  1717. $this->add_message_cache($mailbox.'.msg', $headers->id, $headers, NULL, false, true);
  1718. }
  1719. return $headers;
  1720. }
  1721. /**
  1722. * Fetch body structure from the IMAP server and build
  1723. * an object structure similar to the one generated by PEAR::Mail_mimeDecode
  1724. *
  1725. * @param int $uid Message UID to fetch
  1726. * @param string $structure_str Message BODYSTRUCTURE string (optional)
  1727. * @return object rcube_message_part Message part tree or False on failure
  1728. */
  1729. function &get_structure($uid, $structure_str='')
  1730. {
  1731. $cache_key = $this->mailbox.'.msg';
  1732. $headers = &$this->get_cached_message($cache_key, $uid);
  1733. // return cached message structure
  1734. if (is_object($headers) && is_object($headers->structure)) {
  1735. return $headers->structure;
  1736. }
  1737. if (!$structure_str) {
  1738. $structure_str = $this->conn->fetchStructureString($this->mailbox, $uid, true);
  1739. }
  1740. $structure = rcube_mime_struct::parseStructure($structure_str);
  1741. $struct = false;
  1742. // parse structure and add headers
  1743. if (!empty($structure)) {
  1744. $headers = $this->get_headers($uid);
  1745. $this->_msg_id = $headers->id;
  1746. // set message charset from message headers
  1747. if ($headers->charset)
  1748. $this->struct_charset = $headers->charset;
  1749. else
  1750. $this->struct_charset = $this->_structure_charset($structure);
  1751. $headers->ctype = strtolower($headers->ctype);
  1752. // Here we can recognize malformed BODYSTRUCTURE and
  1753. // 1. [@TODO] parse the message in other way to create our own message structure
  1754. // 2. or just show the raw message body.
  1755. // Example of structure for malformed MIME message:
  1756. // ("text" "plain" NIL NIL NIL "7bit" 2154 70 NIL NIL NIL)
  1757. if ($headers->ctype && !is_array($structure[0]) && $headers->ctype != 'text/plain'
  1758. && strtolower($structure[0].'/'.$structure[1]) == 'text/plain') {
  1759. // we can handle single-part messages, by simple fix in structure (#1486898)
  1760. if (preg_match('/^(text|application)\/(.*)/', $headers->ctype, $m)) {
  1761. $structure[0] = $m[1];
  1762. $structure[1] = $m[2];
  1763. }
  1764. else
  1765. return false;
  1766. }
  1767. $struct = &$this->_structure_part($structure, 0, '', $headers);
  1768. $struct->headers = get_object_vars($headers);
  1769. // don't trust given content-type
  1770. if (empty($struct->parts) && !empty($struct->headers['ctype'])) {
  1771. $struct->mime_id = '1';
  1772. $struct->mimetype = strtolower($struct->headers['ctype']);
  1773. list($struct->ctype_primary, $struct->ctype_secondary) = explode('/', $struct->mimetype);
  1774. }
  1775. // write structure to cache
  1776. if ($this->messages_caching)
  1777. $this->add_message_cache($cache_key, $this->_msg_id, $headers, $struct,
  1778. $this->icache['message.id'][$uid], true);
  1779. }
  1780. return $struct;
  1781. }
  1782. /**
  1783. * Build message part object
  1784. *
  1785. * @param array $part
  1786. * @param int $count
  1787. * @param string $parent
  1788. * @access private
  1789. */
  1790. function &_structure_part($part, $count=0, $parent='', $mime_headers=null)
  1791. {
  1792. $struct = new rcube_message_part;
  1793. $struct->mime_id = empty($parent) ? (string)$count : "$parent.$count";
  1794. // multipart
  1795. if (is_array($part[0])) {
  1796. $struct->ctype_primary = 'multipart';
  1797. /* RFC3501: BODYSTRUCTURE fields of multipart part
  1798. part1 array
  1799. part2 array
  1800. part3 array
  1801. ....
  1802. 1. subtype
  1803. 2. parameters (optional)
  1804. 3. description (optional)
  1805. 4. language (optional)
  1806. 5. location (optional)
  1807. */
  1808. // find first non-array entry
  1809. for ($i=1; $i<count($part); $i++) {
  1810. if (!is_array($part[$i])) {
  1811. $struct->ctype_secondary = strtolower($part[$i]);
  1812. break;
  1813. }
  1814. }
  1815. $struct->mimetype = 'multipart/'.$struct->ctype_secondary;
  1816. // build parts list for headers pre-fetching
  1817. for ($i=0; $i<count($part); $i++) {
  1818. if (!is_array($part[$i]))
  1819. break;
  1820. // fetch message headers if message/rfc822
  1821. // or named part (could contain Content-Location header)
  1822. if (!is_array($part[$i][0])) {
  1823. $tmp_part_id = $struct->mime_id ? $struct->mime_id.'.'.($i+1) : $i+1;
  1824. if (strtolower($part[$i][0]) == 'message' && strtolower($part[$i][1]) == 'rfc822') {
  1825. $mime_part_headers[] = $tmp_part_id;
  1826. }
  1827. else if (in_array('name', (array)$part[$i][2]) && empty($part[$i][3])) {
  1828. $mime_part_headers[] = $tmp_part_id;
  1829. }
  1830. }
  1831. }
  1832. // pre-fetch headers of all parts (in one command for better performance)
  1833. // @TODO: we could do this before _structure_part() call, to fetch
  1834. // headers for parts on all levels
  1835. if ($mime_part_headers) {
  1836. $mime_part_headers = $this->conn->fetchMIMEHeaders($this->mailbox,
  1837. $this->_msg_id, $mime_part_headers);
  1838. }
  1839. $struct->parts = array();
  1840. for ($i=0, $count=0; $i<count($part); $i++) {
  1841. if (!is_array($part[$i]))
  1842. break;
  1843. $tmp_part_id = $struct->mime_id ? $struct->mime_id.'.'.($i+1) : $i+1;
  1844. $struct->parts[] = $this->_structure_part($part[$i], ++$count, $struct->mime_id,
  1845. $mime_part_headers[$tmp_part_id]);
  1846. }
  1847. return $struct;
  1848. }
  1849. /* RFC3501: BODYSTRUCTURE fields of non-multipart part
  1850. 0. type
  1851. 1. subtype
  1852. 2. parameters
  1853. 3. id
  1854. 4. description
  1855. 5. encoding
  1856. 6. size
  1857. -- text
  1858. 7. lines
  1859. -- message/rfc822
  1860. 7. envelope structure
  1861. 8. body structure
  1862. 9. lines
  1863. --
  1864. x. md5 (optional)
  1865. x. disposition (optional)
  1866. x. language (optional)
  1867. x. location (optional)
  1868. */
  1869. // regular part
  1870. $struct->ctype_primary = strtolower($part[0]);
  1871. $struct->ctype_secondary = strtolower($part[1]);
  1872. $struct->mimetype = $struct->ctype_primary.'/'.$struct->ctype_secondary;
  1873. // read content type parameters
  1874. if (is_array($part[2])) {
  1875. $struct->ctype_parameters = array();
  1876. for ($i=0; $i<count($part[2]); $i+=2)
  1877. $struct->ctype_parameters[strtolower($part[2][$i])] = $part[2][$i+1];
  1878. if (isset($struct->ctype_parameters['charset']))
  1879. $struct->charset = $struct->ctype_parameters['charset'];
  1880. }
  1881. // #1487700: workaround for lack of charset in malformed structure
  1882. if (empty($struct->charset) && !empty($mime_headers) && $mime_headers->charset) {
  1883. $struct->charset = $mime_headers->charset;
  1884. }
  1885. // read content encoding
  1886. if (!empty($part[5])) {
  1887. $struct->encoding = strtolower($part[5]);
  1888. $struct->headers['content-transfer-encoding'] = $struct->encoding;
  1889. }
  1890. // get part size
  1891. if (!empty($part[6]))
  1892. $struct->size = intval($part[6]);
  1893. // read part disposition
  1894. $di = 8;
  1895. if ($struct->ctype_primary == 'text') $di += 1;
  1896. else if ($struct->mimetype == 'message/rfc822') $di += 3;
  1897. if (is_array($part[$di]) && count($part[$di]) == 2) {
  1898. $struct->disposition = strtolower($part[$di][0]);
  1899. if (is_array($part[$di][1]))
  1900. for ($n=0; $n<count($part[$di][1]); $n+=2)
  1901. $struct->d_parameters[strtolower($part[$di][1][$n])] = $part[$di][1][$n+1];
  1902. }
  1903. // get message/rfc822's child-parts
  1904. if (is_array($part[8]) && $di != 8) {
  1905. $struct->parts = array();
  1906. for ($i=0, $count=0; $i<count($part[8]); $i++) {
  1907. if (!is_array($part[8][$i]))
  1908. break;
  1909. $struct->parts[] = $this->_structure_part($part[8][$i], ++$count, $struct->mime_id);
  1910. }
  1911. }
  1912. // get part ID
  1913. if (!empty($part[3])) {
  1914. $struct->content_id = $part[3];
  1915. $struct->headers['content-id'] = $part[3];
  1916. if (empty($struct->disposition))
  1917. $struct->disposition = 'inline';
  1918. }
  1919. // fetch message headers if message/rfc822 or named part (could contain Content-Location header)
  1920. if ($struct->ctype_primary == 'message' || ($struct->ctype_parameters['name'] && !$struct->content_id)) {
  1921. if (empty($mime_headers)) {
  1922. $mime_headers = $this->conn->fetchPartHeader(
  1923. $this->mailbox, $this->_msg_id, false, $struct->mime_id);
  1924. }
  1925. if (is_string($mime_headers))
  1926. $struct->headers = $this->_parse_headers($mime_headers) + $struct->headers;
  1927. else if (is_object($mime_headers))
  1928. $struct->headers = get_object_vars($mime_headers) + $struct->headers;
  1929. // get real content-type of message/rfc822
  1930. if ($struct->mimetype == 'message/rfc822') {
  1931. // single-part
  1932. if (!is_array($part[8][0]))
  1933. $struct->real_mimetype = strtolower($part[8][0] . '/' . $part[8][1]);
  1934. // multi-part
  1935. else {
  1936. for ($n=0; $n<count($part[8]); $n++)
  1937. if (!is_array($part[8][$n]))
  1938. break;
  1939. $struct->real_mimetype = 'multipart/' . strtolower($part[8][$n]);
  1940. }
  1941. }
  1942. if ($struct->ctype_primary == 'message' && empty($struct->parts)) {
  1943. if (is_array($part[8]) && $di != 8)
  1944. $struct->parts[] = $this->_structure_part($part[8], ++$count, $struct->mime_id);
  1945. }
  1946. }
  1947. // normalize filename property
  1948. $this->_set_part_filename($struct, $mime_headers);
  1949. return $struct;
  1950. }
  1951. /**
  1952. * Set attachment filename from message part structure
  1953. *
  1954. * @param rcube_message_part $part Part object
  1955. * @param string $headers Part's raw headers
  1956. * @access private
  1957. */
  1958. private function _set_part_filename(&$part, $headers=null)
  1959. {
  1960. if (!empty($part->d_parameters['filename']))
  1961. $filename_mime = $part->d_parameters['filename'];
  1962. else if (!empty($part->d_parameters['filename*']))
  1963. $filename_encoded = $part->d_parameters['filename*'];
  1964. else if (!empty($part->ctype_parameters['name*']))
  1965. $filename_encoded = $part->ctype_parameters['name*'];
  1966. // RFC2231 value continuations
  1967. // TODO: this should be rewrited to support RFC2231 4.1 combinations
  1968. else if (!empty($part->d_parameters['filename*0'])) {
  1969. $i = 0;
  1970. while (isset($part->d_parameters['filename*'.$i])) {
  1971. $filename_mime .= $part->d_parameters['filename*'.$i];
  1972. $i++;
  1973. }
  1974. // some servers (eg. dovecot-1.x) have no support for parameter value continuations
  1975. // we must fetch and parse headers "manually"
  1976. if ($i<2) {
  1977. if (!$headers) {
  1978. $headers = $this->conn->fetchPartHeader(
  1979. $this->mailbox, $this->_msg_id, false, $part->mime_id);
  1980. }
  1981. $filename_mime = '';
  1982. $i = 0;
  1983. while (preg_match('/filename\*'.$i.'\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
  1984. $filename_mime .= $matches[1];
  1985. $i++;
  1986. }
  1987. }
  1988. }
  1989. else if (!empty($part->d_parameters['filename*0*'])) {
  1990. $i = 0;
  1991. while (isset($part->d_parameters['filename*'.$i.'*'])) {
  1992. $filename_encoded .= $part->d_parameters['filename*'.$i.'*'];
  1993. $i++;
  1994. }
  1995. if ($i<2) {
  1996. if (!$headers) {
  1997. $headers = $this->conn->fetchPartHeader(
  1998. $this->mailbox, $this->_msg_id, false, $part->mime_id);
  1999. }
  2000. $filename_encoded = '';
  2001. $i = 0; $matches = array();
  2002. while (preg_match('/filename\*'.$i.'\*\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
  2003. $filename_encoded .= $matches[1];
  2004. $i++;
  2005. }
  2006. }
  2007. }
  2008. else if (!empty($part->ctype_parameters['name*0'])) {
  2009. $i = 0;
  2010. while (isset($part->ctype_parameters['name*'.$i])) {
  2011. $filename_mime .= $part->ctype_parameters['name*'.$i];
  2012. $i++;
  2013. }
  2014. if ($i<2) {
  2015. if (!$headers) {
  2016. $headers = $this->conn->fetchPartHeader(
  2017. $this->mailbox, $this->_msg_id, false, $part->mime_id);
  2018. }
  2019. $filename_mime = '';
  2020. $i = 0; $matches = array();
  2021. while (preg_match('/\s+name\*'.$i.'\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
  2022. $filename_mime .= $matches[1];
  2023. $i++;
  2024. }
  2025. }
  2026. }
  2027. else if (!empty($part->ctype_parameters['name*0*'])) {
  2028. $i = 0;
  2029. while (isset($part->ctype_parameters['name*'.$i.'*'])) {
  2030. $filename_encoded .= $part->ctype_parameters['name*'.$i.'*'];
  2031. $i++;
  2032. }
  2033. if ($i<2) {
  2034. if (!$headers) {
  2035. $headers = $this->conn->fetchPartHeader(
  2036. $this->mailbox, $this->_msg_id, false, $part->mime_id);
  2037. }
  2038. $filename_encoded = '';
  2039. $i = 0; $matches = array();
  2040. while (preg_match('/\s+name\*'.$i.'\*\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
  2041. $filename_encoded .= $matches[1];
  2042. $i++;
  2043. }
  2044. }
  2045. }
  2046. // read 'name' after rfc2231 parameters as it may contains truncated filename (from Thunderbird)
  2047. else if (!empty($part->ctype_parameters['name']))
  2048. $filename_mime = $part->ctype_parameters['name'];
  2049. // Content-Disposition
  2050. else if (!empty($part->headers['content-description']))
  2051. $filename_mime = $part->headers['content-description'];
  2052. else
  2053. return;
  2054. // decode filename
  2055. if (!empty($filename_mime)) {
  2056. $part->filename = rcube_imap::decode_mime_string($filename_mime,
  2057. $part->charset ? $part->charset : ($this->struct_charset ? $this->struct_charset :
  2058. rc_detect_encoding($filename_mime, $this->default_charset)));
  2059. }
  2060. else if (!empty($filename_encoded)) {
  2061. // decode filename according to RFC 2231, Section 4
  2062. if (preg_match("/^([^']*)'[^']*'(.*)$/", $filename_encoded, $fmatches)) {
  2063. $filename_charset = $fmatches[1];
  2064. $filename_encoded = $fmatches[2];
  2065. }
  2066. $part->filename = rcube_charset_convert(urldecode($filename_encoded), $filename_charset);
  2067. }
  2068. }
  2069. /**
  2070. * Get charset name from message structure (first part)
  2071. *
  2072. * @param array $structure Message structure
  2073. * @return string Charset name
  2074. * @access private
  2075. */
  2076. private function _structure_charset($structure)
  2077. {
  2078. while (is_array($structure)) {
  2079. if (is_array($structure[2]) && $structure[2][0] == 'charset')
  2080. return $structure[2][1];
  2081. $structure = $structure[0];
  2082. }
  2083. }
  2084. /**
  2085. * Fetch message body of a specific message from the server
  2086. *
  2087. * @param int $uid Message UID
  2088. * @param string $part Part number
  2089. * @param rcube_message_part $o_part Part object created by get_structure()
  2090. * @param mixed $print True to print part, ressource to write part contents in
  2091. * @param resource $fp File pointer to save the message part
  2092. * @param boolean $skip_charset_conv Disables charset conversion
  2093. *
  2094. * @return string Message/part body if not printed
  2095. */
  2096. function &get_message_part($uid, $part=1, $o_part=NULL, $print=NULL, $fp=NULL, $skip_charset_conv=false)
  2097. {
  2098. // get part encoding if not provided
  2099. if (!is_object($o_part)) {
  2100. $structure_str = $this->conn->fetchStructureString($this->mailbox, $uid, true);
  2101. $structure = new rcube_mime_struct();
  2102. // error or message not found
  2103. if (!$structure->loadStructure($structure_str)) {
  2104. return false;
  2105. }
  2106. $o_part = new rcube_message_part;
  2107. $o_part->ctype_primary = strtolower($structure->getPartType($part));
  2108. $o_part->encoding = strtolower($structure->getPartEncoding($part));
  2109. $o_part->charset = $structure->getPartCharset($part);
  2110. }
  2111. // TODO: Add caching for message parts
  2112. if (!$part) {
  2113. $part = 'TEXT';
  2114. }
  2115. $body = $this->conn->handlePartBody($this->mailbox, $uid, true, $part,
  2116. $o_part->encoding, $print, $fp);
  2117. if ($fp || $print) {
  2118. return true;
  2119. }
  2120. // convert charset (if text or message part)
  2121. if ($body && !$skip_charset_conv &&
  2122. preg_match('/^(text|message)$/', $o_part->ctype_primary)
  2123. ) {
  2124. if (!$o_part->charset || strtoupper($o_part->charset) == 'US-ASCII') {
  2125. $o_part->charset = $this->default_charset;
  2126. }
  2127. $body = rcube_charset_convert($body, $o_part->charset);
  2128. }
  2129. return $body;
  2130. }
  2131. /**
  2132. * Fetch message body of a specific message from the server
  2133. *
  2134. * @param int $uid Message UID
  2135. * @return string $part Message/part body
  2136. * @see rcube_imap::get_message_part()
  2137. */
  2138. function &get_body($uid, $part=1)
  2139. {
  2140. $headers = $this->get_headers($uid);
  2141. return rcube_charset_convert($this->get_message_part($uid, $part, NULL),
  2142. $headers->charset ? $headers->charset : $this->default_charset);
  2143. }
  2144. /**
  2145. * Returns the whole message source as string (or saves to a file)
  2146. *
  2147. * @param int $uid Message UID
  2148. * @param resource $fp File pointer to save the message
  2149. *
  2150. * @return string Message source string
  2151. */
  2152. function &get_raw_body($uid, $fp=null)
  2153. {
  2154. return $this->conn->handlePartBody($this->mailbox, $uid,
  2155. true, null, null, false, $fp);
  2156. }
  2157. /**
  2158. * Returns the message headers as string
  2159. *
  2160. * @param int $uid Message UID
  2161. * @return string Message headers string
  2162. */
  2163. function &get_raw_headers($uid)
  2164. {
  2165. return $this->conn->fetchPartHeader($this->mailbox, $uid, true);
  2166. }
  2167. /**
  2168. * Sends the whole message source to stdout
  2169. *
  2170. * @param int $uid Message UID
  2171. */
  2172. function print_raw_body($uid)
  2173. {
  2174. $this->conn->handlePartBody($this->mailbox, $uid, true, NULL, NULL, true);
  2175. }
  2176. /**
  2177. * Set message flag to one or several messages
  2178. *
  2179. * @param mixed $uids Message UIDs as array or comma-separated string, or '*'
  2180. * @param string $flag Flag to set: SEEN, UNDELETED, DELETED, RECENT, ANSWERED, DRAFT, MDNSENT
  2181. * @param string $mailbox Folder name
  2182. * @param boolean $skip_cache True to skip message cache clean up
  2183. *
  2184. * @return boolean Operation status
  2185. */
  2186. function set_flag($uids, $flag, $mailbox=null, $skip_cache=false)
  2187. {
  2188. if (!strlen($mailbox)) {
  2189. $mailbox = $this->mailbox;
  2190. }
  2191. $flag = strtoupper($flag);
  2192. list($uids, $all_mode) = $this->_parse_uids($uids, $mailbox);
  2193. if (strpos($flag, 'UN') === 0)
  2194. $result = $this->conn->unflag($mailbox, $uids, substr($flag, 2));
  2195. else
  2196. $result = $this->conn->flag($mailbox, $uids, $flag);
  2197. if ($result) {
  2198. // reload message headers if cached
  2199. if ($this->messages_caching && !$skip_cache) {
  2200. $cache_key = $mailbox.'.msg';
  2201. if ($all_mode)
  2202. $this->clear_message_cache($cache_key);
  2203. else
  2204. $this->remove_message_cache($cache_key, explode(',', $uids));
  2205. }
  2206. // clear cached counters
  2207. if ($flag == 'SEEN' || $flag == 'UNSEEN') {
  2208. $this->_clear_messagecount($mailbox, 'SEEN');
  2209. $this->_clear_messagecount($mailbox, 'UNSEEN');
  2210. }
  2211. else if ($flag == 'DELETED') {
  2212. $this->_clear_messagecount($mailbox, 'DELETED');
  2213. }
  2214. }
  2215. return $result;
  2216. }
  2217. /**
  2218. * Remove message flag for one or several messages
  2219. *
  2220. * @param mixed $uids Message UIDs as array or comma-separated string, or '*'
  2221. * @param string $flag Flag to unset: SEEN, DELETED, RECENT, ANSWERED, DRAFT, MDNSENT
  2222. * @param string $mailbox Folder name
  2223. *
  2224. * @return int Number of flagged messages, -1 on failure
  2225. * @see set_flag
  2226. */
  2227. function unset_flag($uids, $flag, $mailbox=null)
  2228. {
  2229. return $this->set_flag($uids, 'UN'.$flag, $mailbox);
  2230. }
  2231. /**
  2232. * Append a mail message (source) to a specific mailbox
  2233. *
  2234. * @param string $mailbox Target mailbox
  2235. * @param string $message The message source string or filename
  2236. * @param string $headers Headers string if $message contains only the body
  2237. * @param boolean $is_file True if $message is a filename
  2238. *
  2239. * @return boolean True on success, False on error
  2240. */
  2241. function save_message($mailbox, &$message, $headers='', $is_file=false)
  2242. {
  2243. if (!strlen($mailbox)) {
  2244. $mailbox = $this->mailbox;
  2245. }
  2246. // make sure mailbox exists
  2247. if ($this->mailbox_exists($mailbox)) {
  2248. if ($is_file)
  2249. $saved = $this->conn->appendFromFile($mailbox, $message, $headers);
  2250. else
  2251. $saved = $this->conn->append($mailbox, $message);
  2252. }
  2253. if ($saved) {
  2254. // increase messagecount of the target mailbox
  2255. $this->_set_messagecount($mailbox, 'ALL', 1);
  2256. }
  2257. return $saved;
  2258. }
  2259. /**
  2260. * Move a message from one mailbox to another
  2261. *
  2262. * @param mixed $uids Message UIDs as array or comma-separated string, or '*'
  2263. * @param string $to_mbox Target mailbox
  2264. * @param string $from_mbox Source mailbox
  2265. * @return boolean True on success, False on error
  2266. */
  2267. function move_message($uids, $to_mbox, $from_mbox='')
  2268. {
  2269. if (!strlen($from_mbox)) {
  2270. $from_mbox = $this->mailbox;
  2271. }
  2272. if ($to_mbox === $from_mbox) {
  2273. return false;
  2274. }
  2275. list($uids, $all_mode) = $this->_parse_uids($uids, $from_mbox);
  2276. // exit if no message uids are specified
  2277. if (empty($uids))
  2278. return false;
  2279. // make sure mailbox exists
  2280. if ($to_mbox != 'INBOX' && !$this->mailbox_exists($to_mbox)) {
  2281. if (in_array($to_mbox, $this->default_folders))
  2282. $this->create_mailbox($to_mbox, true);
  2283. else
  2284. return false;
  2285. }
  2286. $config = rcmail::get_instance()->config;
  2287. $to_trash = $to_mbox == $config->get('trash_mbox');
  2288. // flag messages as read before moving them
  2289. if ($to_trash && $config->get('read_when_deleted')) {
  2290. // don't flush cache (4th argument)
  2291. $this->set_flag($uids, 'SEEN', $from_mbox, true);
  2292. }
  2293. // move messages
  2294. $moved = $this->conn->move($uids, $from_mbox, $to_mbox);
  2295. // send expunge command in order to have the moved message
  2296. // really deleted from the source mailbox
  2297. if ($moved) {
  2298. $this->_expunge($from_mbox, false, $uids);
  2299. $this->_clear_messagecount($from_mbox);
  2300. $this->_clear_messagecount($to_mbox);
  2301. }
  2302. // moving failed
  2303. else if ($to_trash && $config->get('delete_always', false)) {
  2304. $moved = $this->delete_message($uids, $from_mbox);
  2305. }
  2306. if ($moved) {
  2307. // unset threads internal cache
  2308. unset($this->icache['threads']);
  2309. // remove message ids from search set
  2310. if ($this->search_set && $from_mbox == $this->mailbox) {
  2311. // threads are too complicated to just remove messages from set
  2312. if ($this->search_threads || $all_mode)
  2313. $this->refresh_search();
  2314. else {
  2315. $uids = explode(',', $uids);
  2316. foreach ($uids as $uid)
  2317. $a_mids[] = $this->_uid2id($uid, $from_mbox);
  2318. $this->search_set = array_diff($this->search_set, $a_mids);
  2319. }
  2320. }
  2321. // update cached message headers
  2322. $cache_key = $from_mbox.'.msg';
  2323. if ($all_mode || ($start_index = $this->get_message_cache_index_min($cache_key, $uids))) {
  2324. // clear cache from the lowest index on
  2325. $this->clear_message_cache($cache_key, $all_mode ? 1 : $start_index);
  2326. }
  2327. }
  2328. return $moved;
  2329. }
  2330. /**
  2331. * Copy a message from one mailbox to another
  2332. *
  2333. * @param mixed $uids Message UIDs as array or comma-separated string, or '*'
  2334. * @param string $to_mbox Target mailbox
  2335. * @param string $from_mbox Source mailbox
  2336. * @return boolean True on success, False on error
  2337. */
  2338. function copy_message($uids, $to_mbox, $from_mbox='')
  2339. {
  2340. if (!strlen($from_mbox)) {
  2341. $from_mbox = $this->mailbox;
  2342. }
  2343. list($uids, $all_mode) = $this->_parse_uids($uids, $from_mbox);
  2344. // exit if no message uids are specified
  2345. if (empty($uids)) {
  2346. return false;
  2347. }
  2348. // make sure mailbox exists
  2349. if ($to_mbox != 'INBOX' && !$this->mailbox_exists($to_mbox)) {
  2350. if (in_array($to_mbox, $this->default_folders))
  2351. $this->create_mailbox($to_mbox, true);
  2352. else
  2353. return false;
  2354. }
  2355. // copy messages
  2356. $copied = $this->conn->copy($uids, $from_mbox, $to_mbox);
  2357. if ($copied) {
  2358. $this->_clear_messagecount($to_mbox);
  2359. }
  2360. return $copied;
  2361. }
  2362. /**
  2363. * Mark messages as deleted and expunge mailbox
  2364. *
  2365. * @param mixed $uids Message UIDs as array or comma-separated string, or '*'
  2366. * @param string $mailbox Source mailbox
  2367. *
  2368. * @return boolean True on success, False on error
  2369. */
  2370. function delete_message($uids, $mailbox='')
  2371. {
  2372. if (!strlen($mailbox)) {
  2373. $mailbox = $this->mailbox;
  2374. }
  2375. list($uids, $all_mode) = $this->_parse_uids($uids, $mailbox);
  2376. // exit if no message uids are specified
  2377. if (empty($uids))
  2378. return false;
  2379. $deleted = $this->conn->delete($mailbox, $uids);
  2380. if ($deleted) {
  2381. // send expunge command in order to have the deleted message
  2382. // really deleted from the mailbox
  2383. $this->_expunge($mailbox, false, $uids);
  2384. $this->_clear_messagecount($mailbox);
  2385. unset($this->uid_id_map[$mailbox]);
  2386. // unset threads internal cache
  2387. unset($this->icache['threads']);
  2388. // remove message ids from search set
  2389. if ($this->search_set && $mailbox == $this->mailbox) {
  2390. // threads are too complicated to just remove messages from set
  2391. if ($this->search_threads || $all_mode)
  2392. $this->refresh_search();
  2393. else {
  2394. $uids = explode(',', $uids);
  2395. foreach ($uids as $uid)
  2396. $a_mids[] = $this->_uid2id($uid, $mailbox);
  2397. $this->search_set = array_diff($this->search_set, $a_mids);
  2398. }
  2399. }
  2400. // remove deleted messages from cache
  2401. $cache_key = $mailbox.'.msg';
  2402. if ($all_mode || ($start_index = $this->get_message_cache_index_min($cache_key, $uids))) {
  2403. // clear cache from the lowest index on
  2404. $this->clear_message_cache($cache_key, $all_mode ? 1 : $start_index);
  2405. }
  2406. }
  2407. return $deleted;
  2408. }
  2409. /**
  2410. * Clear all messages in a specific mailbox
  2411. *
  2412. * @param string $mailbox Mailbox name
  2413. *
  2414. * @return int Above 0 on success
  2415. */
  2416. function clear_mailbox($mailbox=null)
  2417. {
  2418. if (!strlen($mailbox)) {
  2419. $mailbox = $this->mailbox;
  2420. }
  2421. // SELECT will set messages count for clearFolder()
  2422. if ($this->conn->select($mailbox)) {
  2423. $cleared = $this->conn->clearFolder($mailbox);
  2424. }
  2425. // make sure the message count cache is cleared as well
  2426. if ($cleared) {
  2427. $this->clear_message_cache($mailbox.'.msg');
  2428. $a_mailbox_cache = $this->get_cache('messagecount');
  2429. unset($a_mailbox_cache[$mailbox]);
  2430. $this->update_cache('messagecount', $a_mailbox_cache);
  2431. }
  2432. return $cleared;
  2433. }
  2434. /**
  2435. * Send IMAP expunge command and clear cache
  2436. *
  2437. * @param string $mailbox Mailbox name
  2438. * @param boolean $clear_cache False if cache should not be cleared
  2439. *
  2440. * @return boolean True on success
  2441. */
  2442. function expunge($mailbox='', $clear_cache=true)
  2443. {
  2444. if (!strlen($mailbox)) {
  2445. $mailbox = $this->mailbox;
  2446. }
  2447. return $this->_expunge($mailbox, $clear_cache);
  2448. }
  2449. /**
  2450. * Send IMAP expunge command and clear cache
  2451. *
  2452. * @param string $mailbox Mailbox name
  2453. * @param boolean $clear_cache False if cache should not be cleared
  2454. * @param mixed $uids Message UIDs as array or comma-separated string, or '*'
  2455. * @return boolean True on success
  2456. * @access private
  2457. * @see rcube_imap::expunge()
  2458. */
  2459. private function _expunge($mailbox, $clear_cache=true, $uids=NULL)
  2460. {
  2461. if ($uids && $this->get_capability('UIDPLUS'))
  2462. $a_uids = is_array($uids) ? join(',', $uids) : $uids;
  2463. else
  2464. $a_uids = NULL;
  2465. // force mailbox selection and check if mailbox is writeable
  2466. // to prevent a situation when CLOSE is executed on closed
  2467. // or EXPUNGE on read-only mailbox
  2468. $result = $this->conn->select($mailbox);
  2469. if (!$result) {
  2470. return false;
  2471. }
  2472. if (!$this->conn->data['READ-WRITE']) {
  2473. $this->conn->setError(rcube_imap_generic::ERROR_READONLY, "Mailbox is read-only");
  2474. return false;
  2475. }
  2476. // CLOSE(+SELECT) should be faster than EXPUNGE
  2477. if (empty($a_uids) || $a_uids == '1:*')
  2478. $result = $this->conn->close();
  2479. else
  2480. $result = $this->conn->expunge($mailbox, $a_uids);
  2481. if ($result && $clear_cache) {
  2482. $this->clear_message_cache($mailbox.'.msg');
  2483. $this->_clear_messagecount($mailbox);
  2484. }
  2485. return $result;
  2486. }
  2487. /**
  2488. * Parse message UIDs input
  2489. *
  2490. * @param mixed $uids UIDs array or comma-separated list or '*' or '1:*'
  2491. * @param string $mailbox Mailbox name
  2492. * @return array Two elements array with UIDs converted to list and ALL flag
  2493. * @access private
  2494. */
  2495. private function _parse_uids($uids, $mailbox)
  2496. {
  2497. if ($uids === '*' || $uids === '1:*') {
  2498. if (empty($this->search_set)) {
  2499. $uids = '1:*';
  2500. $all = true;
  2501. }
  2502. // get UIDs from current search set
  2503. // @TODO: skip fetchUIDs() and work with IDs instead of UIDs (?)
  2504. else {
  2505. if ($this->search_threads)
  2506. $uids = $this->conn->fetchUIDs($mailbox, array_keys($this->search_set['depth']));
  2507. else
  2508. $uids = $this->conn->fetchUIDs($mailbox, $this->search_set);
  2509. // save ID-to-UID mapping in local cache
  2510. if (is_array($uids))
  2511. foreach ($uids as $id => $uid)
  2512. $this->uid_id_map[$mailbox][$uid] = $id;
  2513. $uids = join(',', $uids);
  2514. }
  2515. }
  2516. else {
  2517. if (is_array($uids))
  2518. $uids = join(',', $uids);
  2519. if (preg_match('/[^0-9,]/', $uids))
  2520. $uids = '';
  2521. }
  2522. return array($uids, (bool) $all);
  2523. }
  2524. /**
  2525. * Translate UID to message ID
  2526. *
  2527. * @param int $uid Message UID
  2528. * @param string $mailbox Mailbox name
  2529. *
  2530. * @return int Message ID
  2531. */
  2532. function get_id($uid, $mailbox=null)
  2533. {
  2534. if (!strlen($mailbox)) {
  2535. $mailbox = $this->mailbox;
  2536. }
  2537. return $this->_uid2id($uid, $mailbox);
  2538. }
  2539. /**
  2540. * Translate message number to UID
  2541. *
  2542. * @param int $id Message ID
  2543. * @param string $mailbox Mailbox name
  2544. *
  2545. * @return int Message UID
  2546. */
  2547. function get_uid($id, $mailbox=null)
  2548. {
  2549. if (!strlen($mailbox)) {
  2550. $mailbox = $this->mailbox;
  2551. }
  2552. return $this->_id2uid($id, $mailbox);
  2553. }
  2554. /* --------------------------------
  2555. * folder managment
  2556. * --------------------------------*/
  2557. /**
  2558. * Public method for listing subscribed folders
  2559. *
  2560. * @param string $root Optional root folder
  2561. * @param string $name Optional name pattern
  2562. * @param string $filter Optional filter
  2563. *
  2564. * @return array List of mailboxes/folders
  2565. * @access public
  2566. */
  2567. function list_mailboxes($root='', $name='*', $filter=null)
  2568. {
  2569. $a_mboxes = $this->_list_mailboxes($root, $name, $filter);
  2570. // INBOX should always be available
  2571. if ((!$filter || $filter == 'mail') && !in_array('INBOX', $a_mboxes)) {
  2572. array_unshift($a_mboxes, 'INBOX');
  2573. }
  2574. // sort mailboxes
  2575. $a_mboxes = $this->_sort_mailbox_list($a_mboxes);
  2576. return $a_mboxes;
  2577. }
  2578. /**
  2579. * Private method for mailbox listing
  2580. *
  2581. * @param string $root Optional root folder
  2582. * @param string $name Optional name pattern
  2583. * @param mixed $filter Optional filter
  2584. *
  2585. * @return array List of mailboxes/folders
  2586. * @see rcube_imap::list_mailboxes()
  2587. * @access private
  2588. */
  2589. private function _list_mailboxes($root='', $name='*', $filter=null)
  2590. {
  2591. $cache_key = $root.':'.$name;
  2592. if (!empty($filter)) {
  2593. $cache_key .= ':'.(is_string($filter) ? $filter : serialize($filter));
  2594. }
  2595. $cache_key = 'mailboxes.'.md5($cache_key);
  2596. // get cached folder list
  2597. $a_mboxes = $this->get_cache($cache_key);
  2598. if (is_array($a_mboxes)) {
  2599. return $a_mboxes;
  2600. }
  2601. $a_defaults = $a_out = array();
  2602. // Give plugins a chance to provide a list of mailboxes
  2603. $data = rcmail::get_instance()->plugins->exec_hook('mailboxes_list',
  2604. array('root' => $root, 'name' => $name, 'filter' => $filter, 'mode' => 'LSUB'));
  2605. if (isset($data['folders'])) {
  2606. $a_folders = $data['folders'];
  2607. }
  2608. else {
  2609. // Server supports LIST-EXTENDED, we can use selection options
  2610. $config = rcmail::get_instance()->config;
  2611. // #1486225: Some dovecot versions returns wrong result using LIST-EXTENDED
  2612. if (!$config->get('imap_force_lsub') && $this->get_capability('LIST-EXTENDED')) {
  2613. // This will also set mailbox options, LSUB doesn't do that
  2614. $a_folders = $this->conn->listMailboxes($root, $name,
  2615. NULL, array('SUBSCRIBED'));
  2616. // remove non-existent folders
  2617. if (is_array($a_folders)) {
  2618. foreach ($a_folders as $idx => $folder) {
  2619. if ($this->conn->data['LIST'] && ($opts = $this->conn->data['LIST'][$folder])
  2620. && in_array('\\NonExistent', $opts)
  2621. ) {
  2622. unset($a_folders[$idx]);
  2623. }
  2624. }
  2625. }
  2626. }
  2627. // retrieve list of folders from IMAP server using LSUB
  2628. else {
  2629. $a_folders = $this->conn->listSubscribed($root, $name);
  2630. }
  2631. }
  2632. if (!is_array($a_folders) || !sizeof($a_folders)) {
  2633. $a_folders = array();
  2634. }
  2635. // write mailboxlist to cache
  2636. $this->update_cache($cache_key, $a_folders);
  2637. return $a_folders;
  2638. }
  2639. /**
  2640. * Get a list of all folders available on the IMAP server
  2641. *
  2642. * @param string $root IMAP root dir
  2643. * @param string $name Optional name pattern
  2644. * @param mixed $filter Optional filter
  2645. *
  2646. * @return array Indexed array with folder names
  2647. */
  2648. function list_unsubscribed($root='', $name='*', $filter=null)
  2649. {
  2650. // @TODO: caching
  2651. // Give plugins a chance to provide a list of mailboxes
  2652. $data = rcmail::get_instance()->plugins->exec_hook('mailboxes_list',
  2653. array('root' => $root, 'name' => $name, 'filter' => $filter, 'mode' => 'LIST'));
  2654. if (isset($data['folders'])) {
  2655. $a_mboxes = $data['folders'];
  2656. }
  2657. else {
  2658. // retrieve list of folders from IMAP server
  2659. $a_mboxes = $this->conn->listMailboxes($root, $name);
  2660. }
  2661. if (!is_array($a_mboxes)) {
  2662. $a_mboxes = array();
  2663. }
  2664. // INBOX should always be available
  2665. if ((!$filter || $filter == 'mail') && !in_array('INBOX', $a_mboxes)) {
  2666. array_unshift($a_mboxes, 'INBOX');
  2667. }
  2668. // filter folders and sort them
  2669. $a_mboxes = $this->_sort_mailbox_list($a_mboxes);
  2670. return $a_mboxes;
  2671. }
  2672. /**
  2673. * Get mailbox quota information
  2674. * added by Nuny
  2675. *
  2676. * @return mixed Quota info or False if not supported
  2677. */
  2678. function get_quota()
  2679. {
  2680. if ($this->get_capability('QUOTA'))
  2681. return $this->conn->getQuota();
  2682. return false;
  2683. }
  2684. /**
  2685. * Get mailbox size (size of all messages in a mailbox)
  2686. *
  2687. * @param string $mailbox Mailbox name
  2688. *
  2689. * @return int Mailbox size in bytes, False on error
  2690. */
  2691. function get_mailbox_size($mailbox)
  2692. {
  2693. // @TODO: could we try to use QUOTA here?
  2694. $result = $this->conn->fetchHeaderIndex($mailbox, '1:*', 'SIZE', false);
  2695. if (is_array($result))
  2696. $result = array_sum($result);
  2697. return $result;
  2698. }
  2699. /**
  2700. * Subscribe to a specific mailbox(es)
  2701. *
  2702. * @param array $a_mboxes Mailbox name(s)
  2703. * @return boolean True on success
  2704. */
  2705. function subscribe($a_mboxes)
  2706. {
  2707. if (!is_array($a_mboxes))
  2708. $a_mboxes = array($a_mboxes);
  2709. // let this common function do the main work
  2710. return $this->_change_subscription($a_mboxes, 'subscribe');
  2711. }
  2712. /**
  2713. * Unsubscribe mailboxes
  2714. *
  2715. * @param array $a_mboxes Mailbox name(s)
  2716. * @return boolean True on success
  2717. */
  2718. function unsubscribe($a_mboxes)
  2719. {
  2720. if (!is_array($a_mboxes))
  2721. $a_mboxes = array($a_mboxes);
  2722. // let this common function do the main work
  2723. return $this->_change_subscription($a_mboxes, 'unsubscribe');
  2724. }
  2725. /**
  2726. * Create a new mailbox on the server and register it in local cache
  2727. *
  2728. * @param string $mailbox New mailbox name
  2729. * @param boolean $subscribe True if the new mailbox should be subscribed
  2730. *
  2731. * @return boolean True on success
  2732. */
  2733. function create_mailbox($mailbox, $subscribe=false)
  2734. {
  2735. $result = $this->conn->createFolder($mailbox);
  2736. // try to subscribe it
  2737. if ($result) {
  2738. // clear cache
  2739. $this->clear_cache('mailboxes', true);
  2740. if ($subscribe)
  2741. $this->subscribe($mailbox);
  2742. }
  2743. return $result;
  2744. }
  2745. /**
  2746. * Set a new name to an existing mailbox
  2747. *
  2748. * @param string $mailbox Mailbox to rename
  2749. * @param string $new_name New mailbox name
  2750. *
  2751. * @return boolean True on success
  2752. */
  2753. function rename_mailbox($mailbox, $new_name)
  2754. {
  2755. if (!strlen($new_name)) {
  2756. return false;
  2757. }
  2758. $delm = $this->get_hierarchy_delimiter();
  2759. // get list of subscribed folders
  2760. if ((strpos($mailbox, '%') === false) && (strpos($mailbox, '*') === false)) {
  2761. $a_subscribed = $this->_list_mailboxes('', $mailbox . $delm . '*');
  2762. $subscribed = $this->mailbox_exists($mailbox, true);
  2763. }
  2764. else {
  2765. $a_subscribed = $this->_list_mailboxes();
  2766. $subscribed = in_array($mailbox, $a_subscribed);
  2767. }
  2768. $result = $this->conn->renameFolder($mailbox, $new_name);
  2769. if ($result) {
  2770. // unsubscribe the old folder, subscribe the new one
  2771. if ($subscribed) {
  2772. $this->conn->unsubscribe($mailbox);
  2773. $this->conn->subscribe($new_name);
  2774. }
  2775. // check if mailbox children are subscribed
  2776. foreach ($a_subscribed as $c_subscribed) {
  2777. if (preg_match('/^'.preg_quote($mailbox.$delm, '/').'/', $c_subscribed)) {
  2778. $this->conn->unsubscribe($c_subscribed);
  2779. $this->conn->subscribe(preg_replace('/^'.preg_quote($mailbox, '/').'/',
  2780. $new_name, $c_subscribed));
  2781. }
  2782. }
  2783. // clear cache
  2784. $this->clear_message_cache($mailbox.'.msg');
  2785. $this->clear_cache('mailboxes', true);
  2786. }
  2787. return $result;
  2788. }
  2789. /**
  2790. * Remove mailbox from server
  2791. *
  2792. * @param string $mailbox Mailbox name
  2793. *
  2794. * @return boolean True on success
  2795. */
  2796. function delete_mailbox($mailbox)
  2797. {
  2798. $delm = $this->get_hierarchy_delimiter();
  2799. // get list of folders
  2800. if ((strpos($mailbox, '%') === false) && (strpos($mailbox, '*') === false))
  2801. $sub_mboxes = $this->list_unsubscribed('', $mailbox . $delm . '*');
  2802. else
  2803. $sub_mboxes = $this->list_unsubscribed();
  2804. // send delete command to server
  2805. $result = $this->conn->deleteFolder($mailbox);
  2806. if ($result) {
  2807. // unsubscribe mailbox
  2808. $this->conn->unsubscribe($mailbox);
  2809. foreach ($sub_mboxes as $c_mbox) {
  2810. if (preg_match('/^'.preg_quote($mailbox.$delm, '/').'/', $c_mbox)) {
  2811. $this->conn->unsubscribe($c_mbox);
  2812. if ($this->conn->deleteFolder($c_mbox)) {
  2813. $this->clear_message_cache($c_mbox.'.msg');
  2814. }
  2815. }
  2816. }
  2817. // clear mailbox-related cache
  2818. $this->clear_message_cache($mailbox.'.msg');
  2819. $this->clear_cache('mailboxes', true);
  2820. }
  2821. return $result;
  2822. }
  2823. /**
  2824. * Create all folders specified as default
  2825. */
  2826. function create_default_folders()
  2827. {
  2828. // create default folders if they do not exist
  2829. foreach ($this->default_folders as $folder) {
  2830. if (!$this->mailbox_exists($folder))
  2831. $this->create_mailbox($folder, true);
  2832. else if (!$this->mailbox_exists($folder, true))
  2833. $this->subscribe($folder);
  2834. }
  2835. }
  2836. /**
  2837. * Checks if folder exists and is subscribed
  2838. *
  2839. * @param string $mailbox Folder name
  2840. * @param boolean $subscription Enable subscription checking
  2841. *
  2842. * @return boolean TRUE or FALSE
  2843. */
  2844. function mailbox_exists($mailbox, $subscription=false)
  2845. {
  2846. if ($mailbox == 'INBOX') {
  2847. return true;
  2848. }
  2849. $key = $subscription ? 'subscribed' : 'existing';
  2850. if (is_array($this->icache[$key]) && in_array($mailbox, $this->icache[$key]))
  2851. return true;
  2852. if ($subscription) {
  2853. $a_folders = $this->conn->listSubscribed('', $mailbox);
  2854. }
  2855. else {
  2856. $a_folders = $this->conn->listMailboxes('', $mailbox);
  2857. }
  2858. if (is_array($a_folders) && in_array($mailbox, $a_folders)) {
  2859. $this->icache[$key][] = $mailbox;
  2860. return true;
  2861. }
  2862. return false;
  2863. }
  2864. /**
  2865. * Returns the namespace where the folder is in
  2866. *
  2867. * @param string $mailbox Folder name
  2868. *
  2869. * @return string One of 'personal', 'other' or 'shared'
  2870. * @access public
  2871. */
  2872. function mailbox_namespace($mailbox)
  2873. {
  2874. if ($mailbox == 'INBOX') {
  2875. return 'personal';
  2876. }
  2877. foreach ($this->namespace as $type => $namespace) {
  2878. if (is_array($namespace)) {
  2879. foreach ($namespace as $ns) {
  2880. if (strlen($ns[0])) {
  2881. if ((strlen($ns[0])>1 && $mailbox == substr($ns[0], 0, -1))
  2882. || strpos($mailbox, $ns[0]) === 0
  2883. ) {
  2884. return $type;
  2885. }
  2886. }
  2887. }
  2888. }
  2889. }
  2890. return 'personal';
  2891. }
  2892. /**
  2893. * Modify folder name according to namespace.
  2894. * For output it removes prefix of the personal namespace if it's possible.
  2895. * For input it adds the prefix. Use it before creating a folder in root
  2896. * of the folders tree.
  2897. *
  2898. * @param string $mailbox Folder name
  2899. * @param string $mode Mode name (out/in)
  2900. *
  2901. * @return string Folder name
  2902. */
  2903. function mod_mailbox($mailbox, $mode = 'out')
  2904. {
  2905. if (!strlen($mailbox)) {
  2906. return $mailbox;
  2907. }
  2908. $prefix = $this->namespace['prefix']; // see set_env()
  2909. $prefix_len = strlen($prefix);
  2910. if (!$prefix_len) {
  2911. return $mailbox;
  2912. }
  2913. // remove prefix for output
  2914. if ($mode == 'out') {
  2915. if (substr($mailbox, 0, $prefix_len) === $prefix) {
  2916. return substr($mailbox, $prefix_len);
  2917. }
  2918. }
  2919. // add prefix for input (e.g. folder creation)
  2920. else {
  2921. return $prefix . $mailbox;
  2922. }
  2923. return $mailbox;
  2924. }
  2925. /**
  2926. * Gets folder options from LIST response, e.g. \Noselect, \Noinferiors
  2927. *
  2928. * @param string $mailbox Folder name
  2929. * @param bool $force Set to True if options should be refreshed
  2930. * Options are available after LIST command only
  2931. *
  2932. * @return array Options list
  2933. */
  2934. function mailbox_options($mailbox, $force=false)
  2935. {
  2936. if ($mailbox == 'INBOX') {
  2937. return array();
  2938. }
  2939. if (!is_array($this->conn->data['LIST']) || !is_array($this->conn->data['LIST'][$mailbox])) {
  2940. if ($force) {
  2941. $this->conn->listMailboxes('', $mailbox);
  2942. }
  2943. else {
  2944. return array();
  2945. }
  2946. }
  2947. $opts = $this->conn->data['LIST'][$mailbox];
  2948. return is_array($opts) ? $opts : array();
  2949. }
  2950. /**
  2951. * Returns extended information about the folder
  2952. *
  2953. * @param string $mailbox Folder name
  2954. *
  2955. * @return array Data
  2956. */
  2957. function mailbox_info($mailbox)
  2958. {
  2959. $acl = $this->get_capability('ACL');
  2960. $namespace = $this->get_namespace();
  2961. $options = array();
  2962. // check if the folder is a namespace prefix
  2963. if (!empty($namespace)) {
  2964. $mbox = $mailbox . $this->delimiter;
  2965. foreach ($namespace as $ns) {
  2966. if (!empty($ns)) {
  2967. foreach ($ns as $item) {
  2968. if ($item[0] === $mbox) {
  2969. $options['is_root'] = true;
  2970. break 2;
  2971. }
  2972. }
  2973. }
  2974. }
  2975. }
  2976. // check if the folder is other user virtual-root
  2977. if (!$options['is_root'] && !empty($namespace) && !empty($namespace['other'])) {
  2978. $parts = explode($this->delimiter, $mailbox);
  2979. if (count($parts) == 2) {
  2980. $mbox = $parts[0] . $this->delimiter;
  2981. foreach ($namespace['other'] as $item) {
  2982. if ($item[0] === $mbox) {
  2983. $options['is_root'] = true;
  2984. break;
  2985. }
  2986. }
  2987. }
  2988. }
  2989. $options['name'] = $mailbox;
  2990. $options['options'] = $this->mailbox_options($mailbox, true);
  2991. $options['namespace'] = $this->mailbox_namespace($mailbox);
  2992. $options['rights'] = $acl && !$options['is_root'] ? (array)$this->my_rights($mailbox) : array();
  2993. $options['special'] = in_array($mailbox, $this->default_folders);
  2994. // Set 'noselect' and 'norename' flags
  2995. if (is_array($options['options'])) {
  2996. foreach ($options['options'] as $opt) {
  2997. $opt = strtolower($opt);
  2998. if ($opt == '\noselect' || $opt == '\nonexistent') {
  2999. $options['noselect'] = true;
  3000. }
  3001. }
  3002. }
  3003. else {
  3004. $options['noselect'] = true;
  3005. }
  3006. if (!empty($options['rights'])) {
  3007. $options['norename'] = !in_array('x', $options['rights']);
  3008. if (!$options['noselect']) {
  3009. $options['noselect'] = !in_array('r', $options['rights']);
  3010. }
  3011. }
  3012. else {
  3013. $options['norename'] = $options['is_root'] || $options['namespace'] != 'personal';
  3014. }
  3015. return $options;
  3016. }
  3017. /**
  3018. * Get message header names for rcube_imap_generic::fetchHeader(s)
  3019. *
  3020. * @return string Space-separated list of header names
  3021. */
  3022. private function get_fetch_headers()
  3023. {
  3024. $headers = explode(' ', $this->fetch_add_headers);
  3025. $headers = array_map('strtoupper', $headers);
  3026. if ($this->messages_caching || $this->get_all_headers)
  3027. $headers = array_merge($headers, $this->all_headers);
  3028. return implode(' ', array_unique($headers));
  3029. }
  3030. /* -----------------------------------------
  3031. * ACL and METADATA/ANNOTATEMORE methods
  3032. * ----------------------------------------*/
  3033. /**
  3034. * Changes the ACL on the specified mailbox (SETACL)
  3035. *
  3036. * @param string $mailbox Mailbox name
  3037. * @param string $user User name
  3038. * @param string $acl ACL string
  3039. *
  3040. * @return boolean True on success, False on failure
  3041. *
  3042. * @access public
  3043. * @since 0.5-beta
  3044. */
  3045. function set_acl($mailbox, $user, $acl)
  3046. {
  3047. if ($this->get_capability('ACL'))
  3048. return $this->conn->setACL($mailbox, $user, $acl);
  3049. return false;
  3050. }
  3051. /**
  3052. * Removes any <identifier,rights> pair for the
  3053. * specified user from the ACL for the specified
  3054. * mailbox (DELETEACL)
  3055. *
  3056. * @param string $mailbox Mailbox name
  3057. * @param string $user User name
  3058. *
  3059. * @return boolean True on success, False on failure
  3060. *
  3061. * @access public
  3062. * @since 0.5-beta
  3063. */
  3064. function delete_acl($mailbox, $user)
  3065. {
  3066. if ($this->get_capability('ACL'))
  3067. return $this->conn->deleteACL($mailbox, $user);
  3068. return false;
  3069. }
  3070. /**
  3071. * Returns the access control list for mailbox (GETACL)
  3072. *
  3073. * @param string $mailbox Mailbox name
  3074. *
  3075. * @return array User-rights array on success, NULL on error
  3076. * @access public
  3077. * @since 0.5-beta
  3078. */
  3079. function get_acl($mailbox)
  3080. {
  3081. if ($this->get_capability('ACL'))
  3082. return $this->conn->getACL($mailbox);
  3083. return NULL;
  3084. }
  3085. /**
  3086. * Returns information about what rights can be granted to the
  3087. * user (identifier) in the ACL for the mailbox (LISTRIGHTS)
  3088. *
  3089. * @param string $mailbox Mailbox name
  3090. * @param string $user User name
  3091. *
  3092. * @return array List of user rights
  3093. * @access public
  3094. * @since 0.5-beta
  3095. */
  3096. function list_rights($mailbox, $user)
  3097. {
  3098. if ($this->get_capability('ACL'))
  3099. return $this->conn->listRights($mailbox, $user);
  3100. return NULL;
  3101. }
  3102. /**
  3103. * Returns the set of rights that the current user has to
  3104. * mailbox (MYRIGHTS)
  3105. *
  3106. * @param string $mailbox Mailbox name
  3107. *
  3108. * @return array MYRIGHTS response on success, NULL on error
  3109. * @access public
  3110. * @since 0.5-beta
  3111. */
  3112. function my_rights($mailbox)
  3113. {
  3114. if ($this->get_capability('ACL'))
  3115. return $this->conn->myRights($mailbox);
  3116. return NULL;
  3117. }
  3118. /**
  3119. * Sets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
  3120. *
  3121. * @param string $mailbox Mailbox name (empty for server metadata)
  3122. * @param array $entries Entry-value array (use NULL value as NIL)
  3123. *
  3124. * @return boolean True on success, False on failure
  3125. * @access public
  3126. * @since 0.5-beta
  3127. */
  3128. function set_metadata($mailbox, $entries)
  3129. {
  3130. if ($this->get_capability('METADATA') ||
  3131. (!strlen($mailbox) && $this->get_capability('METADATA-SERVER'))
  3132. ) {
  3133. return $this->conn->setMetadata($mailbox, $entries);
  3134. }
  3135. else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
  3136. foreach ((array)$entries as $entry => $value) {
  3137. list($ent, $attr) = $this->md2annotate($entry);
  3138. $entries[$entry] = array($ent, $attr, $value);
  3139. }
  3140. return $this->conn->setAnnotation($mailbox, $entries);
  3141. }
  3142. return false;
  3143. }
  3144. /**
  3145. * Unsets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
  3146. *
  3147. * @param string $mailbox Mailbox name (empty for server metadata)
  3148. * @param array $entries Entry names array
  3149. *
  3150. * @return boolean True on success, False on failure
  3151. *
  3152. * @access public
  3153. * @since 0.5-beta
  3154. */
  3155. function delete_metadata($mailbox, $entries)
  3156. {
  3157. if ($this->get_capability('METADATA') ||
  3158. (!strlen($mailbox) && $this->get_capability('METADATA-SERVER'))
  3159. ) {
  3160. return $this->conn->deleteMetadata($mailbox, $entries);
  3161. }
  3162. else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
  3163. foreach ((array)$entries as $idx => $entry) {
  3164. list($ent, $attr) = $this->md2annotate($entry);
  3165. $entries[$idx] = array($ent, $attr, NULL);
  3166. }
  3167. return $this->conn->setAnnotation($mailbox, $entries);
  3168. }
  3169. return false;
  3170. }
  3171. /**
  3172. * Returns IMAP metadata/annotations (GETMETADATA/GETANNOTATION)
  3173. *
  3174. * @param string $mailbox Mailbox name (empty for server metadata)
  3175. * @param array $entries Entries
  3176. * @param array $options Command options (with MAXSIZE and DEPTH keys)
  3177. *
  3178. * @return array Metadata entry-value hash array on success, NULL on error
  3179. *
  3180. * @access public
  3181. * @since 0.5-beta
  3182. */
  3183. function get_metadata($mailbox, $entries, $options=array())
  3184. {
  3185. if ($this->get_capability('METADATA') ||
  3186. (!strlen($mailbox) && $this->get_capability('METADATA-SERVER'))
  3187. ) {
  3188. return $this->conn->getMetadata($mailbox, $entries, $options);
  3189. }
  3190. else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
  3191. $queries = array();
  3192. $res = array();
  3193. // Convert entry names
  3194. foreach ((array)$entries as $entry) {
  3195. list($ent, $attr) = $this->md2annotate($entry);
  3196. $queries[$attr][] = $ent;
  3197. }
  3198. // @TODO: Honor MAXSIZE and DEPTH options
  3199. foreach ($queries as $attrib => $entry)
  3200. if ($result = $this->conn->getAnnotation($mailbox, $entry, $attrib))
  3201. $res = array_merge($res, $result);
  3202. return $res;
  3203. }
  3204. return NULL;
  3205. }
  3206. /**
  3207. * Converts the METADATA extension entry name into the correct
  3208. * entry-attrib names for older ANNOTATEMORE version.
  3209. *
  3210. * @param string $entry Entry name
  3211. *
  3212. * @return array Entry-attribute list, NULL if not supported (?)
  3213. */
  3214. private function md2annotate($entry)
  3215. {
  3216. if (substr($entry, 0, 7) == '/shared') {
  3217. return array(substr($entry, 7), 'value.shared');
  3218. }
  3219. else if (substr($entry, 0, 8) == '/private') {
  3220. return array(substr($entry, 8), 'value.priv');
  3221. }
  3222. // @TODO: log error
  3223. return NULL;
  3224. }
  3225. /* --------------------------------
  3226. * internal caching methods
  3227. * --------------------------------*/
  3228. /**
  3229. * Enable or disable indexes caching
  3230. *
  3231. * @param boolean $type Cache type (@see rcmail::get_cache)
  3232. * @access public
  3233. */
  3234. function set_caching($type)
  3235. {
  3236. if ($type) {
  3237. $this->caching = true;
  3238. }
  3239. else {
  3240. if ($this->cache)
  3241. $this->cache->close();
  3242. $this->cache = null;
  3243. $this->caching = false;
  3244. }
  3245. }
  3246. /**
  3247. * Getter for IMAP cache object
  3248. */
  3249. private function get_cache_engine()
  3250. {
  3251. if ($this->caching && !$this->cache) {
  3252. $rcmail = rcmail::get_instance();
  3253. $this->cache = $rcmail->get_cache('IMAP', $type);
  3254. }
  3255. return $this->cache;
  3256. }
  3257. /**
  3258. * Returns cached value
  3259. *
  3260. * @param string $key Cache key
  3261. * @return mixed
  3262. * @access public
  3263. */
  3264. function get_cache($key)
  3265. {
  3266. if ($cache = $this->get_cache_engine()) {
  3267. return $cache->get($key);
  3268. }
  3269. }
  3270. /**
  3271. * Update cache
  3272. *
  3273. * @param string $key Cache key
  3274. * @param mixed $data Data
  3275. * @access public
  3276. */
  3277. function update_cache($key, $data)
  3278. {
  3279. if ($cache = $this->get_cache_engine()) {
  3280. $cache->set($key, $data);
  3281. }
  3282. }
  3283. /**
  3284. * Clears the cache.
  3285. *
  3286. * @param string $key Cache key name or pattern
  3287. * @param boolean $prefix_mode Enable it to clear all keys starting
  3288. * with prefix specified in $key
  3289. * @access public
  3290. */
  3291. function clear_cache($key=null, $prefix_mode=false)
  3292. {
  3293. if ($cache = $this->get_cache_engine()) {
  3294. $cache->remove($key, $prefix_mode);
  3295. }
  3296. }
  3297. /* --------------------------------
  3298. * message caching methods
  3299. * --------------------------------*/
  3300. /**
  3301. * Enable or disable messages caching
  3302. *
  3303. * @param boolean $set Flag
  3304. * @access public
  3305. */
  3306. function set_messages_caching($set)
  3307. {
  3308. $rcmail = rcmail::get_instance();
  3309. if ($set && ($dbh = $rcmail->get_dbh())) {
  3310. $this->db = $dbh;
  3311. $this->messages_caching = true;
  3312. }
  3313. else {
  3314. $this->messages_caching = false;
  3315. }
  3316. }
  3317. /**
  3318. * Checks if the cache is up-to-date
  3319. *
  3320. * @param string $mailbox Mailbox name
  3321. * @param string $cache_key Internal cache key
  3322. * @return int Cache status: -3 = off, -2 = incomplete, -1 = dirty, 1 = OK
  3323. */
  3324. private function check_cache_status($mailbox, $cache_key)
  3325. {
  3326. if (!$this->messages_caching)
  3327. return -3;
  3328. $cache_index = $this->get_message_cache_index($cache_key);
  3329. $msg_count = $this->_messagecount($mailbox);
  3330. $cache_count = count($cache_index);
  3331. // empty mailbox
  3332. if (!$msg_count) {
  3333. return $cache_count ? -2 : 1;
  3334. }
  3335. if ($cache_count == $msg_count) {
  3336. if ($this->skip_deleted) {
  3337. if (!empty($this->icache['all_undeleted_idx'])) {
  3338. $uids = rcube_imap_generic::uncompressMessageSet($this->icache['all_undeleted_idx']);
  3339. $uids = array_flip($uids);
  3340. foreach ($cache_index as $uid) {
  3341. unset($uids[$uid]);
  3342. }
  3343. }
  3344. else {
  3345. // get all undeleted messages excluding cached UIDs
  3346. $uids = $this->search_once($mailbox, 'ALL UNDELETED NOT UID '.
  3347. rcube_imap_generic::compressMessageSet($cache_index));
  3348. }
  3349. if (empty($uids)) {
  3350. return 1;
  3351. }
  3352. } else {
  3353. // get UID of the message with highest index
  3354. $uid = $this->_id2uid($msg_count, $mailbox);
  3355. $cache_uid = array_pop($cache_index);
  3356. // uids of highest message matches -> cache seems OK
  3357. if ($cache_uid == $uid) {
  3358. return 1;
  3359. }
  3360. }
  3361. // cache is dirty
  3362. return -1;
  3363. }
  3364. // if cache count differs less than 10% report as dirty
  3365. return (abs($msg_count - $cache_count) < $msg_count/10) ? -1 : -2;
  3366. }
  3367. /**
  3368. * @param string $key Cache key
  3369. * @param string $from
  3370. * @param string $to
  3371. * @param string $sort_field
  3372. * @param string $sort_order
  3373. * @access private
  3374. */
  3375. private function get_message_cache($key, $from, $to, $sort_field, $sort_order)
  3376. {
  3377. if (!$this->messages_caching)
  3378. return NULL;
  3379. // use idx sort as default sorting
  3380. if (!$sort_field || !in_array($sort_field, $this->db_header_fields)) {
  3381. $sort_field = 'idx';
  3382. }
  3383. $result = array();
  3384. $sql_result = $this->db->limitquery(
  3385. "SELECT idx, uid, headers".
  3386. " FROM ".get_table_name('messages').
  3387. " WHERE user_id=?".
  3388. " AND cache_key=?".
  3389. " ORDER BY ".$this->db->quoteIdentifier($sort_field)." ".strtoupper($sort_order),
  3390. $from,
  3391. $to - $from,
  3392. $_SESSION['user_id'],
  3393. $key);
  3394. while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
  3395. $uid = intval($sql_arr['uid']);
  3396. $result[$uid] = $this->db->decode(unserialize($sql_arr['headers']));
  3397. // featch headers if unserialize failed
  3398. if (empty($result[$uid]))
  3399. $result[$uid] = $this->conn->fetchHeader(
  3400. preg_replace('/.msg$/', '', $key), $uid, true, false, $this->get_fetch_headers());
  3401. }
  3402. return $result;
  3403. }
  3404. /**
  3405. * @param string $key Cache key
  3406. * @param int $uid Message UID
  3407. * @return mixed
  3408. * @access private
  3409. */
  3410. private function &get_cached_message($key, $uid)
  3411. {
  3412. $internal_key = 'message';
  3413. if ($this->messages_caching && !isset($this->icache[$internal_key][$uid])) {
  3414. $sql_result = $this->db->query(
  3415. "SELECT idx, headers, structure, message_id".
  3416. " FROM ".get_table_name('messages').
  3417. " WHERE user_id=?".
  3418. " AND cache_key=?".
  3419. " AND uid=?",
  3420. $_SESSION['user_id'],
  3421. $key,
  3422. $uid);
  3423. if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
  3424. $this->icache['message.id'][$uid] = intval($sql_arr['message_id']);
  3425. $this->uid_id_map[preg_replace('/\.msg$/', '', $key)][$uid] = intval($sql_arr['idx']);
  3426. $this->icache[$internal_key][$uid] = $this->db->decode(unserialize($sql_arr['headers']));
  3427. if (is_object($this->icache[$internal_key][$uid]) && !empty($sql_arr['structure']))
  3428. $this->icache[$internal_key][$uid]->structure = $this->db->decode(unserialize($sql_arr['structure']));
  3429. }
  3430. }
  3431. return $this->icache[$internal_key][$uid];
  3432. }
  3433. /**
  3434. * @param string $key Cache key
  3435. * @param string $sort_field Sorting column
  3436. * @param string $sort_order Sorting order
  3437. * @return array Messages index
  3438. * @access private
  3439. */
  3440. private function get_message_cache_index($key, $sort_field='idx', $sort_order='ASC')
  3441. {
  3442. if (!$this->messages_caching || empty($key))
  3443. return NULL;
  3444. // use idx sort as default
  3445. if (!$sort_field || !in_array($sort_field, $this->db_header_fields))
  3446. $sort_field = 'idx';
  3447. if (array_key_exists('index', $this->icache)
  3448. && $this->icache['index']['key'] == $key
  3449. && $this->icache['index']['sort_field'] == $sort_field
  3450. ) {
  3451. if ($this->icache['index']['sort_order'] == $sort_order)
  3452. return $this->icache['index']['result'];
  3453. else
  3454. return array_reverse($this->icache['index']['result'], true);
  3455. }
  3456. $this->icache['index'] = array(
  3457. 'result' => array(),
  3458. 'key' => $key,
  3459. 'sort_field' => $sort_field,
  3460. 'sort_order' => $sort_order,
  3461. );
  3462. $sql_result = $this->db->query(
  3463. "SELECT idx, uid".
  3464. " FROM ".get_table_name('messages').
  3465. " WHERE user_id=?".
  3466. " AND cache_key=?".
  3467. " ORDER BY ".$this->db->quote_identifier($sort_field)." ".$sort_order,
  3468. $_SESSION['user_id'],
  3469. $key);
  3470. while ($sql_arr = $this->db->fetch_assoc($sql_result))
  3471. $this->icache['index']['result'][$sql_arr['idx']] = intval($sql_arr['uid']);
  3472. return $this->icache['index']['result'];
  3473. }
  3474. /**
  3475. * @access private
  3476. */
  3477. private function add_message_cache($key, $index, $headers, $struct=null, $force=false, $internal_cache=false)
  3478. {
  3479. if (empty($key) || !is_object($headers) || empty($headers->uid))
  3480. return;
  3481. // add to internal (fast) cache
  3482. if ($internal_cache) {
  3483. $this->icache['message'][$headers->uid] = clone $headers;
  3484. $this->icache['message'][$headers->uid]->structure = $struct;
  3485. }
  3486. // no further caching
  3487. if (!$this->messages_caching)
  3488. return;
  3489. // known message id
  3490. if (is_int($force) && $force > 0) {
  3491. $message_id = $force;
  3492. }
  3493. // check for an existing record (probably headers are cached but structure not)
  3494. else if (!$force) {
  3495. $sql_result = $this->db->query(
  3496. "SELECT message_id".
  3497. " FROM ".get_table_name('messages').
  3498. " WHERE user_id=?".
  3499. " AND cache_key=?".
  3500. " AND uid=?",
  3501. $_SESSION['user_id'],
  3502. $key,
  3503. $headers->uid);
  3504. if ($sql_arr = $this->db->fetch_assoc($sql_result))
  3505. $message_id = $sql_arr['message_id'];
  3506. }
  3507. // update cache record
  3508. if ($message_id) {
  3509. $this->db->query(
  3510. "UPDATE ".get_table_name('messages').
  3511. " SET idx=?, headers=?, structure=?".
  3512. " WHERE message_id=?",
  3513. $index,
  3514. serialize($this->db->encode(clone $headers)),
  3515. is_object($struct) ? serialize($this->db->encode(clone $struct)) : NULL,
  3516. $message_id
  3517. );
  3518. }
  3519. else { // insert new record
  3520. $this->db->query(
  3521. "INSERT INTO ".get_table_name('messages').
  3522. " (user_id, del, cache_key, created, idx, uid, subject, ".
  3523. $this->db->quoteIdentifier('from').", ".
  3524. $this->db->quoteIdentifier('to').", ".
  3525. "cc, date, size, headers, structure)".
  3526. " VALUES (?, 0, ?, ".$this->db->now().", ?, ?, ?, ?, ?, ?, ".
  3527. $this->db->fromunixtime($headers->timestamp).", ?, ?, ?)",
  3528. $_SESSION['user_id'],
  3529. $key,
  3530. $index,
  3531. $headers->uid,
  3532. (string)mb_substr($this->db->encode($this->decode_header($headers->subject, true)), 0, 128),
  3533. (string)mb_substr($this->db->encode($this->decode_header($headers->from, true)), 0, 128),
  3534. (string)mb_substr($this->db->encode($this->decode_header($headers->to, true)), 0, 128),
  3535. (string)mb_substr($this->db->encode($this->decode_header($headers->cc, true)), 0, 128),
  3536. (int)$headers->size,
  3537. serialize($this->db->encode(clone $headers)),
  3538. is_object($struct) ? serialize($this->db->encode(clone $struct)) : NULL
  3539. );
  3540. }
  3541. unset($this->icache['index']);
  3542. }
  3543. /**
  3544. * @access private
  3545. */
  3546. private function remove_message_cache($key, $ids, $idx=false)
  3547. {
  3548. if (!$this->messages_caching)
  3549. return;
  3550. $this->db->query(
  3551. "DELETE FROM ".get_table_name('messages').
  3552. " WHERE user_id=?".
  3553. " AND cache_key=?".
  3554. " AND ".($idx ? "idx" : "uid")." IN (".$this->db->array2list($ids, 'integer').")",
  3555. $_SESSION['user_id'],
  3556. $key);
  3557. unset($this->icache['index']);
  3558. }
  3559. /**
  3560. * @param string $key Cache key
  3561. * @param int $start_index Start index
  3562. * @access private
  3563. */
  3564. private function clear_message_cache($key, $start_index=1)
  3565. {
  3566. if (!$this->messages_caching)
  3567. return;
  3568. $this->db->query(
  3569. "DELETE FROM ".get_table_name('messages').
  3570. " WHERE user_id=?".
  3571. " AND cache_key=?".
  3572. " AND idx>=?",
  3573. $_SESSION['user_id'], $key, $start_index);
  3574. unset($this->icache['index']);
  3575. }
  3576. /**
  3577. * @access private
  3578. */
  3579. private function get_message_cache_index_min($key, $uids=NULL)
  3580. {
  3581. if (!$this->messages_caching)
  3582. return;
  3583. if (!empty($uids) && !is_array($uids)) {
  3584. if ($uids == '*' || $uids == '1:*')
  3585. $uids = NULL;
  3586. else
  3587. $uids = explode(',', $uids);
  3588. }
  3589. $sql_result = $this->db->query(
  3590. "SELECT MIN(idx) AS minidx".
  3591. " FROM ".get_table_name('messages').
  3592. " WHERE user_id=?".
  3593. " AND cache_key=?"
  3594. .(!empty($uids) ? " AND uid IN (".$this->db->array2list($uids, 'integer').")" : ''),
  3595. $_SESSION['user_id'],
  3596. $key);
  3597. if ($sql_arr = $this->db->fetch_assoc($sql_result))
  3598. return $sql_arr['minidx'];
  3599. else
  3600. return 0;
  3601. }
  3602. /**
  3603. * @param string $key Cache key
  3604. * @param int $id Message (sequence) ID
  3605. * @return int Message UID
  3606. * @access private
  3607. */
  3608. private function get_cache_id2uid($key, $id)
  3609. {
  3610. if (!$this->messages_caching)
  3611. return null;
  3612. if (array_key_exists('index', $this->icache)
  3613. && $this->icache['index']['key'] == $key
  3614. ) {
  3615. return $this->icache['index']['result'][$id];
  3616. }
  3617. $sql_result = $this->db->query(
  3618. "SELECT uid".
  3619. " FROM ".get_table_name('messages').
  3620. " WHERE user_id=?".
  3621. " AND cache_key=?".
  3622. " AND idx=?",
  3623. $_SESSION['user_id'], $key, $id);
  3624. if ($sql_arr = $this->db->fetch_assoc($sql_result))
  3625. return intval($sql_arr['uid']);
  3626. return null;
  3627. }
  3628. /**
  3629. * @param string $key Cache key
  3630. * @param int $uid Message UID
  3631. * @return int Message (sequence) ID
  3632. * @access private
  3633. */
  3634. private function get_cache_uid2id($key, $uid)
  3635. {
  3636. if (!$this->messages_caching)
  3637. return null;
  3638. if (array_key_exists('index', $this->icache)
  3639. && $this->icache['index']['key'] == $key
  3640. ) {
  3641. return array_search($uid, $this->icache['index']['result']);
  3642. }
  3643. $sql_result = $this->db->query(
  3644. "SELECT idx".
  3645. " FROM ".get_table_name('messages').
  3646. " WHERE user_id=?".
  3647. " AND cache_key=?".
  3648. " AND uid=?",
  3649. $_SESSION['user_id'], $key, $uid);
  3650. if ($sql_arr = $this->db->fetch_assoc($sql_result))
  3651. return intval($sql_arr['idx']);
  3652. return null;
  3653. }
  3654. /* --------------------------------
  3655. * encoding/decoding methods
  3656. * --------------------------------*/
  3657. /**
  3658. * Split an address list into a structured array list
  3659. *
  3660. * @param string $input Input string
  3661. * @param int $max List only this number of addresses
  3662. * @param boolean $decode Decode address strings
  3663. * @return array Indexed list of addresses
  3664. */
  3665. function decode_address_list($input, $max=null, $decode=true)
  3666. {
  3667. $a = $this->_parse_address_list($input, $decode);
  3668. $out = array();
  3669. // Special chars as defined by RFC 822 need to in quoted string (or escaped).
  3670. $special_chars = '[\(\)\<\>\\\.\[\]@,;:"]';
  3671. if (!is_array($a))
  3672. return $out;
  3673. $c = count($a);
  3674. $j = 0;
  3675. foreach ($a as $val) {
  3676. $j++;
  3677. $address = trim($val['address']);
  3678. $name = trim($val['name']);
  3679. if ($name && $address && $name != $address)
  3680. $string = sprintf('%s <%s>', preg_match("/$special_chars/", $name) ? '"'.addcslashes($name, '"').'"' : $name, $address);
  3681. else if ($address)
  3682. $string = $address;
  3683. else if ($name)
  3684. $string = $name;
  3685. $out[$j] = array(
  3686. 'name' => $name,
  3687. 'mailto' => $address,
  3688. 'string' => $string
  3689. );
  3690. if ($max && $j==$max)
  3691. break;
  3692. }
  3693. return $out;
  3694. }
  3695. /**
  3696. * Decode a message header value
  3697. *
  3698. * @param string $input Header value
  3699. * @param boolean $remove_quotas Remove quotes if necessary
  3700. * @return string Decoded string
  3701. */
  3702. function decode_header($input, $remove_quotes=false)
  3703. {
  3704. $str = rcube_imap::decode_mime_string((string)$input, $this->default_charset);
  3705. if ($str[0] == '"' && $remove_quotes)
  3706. $str = str_replace('"', '', $str);
  3707. return $str;
  3708. }
  3709. /**
  3710. * Decode a mime-encoded string to internal charset
  3711. *
  3712. * @param string $input Header value
  3713. * @param string $fallback Fallback charset if none specified
  3714. *
  3715. * @return string Decoded string
  3716. * @static
  3717. */
  3718. public static function decode_mime_string($input, $fallback=null)
  3719. {
  3720. if (!empty($fallback)) {
  3721. $default_charset = $fallback;
  3722. }
  3723. else {
  3724. $default_charset = rcmail::get_instance()->config->get('default_charset', 'ISO-8859-1');
  3725. }
  3726. // rfc: all line breaks or other characters not found
  3727. // in the Base64 Alphabet must be ignored by decoding software
  3728. // delete all blanks between MIME-lines, differently we can
  3729. // receive unnecessary blanks and broken utf-8 symbols
  3730. $input = preg_replace("/\?=\s+=\?/", '?==?', $input);
  3731. // encoded-word regexp
  3732. $re = '/=\?([^?]+)\?([BbQq])\?([^?\n]*)\?=/';
  3733. // Find all RFC2047's encoded words
  3734. if (preg_match_all($re, $input, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) {
  3735. // Initialize variables
  3736. $tmp = array();
  3737. $out = '';
  3738. $start = 0;
  3739. foreach ($matches as $idx => $m) {
  3740. $pos = $m[0][1];
  3741. $charset = $m[1][0];
  3742. $encoding = $m[2][0];
  3743. $text = $m[3][0];
  3744. $length = strlen($m[0][0]);
  3745. // Append everything that is before the text to be decoded
  3746. if ($start != $pos) {
  3747. $substr = substr($input, $start, $pos-$start);
  3748. $out .= rcube_charset_convert($substr, $default_charset);
  3749. $start = $pos;
  3750. }
  3751. $start += $length;
  3752. // Per RFC2047, each string part "MUST represent an integral number
  3753. // of characters . A multi-octet character may not be split across
  3754. // adjacent encoded-words." However, some mailers break this, so we
  3755. // try to handle characters spanned across parts anyway by iterating
  3756. // through and aggregating sequential encoded parts with the same
  3757. // character set and encoding, then perform the decoding on the
  3758. // aggregation as a whole.
  3759. $tmp[] = $text;
  3760. if ($next_match = $matches[$idx+1]) {
  3761. if ($next_match[0][1] == $start
  3762. && $next_match[1][0] == $charset
  3763. && $next_match[2][0] == $encoding
  3764. ) {
  3765. continue;
  3766. }
  3767. }
  3768. $count = count($tmp);
  3769. $text = '';
  3770. // Decode and join encoded-word's chunks
  3771. if ($encoding == 'B' || $encoding == 'b') {
  3772. // base64 must be decoded a segment at a time
  3773. for ($i=0; $i<$count; $i++)
  3774. $text .= base64_decode($tmp[$i]);
  3775. }
  3776. else { //if ($encoding == 'Q' || $encoding == 'q') {
  3777. // quoted printable can be combined and processed at once
  3778. for ($i=0; $i<$count; $i++)
  3779. $text .= $tmp[$i];
  3780. $text = str_replace('_', ' ', $text);
  3781. $text = quoted_printable_decode($text);
  3782. }
  3783. $out .= rcube_charset_convert($text, $charset);
  3784. $tmp = array();
  3785. }
  3786. // add the last part of the input string
  3787. if ($start != strlen($input)) {
  3788. $out .= rcube_charset_convert(substr($input, $start), $default_charset);
  3789. }
  3790. // return the results
  3791. return $out;
  3792. }
  3793. // no encoding information, use fallback
  3794. return rcube_charset_convert($input, $default_charset);
  3795. }
  3796. /**
  3797. * Decode a mime part
  3798. *
  3799. * @param string $input Input string
  3800. * @param string $encoding Part encoding
  3801. * @return string Decoded string
  3802. */
  3803. function mime_decode($input, $encoding='7bit')
  3804. {
  3805. switch (strtolower($encoding)) {
  3806. case 'quoted-printable':
  3807. return quoted_printable_decode($input);
  3808. case 'base64':
  3809. return base64_decode($input);
  3810. case 'x-uuencode':
  3811. case 'x-uue':
  3812. case 'uue':
  3813. case 'uuencode':
  3814. return convert_uudecode($input);
  3815. case '7bit':
  3816. default:
  3817. return $input;
  3818. }
  3819. }
  3820. /**
  3821. * Convert body charset to RCMAIL_CHARSET according to the ctype_parameters
  3822. *
  3823. * @param string $body Part body to decode
  3824. * @param string $ctype_param Charset to convert from
  3825. * @return string Content converted to internal charset
  3826. */
  3827. function charset_decode($body, $ctype_param)
  3828. {
  3829. if (is_array($ctype_param) && !empty($ctype_param['charset']))
  3830. return rcube_charset_convert($body, $ctype_param['charset']);
  3831. // defaults to what is specified in the class header
  3832. return rcube_charset_convert($body, $this->default_charset);
  3833. }
  3834. /* --------------------------------
  3835. * private methods
  3836. * --------------------------------*/
  3837. /**
  3838. * Validate the given input and save to local properties
  3839. *
  3840. * @param string $sort_field Sort column
  3841. * @param string $sort_order Sort order
  3842. * @access private
  3843. */
  3844. private function _set_sort_order($sort_field, $sort_order)
  3845. {
  3846. if ($sort_field != null)
  3847. $this->sort_field = asciiwords($sort_field);
  3848. if ($sort_order != null)
  3849. $this->sort_order = strtoupper($sort_order) == 'DESC' ? 'DESC' : 'ASC';
  3850. }
  3851. /**
  3852. * Sort mailboxes first by default folders and then in alphabethical order
  3853. *
  3854. * @param array $a_folders Mailboxes list
  3855. * @access private
  3856. */
  3857. private function _sort_mailbox_list($a_folders)
  3858. {
  3859. $a_out = $a_defaults = $folders = array();
  3860. $delimiter = $this->get_hierarchy_delimiter();
  3861. // find default folders and skip folders starting with '.'
  3862. foreach ($a_folders as $i => $folder) {
  3863. if ($folder[0] == '.')
  3864. continue;
  3865. if (($p = array_search($folder, $this->default_folders)) !== false && !$a_defaults[$p])
  3866. $a_defaults[$p] = $folder;
  3867. else
  3868. $folders[$folder] = rcube_charset_convert($folder, 'UTF7-IMAP');
  3869. }
  3870. // sort folders and place defaults on the top
  3871. asort($folders, SORT_LOCALE_STRING);
  3872. ksort($a_defaults);
  3873. $folders = array_merge($a_defaults, array_keys($folders));
  3874. // finally we must rebuild the list to move
  3875. // subfolders of default folders to their place...
  3876. // ...also do this for the rest of folders because
  3877. // asort() is not properly sorting case sensitive names
  3878. while (list($key, $folder) = each($folders)) {
  3879. // set the type of folder name variable (#1485527)
  3880. $a_out[] = (string) $folder;
  3881. unset($folders[$key]);
  3882. $this->_rsort($folder, $delimiter, $folders, $a_out);
  3883. }
  3884. return $a_out;
  3885. }
  3886. /**
  3887. * @access private
  3888. */
  3889. private function _rsort($folder, $delimiter, &$list, &$out)
  3890. {
  3891. while (list($key, $name) = each($list)) {
  3892. if (strpos($name, $folder.$delimiter) === 0) {
  3893. // set the type of folder name variable (#1485527)
  3894. $out[] = (string) $name;
  3895. unset($list[$key]);
  3896. $this->_rsort($name, $delimiter, $list, $out);
  3897. }
  3898. }
  3899. reset($list);
  3900. }
  3901. /**
  3902. * @param int $uid Message UID
  3903. * @param string $mailbox Mailbox name
  3904. * @return int Message (sequence) ID
  3905. * @access private
  3906. */
  3907. private function _uid2id($uid, $mailbox=NULL)
  3908. {
  3909. if (!strlen($mailbox)) {
  3910. $mailbox = $this->mailbox;
  3911. }
  3912. if (!isset($this->uid_id_map[$mailbox][$uid])) {
  3913. if (!($id = $this->get_cache_uid2id($mailbox.'.msg', $uid)))
  3914. $id = $this->conn->UID2ID($mailbox, $uid);
  3915. $this->uid_id_map[$mailbox][$uid] = $id;
  3916. }
  3917. return $this->uid_id_map[$mailbox][$uid];
  3918. }
  3919. /**
  3920. * @param int $id Message (sequence) ID
  3921. * @param string $mailbox Mailbox name
  3922. *
  3923. * @return int Message UID
  3924. * @access private
  3925. */
  3926. private function _id2uid($id, $mailbox=null)
  3927. {
  3928. if (!strlen($mailbox)) {
  3929. $mailbox = $this->mailbox;
  3930. }
  3931. if ($uid = array_search($id, (array)$this->uid_id_map[$mailbox])) {
  3932. return $uid;
  3933. }
  3934. if (!($uid = $this->get_cache_id2uid($mailbox.'.msg', $id))) {
  3935. $uid = $this->conn->ID2UID($mailbox, $id);
  3936. }
  3937. $this->uid_id_map[$mailbox][$uid] = $id;
  3938. return $uid;
  3939. }
  3940. /**
  3941. * Subscribe/unsubscribe a list of mailboxes and update local cache
  3942. * @access private
  3943. */
  3944. private function _change_subscription($a_mboxes, $mode)
  3945. {
  3946. $updated = false;
  3947. if (is_array($a_mboxes))
  3948. foreach ($a_mboxes as $i => $mailbox) {
  3949. $a_mboxes[$i] = $mailbox;
  3950. if ($mode == 'subscribe')
  3951. $updated = $this->conn->subscribe($mailbox);
  3952. else if ($mode == 'unsubscribe')
  3953. $updated = $this->conn->unsubscribe($mailbox);
  3954. }
  3955. // clear cached mailbox list(s)
  3956. if ($updated) {
  3957. $this->clear_cache('mailboxes', true);
  3958. }
  3959. return $updated;
  3960. }
  3961. /**
  3962. * Increde/decrese messagecount for a specific mailbox
  3963. * @access private
  3964. */
  3965. private function _set_messagecount($mailbox, $mode, $increment)
  3966. {
  3967. $mode = strtoupper($mode);
  3968. $a_mailbox_cache = $this->get_cache('messagecount');
  3969. if (!is_array($a_mailbox_cache[$mailbox]) || !isset($a_mailbox_cache[$mailbox][$mode]) || !is_numeric($increment))
  3970. return false;
  3971. // add incremental value to messagecount
  3972. $a_mailbox_cache[$mailbox][$mode] += $increment;
  3973. // there's something wrong, delete from cache
  3974. if ($a_mailbox_cache[$mailbox][$mode] < 0)
  3975. unset($a_mailbox_cache[$mailbox][$mode]);
  3976. // write back to cache
  3977. $this->update_cache('messagecount', $a_mailbox_cache);
  3978. return true;
  3979. }
  3980. /**
  3981. * Remove messagecount of a specific mailbox from cache
  3982. * @access private
  3983. */
  3984. private function _clear_messagecount($mailbox, $mode=null)
  3985. {
  3986. $a_mailbox_cache = $this->get_cache('messagecount');
  3987. if (is_array($a_mailbox_cache[$mailbox])) {
  3988. if ($mode) {
  3989. unset($a_mailbox_cache[$mailbox][$mode]);
  3990. }
  3991. else {
  3992. unset($a_mailbox_cache[$mailbox]);
  3993. }
  3994. $this->update_cache('messagecount', $a_mailbox_cache);
  3995. }
  3996. }
  3997. /**
  3998. * Split RFC822 header string into an associative array
  3999. * @access private
  4000. */
  4001. private function _parse_headers($headers)
  4002. {
  4003. $a_headers = array();
  4004. $headers = preg_replace('/\r?\n(\t| )+/', ' ', $headers);
  4005. $lines = explode("\n", $headers);
  4006. $c = count($lines);
  4007. for ($i=0; $i<$c; $i++) {
  4008. if ($p = strpos($lines[$i], ': ')) {
  4009. $field = strtolower(substr($lines[$i], 0, $p));
  4010. $value = trim(substr($lines[$i], $p+1));
  4011. if (!empty($value))
  4012. $a_headers[$field] = $value;
  4013. }
  4014. }
  4015. return $a_headers;
  4016. }
  4017. /**
  4018. * @access private
  4019. */
  4020. private function _parse_address_list($str, $decode=true)
  4021. {
  4022. // remove any newlines and carriage returns before
  4023. $str = preg_replace('/\r?\n(\s|\t)?/', ' ', $str);
  4024. // extract list items, remove comments
  4025. $str = self::explode_header_string(',;', $str, true);
  4026. $result = array();
  4027. foreach ($str as $key => $val) {
  4028. $name = '';
  4029. $address = '';
  4030. $val = trim($val);
  4031. if (preg_match('/(.*)<(\S+@\S+)>$/', $val, $m)) {
  4032. $address = $m[2];
  4033. $name = trim($m[1]);
  4034. }
  4035. else if (preg_match('/^(\S+@\S+)$/', $val, $m)) {
  4036. $address = $m[1];
  4037. $name = '';
  4038. }
  4039. else {
  4040. $name = $val;
  4041. }
  4042. // dequote and/or decode name
  4043. if ($name) {
  4044. if ($name[0] == '"') {
  4045. $name = substr($name, 1, -1);
  4046. $name = stripslashes($name);
  4047. }
  4048. if ($decode) {
  4049. $name = $this->decode_header($name);
  4050. }
  4051. }
  4052. if (!$address && $name) {
  4053. $address = $name;
  4054. }
  4055. if ($address) {
  4056. $result[$key] = array('name' => $name, 'address' => $address);
  4057. }
  4058. }
  4059. return $result;
  4060. }
  4061. /**
  4062. * Explodes header (e.g. address-list) string into array of strings
  4063. * using specified separator characters with proper handling
  4064. * of quoted-strings and comments (RFC2822)
  4065. *
  4066. * @param string $separator String containing separator characters
  4067. * @param string $str Header string
  4068. * @param bool $remove_comments Enable to remove comments
  4069. *
  4070. * @return array Header items
  4071. */
  4072. static function explode_header_string($separator, $str, $remove_comments=false)
  4073. {
  4074. $length = strlen($str);
  4075. $result = array();
  4076. $quoted = false;
  4077. $comment = 0;
  4078. $out = '';
  4079. for ($i=0; $i<$length; $i++) {
  4080. // we're inside a quoted string
  4081. if ($quoted) {
  4082. if ($str[$i] == '"') {
  4083. $quoted = false;
  4084. }
  4085. else if ($str[$i] == '\\') {
  4086. if ($comment <= 0) {
  4087. $out .= '\\';
  4088. }
  4089. $i++;
  4090. }
  4091. }
  4092. // we're inside a comment string
  4093. else if ($comment > 0) {
  4094. if ($str[$i] == ')') {
  4095. $comment--;
  4096. }
  4097. else if ($str[$i] == '(') {
  4098. $comment++;
  4099. }
  4100. else if ($str[$i] == '\\') {
  4101. $i++;
  4102. }
  4103. continue;
  4104. }
  4105. // separator, add to result array
  4106. else if (strpos($separator, $str[$i]) !== false) {
  4107. if ($out) {
  4108. $result[] = $out;
  4109. }
  4110. $out = '';
  4111. continue;
  4112. }
  4113. // start of quoted string
  4114. else if ($str[$i] == '"') {
  4115. $quoted = true;
  4116. }
  4117. // start of comment
  4118. else if ($remove_comments && $str[$i] == '(') {
  4119. $comment++;
  4120. }
  4121. if ($comment <= 0) {
  4122. $out .= $str[$i];
  4123. }
  4124. }
  4125. if ($out && $comment <= 0) {
  4126. $result[] = $out;
  4127. }
  4128. return $result;
  4129. }
  4130. /**
  4131. * This is our own debug handler for the IMAP connection
  4132. * @access public
  4133. */
  4134. public function debug_handler(&$imap, $message)
  4135. {
  4136. write_log('imap', $message);
  4137. }
  4138. } // end class rcube_imap
  4139. /**
  4140. * Class representing a message part
  4141. *
  4142. * @package Mail
  4143. */
  4144. class rcube_message_part
  4145. {
  4146. var $mime_id = '';
  4147. var $ctype_primary = 'text';
  4148. var $ctype_secondary = 'plain';
  4149. var $mimetype = 'text/plain';
  4150. var $disposition = '';
  4151. var $filename = '';
  4152. var $encoding = '8bit';
  4153. var $charset = '';
  4154. var $size = 0;
  4155. var $headers = array();
  4156. var $d_parameters = array();
  4157. var $ctype_parameters = array();
  4158. function __clone()
  4159. {
  4160. if (isset($this->parts))
  4161. foreach ($this->parts as $idx => $part)
  4162. if (is_object($part))
  4163. $this->parts[$idx] = clone $part;
  4164. }
  4165. }
  4166. /**
  4167. * Class for sorting an array of rcube_mail_header objects in a predetermined order.
  4168. *
  4169. * @package Mail
  4170. * @author Eric Stadtherr
  4171. */
  4172. class rcube_header_sorter
  4173. {
  4174. var $sequence_numbers = array();
  4175. /**
  4176. * Set the predetermined sort order.
  4177. *
  4178. * @param array $seqnums Numerically indexed array of IMAP message sequence numbers
  4179. */
  4180. function set_sequence_numbers($seqnums)
  4181. {
  4182. $this->sequence_numbers = array_flip($seqnums);
  4183. }
  4184. /**
  4185. * Sort the array of header objects
  4186. *
  4187. * @param array $headers Array of rcube_mail_header objects indexed by UID
  4188. */
  4189. function sort_headers(&$headers)
  4190. {
  4191. /*
  4192. * uksort would work if the keys were the sequence number, but unfortunately
  4193. * the keys are the UIDs. We'll use uasort instead and dereference the value
  4194. * to get the sequence number (in the "id" field).
  4195. *
  4196. * uksort($headers, array($this, "compare_seqnums"));
  4197. */
  4198. uasort($headers, array($this, "compare_seqnums"));
  4199. }
  4200. /**
  4201. * Sort method called by uasort()
  4202. *
  4203. * @param rcube_mail_header $a
  4204. * @param rcube_mail_header $b
  4205. */
  4206. function compare_seqnums($a, $b)
  4207. {
  4208. // First get the sequence number from the header object (the 'id' field).
  4209. $seqa = $a->id;
  4210. $seqb = $b->id;
  4211. // then find each sequence number in my ordered list
  4212. $posa = isset($this->sequence_numbers[$seqa]) ? intval($this->sequence_numbers[$seqa]) : -1;
  4213. $posb = isset($this->sequence_numbers[$seqb]) ? intval($this->sequence_numbers[$seqb]) : -1;
  4214. // return the relative position as the comparison value
  4215. return $posa - $posb;
  4216. }
  4217. }