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

/program/include/crystal_imap.php

https://github.com/crystalmail/Crystal-Mail
PHP | 3834 lines | 2147 code | 627 blank | 1060 comment | 635 complexity | 26e1820e60feb938b38e495d51ff5ca5 MD5 | raw file
Possible License(s): AGPL-1.0

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

  1. <?php
  2. /*
  3. +-----------------------------------------------------------------------+
  4. | program/include/crystal_imap.php |
  5. | |
  6. | This file is part of the RoundCube Webmail client |
  7. | Copyright (C) 2005-2010, RoundCube Dev. - Switzerland |
  8. | Licensed under the GNU GPL |
  9. | |
  10. | PURPOSE: |
  11. | IMAP Engine |
  12. | |
  13. +-----------------------------------------------------------------------+
  14. | Author: Thomas Bruederli <crystalmail@gmail.com> |
  15. | Author: Aleksander Machniak <alec@alec.pl> |
  16. +-----------------------------------------------------------------------+
  17. $Id: crystal_imap.php 3494 2010-04-15 12:21:03Z alec $
  18. */
  19. /**
  20. * Interface class for accessing an IMAP server
  21. *
  22. * @package Mail
  23. * @author Thomas Bruederli <crystalmail@gmail.com>
  24. * @version 1.6
  25. */
  26. class crystal_imap
  27. {
  28. public $debug_level = 1;
  29. public $error_code = 0;
  30. public $skip_deleted = false;
  31. public $root_dir = '';
  32. public $page_size = 10;
  33. public $list_page = 1;
  34. public $delimiter = NULL;
  35. public $threading = false;
  36. public $fetch_add_headers = '';
  37. public $conn; // crystal_imap_generic object
  38. private $db;
  39. private $root_ns = '';
  40. private $mailbox = 'INBOX';
  41. private $sort_field = '';
  42. private $sort_order = 'DESC';
  43. private $caching_enabled = false;
  44. private $default_charset = 'ISO-8859-1';
  45. private $struct_charset = NULL;
  46. private $default_folders = array('INBOX');
  47. private $icache = array();
  48. private $cache = array();
  49. private $cache_keys = array();
  50. private $cache_changes = array();
  51. private $uid_id_map = array();
  52. private $msg_headers = array();
  53. public $search_set = NULL;
  54. public $search_string = '';
  55. private $search_charset = '';
  56. private $search_sort_field = '';
  57. private $search_threads = false;
  58. private $db_header_fields = array('idx', 'uid', 'subject', 'from', 'to', 'cc', 'date', 'size');
  59. private $options = array('auth_method' => 'check');
  60. private $host, $user, $pass, $port, $ssl;
  61. /**
  62. * Object constructor
  63. *
  64. * @param object DB Database connection
  65. */
  66. function __construct($db_conn)
  67. {
  68. $this->db = $db_conn;
  69. $this->conn = new crystal_imap_generic();
  70. }
  71. /**
  72. * Connect to an IMAP server
  73. *
  74. * @param string Host to connect
  75. * @param string Username for IMAP account
  76. * @param string Password for IMAP account
  77. * @param number Port to connect to
  78. * @param string SSL schema (either ssl or tls) or null if plain connection
  79. * @return boolean TRUE on success, FALSE on failure
  80. * @access public
  81. */
  82. function connect($host, $user, $pass, $port=143, $use_ssl=null)
  83. {
  84. // check for Open-SSL support in PHP build
  85. if ($use_ssl && extension_loaded('openssl'))
  86. $this->options['ssl_mode'] = $use_ssl == 'imaps' ? 'ssl' : $use_ssl;
  87. else if ($use_ssl) {
  88. raise_error(array('code' => 403, 'type' => 'imap',
  89. 'file' => __FILE__, 'line' => __LINE__,
  90. 'message' => "OpenSSL not available"), true, false);
  91. $port = 143;
  92. }
  93. $this->options['port'] = $port;
  94. $attempt = 0;
  95. do {
  96. $data = cmail::get_instance()->plugins->exec_hook('imap_connect',
  97. array('host' => $host, 'user' => $user, 'attempt' => ++$attempt));
  98. if (!empty($data['pass']))
  99. $pass = $data['pass'];
  100. $this->conn->connect($data['host'], $data['user'], $pass, $this->options);
  101. } while(!$this->conn->connected() && $data['retry']);
  102. $this->host = $data['host'];
  103. $this->user = $data['user'];
  104. $this->pass = $pass;
  105. $this->port = $port;
  106. $this->ssl = $use_ssl;
  107. // print trace messages
  108. if ($this->conn->connected()) {
  109. if ($this->conn->message && ($this->debug_level & 8)) {
  110. console($this->conn->message);
  111. }
  112. // get server properties
  113. if (!empty($this->conn->rootdir))
  114. $this->set_rootdir($this->conn->rootdir);
  115. if (empty($this->delimiter))
  116. $this->get_hierarchy_delimiter();
  117. return true;
  118. }
  119. // write error log
  120. else if ($this->conn->error) {
  121. $this->error_code = $this->conn->errornum;
  122. raise_error(array('code' => 403, 'type' => 'imap',
  123. 'file' => __FILE__, 'line' => __LINE__,
  124. 'message' => $this->conn->error), true, false);
  125. }
  126. return false;
  127. }
  128. /**
  129. * Close IMAP connection
  130. * Usually done on script shutdown
  131. *
  132. * @access public
  133. */
  134. function close()
  135. {
  136. if ($this->conn && $this->conn->connected())
  137. $this->conn->close();
  138. $this->write_cache();
  139. }
  140. /**
  141. * Close IMAP connection and re-connect
  142. * This is used to avoid some strange socket errors when talking to Courier IMAP
  143. *
  144. * @access public
  145. */
  146. function reconnect()
  147. {
  148. $this->close();
  149. $this->connect($this->host, $this->user, $this->pass, $this->port, $this->ssl);
  150. // issue SELECT command to restore connection status
  151. if ($this->mailbox)
  152. $this->conn->select($this->mailbox);
  153. }
  154. /**
  155. * Set options to be used in crystal_imap_generic::connect()
  156. */
  157. function set_options($opt)
  158. {
  159. $this->options = array_merge($this->options, (array)$opt);
  160. }
  161. /**
  162. * Set a root folder for the IMAP connection.
  163. *
  164. * Only folders within this root folder will be displayed
  165. * and all folder paths will be translated using this folder name
  166. *
  167. * @param string Root folder
  168. * @access public
  169. */
  170. function set_rootdir($root)
  171. {
  172. if (preg_match('/[.\/]$/', $root)) //(substr($root, -1, 1)==='/')
  173. $root = substr($root, 0, -1);
  174. $this->root_dir = $root;
  175. $this->options['rootdir'] = $root;
  176. if (empty($this->delimiter))
  177. $this->get_hierarchy_delimiter();
  178. }
  179. /**
  180. * Set default message charset
  181. *
  182. * This will be used for message decoding if a charset specification is not available
  183. *
  184. * @param string Charset string
  185. * @access public
  186. */
  187. function set_charset($cs)
  188. {
  189. $this->default_charset = $cs;
  190. }
  191. /**
  192. * This list of folders will be listed above all other folders
  193. *
  194. * @param array Indexed list of folder names
  195. * @access public
  196. */
  197. function set_default_mailboxes($arr)
  198. {
  199. if (is_array($arr)) {
  200. $this->default_folders = $arr;
  201. // add inbox if not included
  202. if (!in_array('INBOX', $this->default_folders))
  203. array_unshift($this->default_folders, 'INBOX');
  204. }
  205. }
  206. /**
  207. * Set internal mailbox reference.
  208. *
  209. * All operations will be perfomed on this mailbox/folder
  210. *
  211. * @param string Mailbox/Folder name
  212. * @access public
  213. */
  214. function set_mailbox($new_mbox)
  215. {
  216. $mailbox = $this->mod_mailbox($new_mbox);
  217. if ($this->mailbox == $mailbox)
  218. return;
  219. $this->mailbox = $mailbox;
  220. // clear messagecount cache for this mailbox
  221. $this->_clear_messagecount($mailbox);
  222. }
  223. /**
  224. * Set internal list page
  225. *
  226. * @param number Page number to list
  227. * @access public
  228. */
  229. function set_page($page)
  230. {
  231. $this->list_page = (int)$page;
  232. }
  233. /**
  234. * Set internal page size
  235. *
  236. * @param number Number of messages to display on one page
  237. * @access public
  238. */
  239. function set_pagesize($size)
  240. {
  241. $this->page_size = (int)$size;
  242. }
  243. /**
  244. * Save a set of message ids for future message listing methods
  245. *
  246. * @param string IMAP Search query
  247. * @param array List of message ids or NULL if empty
  248. * @param string Charset of search string
  249. * @param string Sorting field
  250. */
  251. function set_search_set($str=null, $msgs=null, $charset=null, $sort_field=null, $threads=false)
  252. {
  253. if (is_array($str) && $msgs == null)
  254. list($str, $msgs, $charset, $sort_field, $threads) = $str;
  255. if ($msgs != null && !is_array($msgs))
  256. $msgs = explode(',', $msgs);
  257. $this->search_string = $str;
  258. $this->search_set = $msgs;
  259. $this->search_charset = $charset;
  260. $this->search_sort_field = $sort_field;
  261. $this->search_threads = $threads;
  262. }
  263. /**
  264. * Return the saved search set as hash array
  265. * @return array Search set
  266. */
  267. function get_search_set()
  268. {
  269. return array($this->search_string,
  270. $this->search_set,
  271. $this->search_charset,
  272. $this->search_sort_field,
  273. $this->search_threads,
  274. );
  275. }
  276. /**
  277. * Returns the currently used mailbox name
  278. *
  279. * @return string Name of the mailbox/folder
  280. * @access public
  281. */
  282. function get_mailbox_name()
  283. {
  284. return $this->conn->connected() ? $this->mod_mailbox($this->mailbox, 'out') : '';
  285. }
  286. /**
  287. * Returns the IMAP server's capability
  288. *
  289. * @param string Capability name
  290. * @return mixed Capability value or TRUE if supported, FALSE if not
  291. * @access public
  292. */
  293. function get_capability($cap)
  294. {
  295. return $this->conn->getCapability(strtoupper($cap));
  296. }
  297. /**
  298. * Sets threading flag to the best supported THREAD algorithm
  299. *
  300. * @param boolean TRUE to enable and FALSE
  301. * @return string Algorithm or false if THREAD is not supported
  302. * @access public
  303. */
  304. function set_threading($enable=false)
  305. {
  306. $this->threading = false;
  307. if ($enable) {
  308. if ($this->get_capability('THREAD=REFS'))
  309. $this->threading = 'REFS';
  310. else if ($this->get_capability('THREAD=REFERENCES'))
  311. $this->threading = 'REFERENCES';
  312. else if ($this->get_capability('THREAD=ORDEREDSUBJECT'))
  313. $this->threading = 'ORDEREDSUBJECT';
  314. }
  315. return $this->threading;
  316. }
  317. /**
  318. * Checks the PERMANENTFLAGS capability of the current mailbox
  319. * and returns true if the given flag is supported by the IMAP server
  320. *
  321. * @param string Permanentflag name
  322. * @return mixed True if this flag is supported
  323. * @access public
  324. */
  325. function check_permflag($flag)
  326. {
  327. $flag = strtoupper($flag);
  328. $imap_flag = $this->conn->flags[$flag];
  329. return (in_array_nocase($imap_flag, $this->conn->permanentflags));
  330. }
  331. /**
  332. * Returns the delimiter that is used by the IMAP server for folder separation
  333. *
  334. * @return string Delimiter string
  335. * @access public
  336. */
  337. function get_hierarchy_delimiter()
  338. {
  339. if ($this->conn && empty($this->delimiter))
  340. $this->delimiter = $this->conn->getHierarchyDelimiter();
  341. if (empty($this->delimiter))
  342. $this->delimiter = '/';
  343. return $this->delimiter;
  344. }
  345. /**
  346. * Get message count for a specific mailbox
  347. *
  348. * @param string Mailbox/folder name
  349. * @param string Mode for count [ALL|THREADS|UNSEEN|RECENT]
  350. * @param boolean Force reading from server and update cache
  351. * @return int Number of messages
  352. * @access public
  353. */
  354. function messagecount($mbox_name='', $mode='ALL', $force=false)
  355. {
  356. $mailbox = $mbox_name ? $this->mod_mailbox($mbox_name) : $this->mailbox;
  357. return $this->_messagecount($mailbox, $mode, $force);
  358. }
  359. /**
  360. * Private method for getting nr of messages
  361. *
  362. * @access private
  363. * @see crystal_imap::messagecount()
  364. */
  365. private function _messagecount($mailbox='', $mode='ALL', $force=false)
  366. {
  367. $mode = strtoupper($mode);
  368. if (empty($mailbox))
  369. $mailbox = $this->mailbox;
  370. // count search set
  371. if ($this->search_string && $mailbox == $this->mailbox && ($mode == 'ALL' || $mode == 'THREADS') && !$force) {
  372. if ($this->search_threads)
  373. return $mode == 'ALL' ? count((array)$this->search_set['depth']) : count((array)$this->search_set['tree']);
  374. else
  375. return count((array)$this->search_set);
  376. }
  377. $a_mailbox_cache = $this->get_cache('messagecount');
  378. // return cached value
  379. if (!$force && is_array($a_mailbox_cache[$mailbox]) && isset($a_mailbox_cache[$mailbox][$mode]))
  380. return $a_mailbox_cache[$mailbox][$mode];
  381. if (!is_array($a_mailbox_cache[$mailbox]))
  382. $a_mailbox_cache[$mailbox] = array();
  383. if ($mode == 'THREADS') {
  384. $count = $this->_threadcount($mailbox, $msg_count);
  385. $_SESSION['maxuid'][$mailbox] = $msg_count ? $this->_id2uid($msg_count) : 0;
  386. }
  387. // RECENT count is fetched a bit different
  388. else if ($mode == 'RECENT') {
  389. $count = $this->conn->checkForRecent($mailbox);
  390. }
  391. // use SEARCH for message counting
  392. else if ($this->skip_deleted) {
  393. $search_str = "ALL UNDELETED";
  394. // get message count and store in cache
  395. if ($mode == 'UNSEEN')
  396. $search_str .= " UNSEEN";
  397. // get message count using SEARCH
  398. // not very performant but more precise (using UNDELETED)
  399. // disable THREADS for this request
  400. $threads = $this->threading;
  401. $this->threading = false;
  402. $index = $this->_search_index($mailbox, $search_str);
  403. $this->threading = $threads;
  404. $count = is_array($index) ? count($index) : 0;
  405. if ($mode == 'ALL')
  406. $_SESSION['maxuid'][$mailbox] = $index ? $this->_id2uid(max($index)) : 0;
  407. }
  408. else {
  409. if ($mode == 'UNSEEN')
  410. $count = $this->conn->countUnseen($mailbox);
  411. else {
  412. $count = $this->conn->countMessages($mailbox);
  413. $_SESSION['maxuid'][$mailbox] = $count ? $this->_id2uid($count) : 0;
  414. }
  415. }
  416. $a_mailbox_cache[$mailbox][$mode] = (int)$count;
  417. // write back to cache
  418. $this->update_cache('messagecount', $a_mailbox_cache);
  419. return (int)$count;
  420. }
  421. /**
  422. * Private method for getting nr of threads
  423. *
  424. * @access private
  425. * @see crystal_imap::messagecount()
  426. */
  427. private function _threadcount($mailbox, &$msg_count)
  428. {
  429. if (!empty($this->icache['threads']))
  430. return count($this->icache['threads']['tree']);
  431. list ($thread_tree, $msg_depth, $has_children) = $this->_fetch_threads($mailbox);
  432. $msg_count = count($msg_depth);
  433. // $this->update_thread_cache($mailbox, $thread_tree, $msg_depth, $has_children);
  434. return count($thread_tree);
  435. }
  436. /**
  437. * Public method for listing headers
  438. * convert mailbox name with root dir first
  439. *
  440. * @param string Mailbox/folder name
  441. * @param int Current page to list
  442. * @param string Header field to sort by
  443. * @param string Sort order [ASC|DESC]
  444. * @param boolean Number of slice items to extract from result array
  445. * @return array Indexed array with message header objects
  446. * @access public
  447. */
  448. function list_headers($mbox_name='', $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
  449. {
  450. $mailbox = $mbox_name ? $this->mod_mailbox($mbox_name) : $this->mailbox;
  451. return $this->_list_headers($mailbox, $page, $sort_field, $sort_order, false, $slice);
  452. }
  453. /**
  454. * Private method for listing message headers
  455. *
  456. * @access private
  457. * @see crystal_imap::list_headers
  458. */
  459. private function _list_headers($mailbox='', $page=NULL, $sort_field=NULL, $sort_order=NULL, $recursive=false, $slice=0)
  460. {
  461. if (!strlen($mailbox))
  462. return array();
  463. // use saved message set
  464. if ($this->search_string && $mailbox == $this->mailbox)
  465. return $this->_list_header_set($mailbox, $page, $sort_field, $sort_order, $slice);
  466. if ($this->threading)
  467. return $this->_list_thread_headers($mailbox, $page, $sort_field, $sort_order, $recursive, $slice);
  468. $this->_set_sort_order($sort_field, $sort_order);
  469. $page = $page ? $page : $this->list_page;
  470. $cache_key = $mailbox.'.msg';
  471. $cache_status = $this->check_cache_status($mailbox, $cache_key);
  472. // cache is OK, we can get all messages from local cache
  473. if ($cache_status>0) {
  474. $start_msg = ($page-1) * $this->page_size;
  475. $a_msg_headers = $this->get_message_cache($cache_key, $start_msg,
  476. $start_msg+$this->page_size, $this->sort_field, $this->sort_order);
  477. $result = array_values($a_msg_headers);
  478. if ($slice)
  479. $result = array_slice($result, -$slice, $slice);
  480. return $result;
  481. }
  482. // cache is dirty, sync it
  483. else if ($this->caching_enabled && $cache_status==-1 && !$recursive) {
  484. $this->sync_header_index($mailbox);
  485. return $this->_list_headers($mailbox, $page, $this->sort_field, $this->sort_order, true, $slice);
  486. }
  487. // retrieve headers from IMAP
  488. $a_msg_headers = array();
  489. // use message index sort as default sorting (for better performance)
  490. if (!$this->sort_field) {
  491. if ($this->skip_deleted) {
  492. // @TODO: this could be cached
  493. if ($msg_index = $this->_search_index($mailbox, 'ALL UNDELETED')) {
  494. $max = max($msg_index);
  495. list($begin, $end) = $this->_get_message_range(count($msg_index), $page);
  496. $msg_index = array_slice($msg_index, $begin, $end-$begin);
  497. }
  498. }
  499. else if ($max = $this->conn->countMessages($mailbox)) {
  500. list($begin, $end) = $this->_get_message_range($max, $page);
  501. $msg_index = range($begin+1, $end);
  502. }
  503. else
  504. $msg_index = array();
  505. if ($slice)
  506. $msg_index = array_slice($msg_index, ($this->sort_order == 'DESC' ? 0 : -$slice), $slice);
  507. // fetch reqested headers from server
  508. if ($msg_index)
  509. $this->_fetch_headers($mailbox, join(",", $msg_index), $a_msg_headers, $cache_key);
  510. }
  511. // use SORT command
  512. else if ($this->get_capability('SORT')) {
  513. if ($msg_index = $this->conn->sort($mailbox, $this->sort_field, $this->skip_deleted ? 'UNDELETED' : '')) {
  514. list($begin, $end) = $this->_get_message_range(count($msg_index), $page);
  515. $max = max($msg_index);
  516. $msg_index = array_slice($msg_index, $begin, $end-$begin);
  517. if ($slice)
  518. $msg_index = array_slice($msg_index, ($this->sort_order == 'DESC' ? 0 : -$slice), $slice);
  519. // fetch reqested headers from server
  520. $this->_fetch_headers($mailbox, join(',', $msg_index), $a_msg_headers, $cache_key);
  521. }
  522. }
  523. // fetch specified header for all messages and sort
  524. else if ($a_index = $this->conn->fetchHeaderIndex($mailbox, "1:*", $this->sort_field, $this->skip_deleted)) {
  525. asort($a_index); // ASC
  526. $msg_index = array_keys($a_index);
  527. $max = max($msg_index);
  528. list($begin, $end) = $this->_get_message_range(count($msg_index), $page);
  529. $msg_index = array_slice($msg_index, $begin, $end-$begin);
  530. if ($slice)
  531. $msg_index = array_slice($msg_index, ($this->sort_order == 'DESC' ? 0 : -$slice), $slice);
  532. // fetch reqested headers from server
  533. $this->_fetch_headers($mailbox, join(",", $msg_index), $a_msg_headers, $cache_key);
  534. }
  535. // delete cached messages with a higher index than $max+1
  536. // Changed $max to $max+1 to fix this bug : #1484295
  537. $this->clear_message_cache($cache_key, $max + 1);
  538. // kick child process to sync cache
  539. // ...
  540. // return empty array if no messages found
  541. if (!is_array($a_msg_headers) || empty($a_msg_headers))
  542. return array();
  543. // use this class for message sorting
  544. $sorter = new crystal_header_sorter();
  545. $sorter->set_sequence_numbers($msg_index);
  546. $sorter->sort_headers($a_msg_headers);
  547. if ($this->sort_order == 'DESC')
  548. $a_msg_headers = array_reverse($a_msg_headers);
  549. return array_values($a_msg_headers);
  550. }
  551. /**
  552. * Private method for listing message headers using threads
  553. *
  554. * @access private
  555. * @see crystal_imap::list_headers
  556. */
  557. private function _list_thread_headers($mailbox, $page=NULL, $sort_field=NULL, $sort_order=NULL, $recursive=false, $slice=0)
  558. {
  559. $this->_set_sort_order($sort_field, $sort_order);
  560. $page = $page ? $page : $this->list_page;
  561. // $cache_key = $mailbox.'.msg';
  562. // $cache_status = $this->check_cache_status($mailbox, $cache_key);
  563. // get all threads (default sort order)
  564. list ($thread_tree, $msg_depth, $has_children) = $this->_fetch_threads($mailbox);
  565. if (empty($thread_tree))
  566. return array();
  567. $msg_index = $this->_sort_threads($mailbox, $thread_tree);
  568. return $this->_fetch_thread_headers($mailbox,
  569. $thread_tree, $msg_depth, $has_children, $msg_index, $page, $slice);
  570. }
  571. /**
  572. * Private method for fetching threads data
  573. *
  574. * @param string Mailbox/folder name
  575. * @return array Array with thread data
  576. * @access private
  577. */
  578. private function _fetch_threads($mailbox)
  579. {
  580. if (empty($this->icache['threads'])) {
  581. // get all threads
  582. list ($thread_tree, $msg_depth, $has_children) = $this->conn->thread(
  583. $mailbox, $this->threading, $this->skip_deleted ? 'UNDELETED' : '');
  584. // add to internal (fast) cache
  585. $this->icache['threads'] = array();
  586. $this->icache['threads']['tree'] = $thread_tree;
  587. $this->icache['threads']['depth'] = $msg_depth;
  588. $this->icache['threads']['has_children'] = $has_children;
  589. }
  590. return array(
  591. $this->icache['threads']['tree'],
  592. $this->icache['threads']['depth'],
  593. $this->icache['threads']['has_children'],
  594. );
  595. }
  596. /**
  597. * Private method for fetching threaded messages headers
  598. *
  599. * @access private
  600. */
  601. private function _fetch_thread_headers($mailbox, $thread_tree, $msg_depth, $has_children, $msg_index, $page, $slice=0)
  602. {
  603. $cache_key = $mailbox.'.msg';
  604. // now get IDs for current page
  605. list($begin, $end) = $this->_get_message_range(count($msg_index), $page);
  606. $msg_index = array_slice($msg_index, $begin, $end-$begin);
  607. if ($slice)
  608. $msg_index = array_slice($msg_index, ($this->sort_order == 'DESC' ? 0 : -$slice), $slice);
  609. if ($this->sort_order == 'DESC')
  610. $msg_index = array_reverse($msg_index);
  611. // flatten threads array
  612. // @TODO: fetch children only in expanded mode (?)
  613. $all_ids = array();
  614. foreach($msg_index as $root) {
  615. $all_ids[] = $root;
  616. if (!empty($thread_tree[$root]))
  617. $all_ids = array_merge($all_ids, array_keys_recursive($thread_tree[$root]));
  618. }
  619. // fetch reqested headers from server
  620. $this->_fetch_headers($mailbox, $all_ids, $a_msg_headers, $cache_key);
  621. // return empty array if no messages found
  622. if (!is_array($a_msg_headers) || empty($a_msg_headers))
  623. return array();
  624. // use this class for message sorting
  625. $sorter = new crystal_header_sorter();
  626. $sorter->set_sequence_numbers($all_ids);
  627. $sorter->sort_headers($a_msg_headers);
  628. // Set depth, has_children and unread_children fields in headers
  629. $this->_set_thread_flags($a_msg_headers, $msg_depth, $has_children);
  630. return array_values($a_msg_headers);
  631. }
  632. /**
  633. * Private method for setting threaded messages flags:
  634. * depth, has_children and unread_children
  635. *
  636. * @param array Reference to headers array indexed by message ID
  637. * @param array Array of messages depth indexed by message ID
  638. * @param array Array of messages children flags indexed by message ID
  639. * @return array Message headers array indexed by message ID
  640. * @access private
  641. */
  642. private function _set_thread_flags(&$headers, $msg_depth, $msg_children)
  643. {
  644. $parents = array();
  645. foreach ($headers as $idx => $header) {
  646. $id = $header->id;
  647. $depth = $msg_depth[$id];
  648. $parents = array_slice($parents, 0, $depth);
  649. if (!empty($parents)) {
  650. $headers[$idx]->parent_uid = end($parents);
  651. if (!$header->seen)
  652. $headers[$parents[0]]->unread_children++;
  653. }
  654. array_push($parents, $header->uid);
  655. $headers[$idx]->depth = $depth;
  656. $headers[$idx]->has_children = $msg_children[$id];
  657. }
  658. }
  659. /**
  660. * Private method for listing a set of message headers (search results)
  661. *
  662. * @param string Mailbox/folder name
  663. * @param int Current page to list
  664. * @param string Header field to sort by
  665. * @param string Sort order [ASC|DESC]
  666. * @param boolean Number of slice items to extract from result array
  667. * @return array Indexed array with message header objects
  668. * @access private
  669. * @see crystal_imap::list_header_set()
  670. */
  671. private function _list_header_set($mailbox, $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
  672. {
  673. if (!strlen($mailbox) || empty($this->search_set))
  674. return array();
  675. // use saved messages from searching
  676. if ($this->threading)
  677. return $this->_list_thread_header_set($mailbox, $page, $sort_field, $sort_order, $slice);
  678. // search set is threaded, we need a new one
  679. if ($this->search_threads)
  680. $this->search('', $this->search_string, $this->search_charset, $sort_field);
  681. $msgs = $this->search_set;
  682. $a_msg_headers = array();
  683. $page = $page ? $page : $this->list_page;
  684. $start_msg = ($page-1) * $this->page_size;
  685. $this->_set_sort_order($sort_field, $sort_order);
  686. // quickest method (default sorting)
  687. if (!$this->search_sort_field && !$this->sort_field) {
  688. if ($sort_order == 'DESC')
  689. $msgs = array_reverse($msgs);
  690. // get messages uids for one page
  691. $msgs = array_slice(array_values($msgs), $start_msg, min(count($msgs)-$start_msg, $this->page_size));
  692. if ($slice)
  693. $msgs = array_slice($msgs, -$slice, $slice);
  694. // fetch headers
  695. $this->_fetch_headers($mailbox, join(',',$msgs), $a_msg_headers, NULL);
  696. // I didn't found in RFC that FETCH always returns messages sorted by index
  697. $sorter = new crystal_header_sorter();
  698. $sorter->set_sequence_numbers($msgs);
  699. $sorter->sort_headers($a_msg_headers);
  700. return array_values($a_msg_headers);
  701. }
  702. // sorted messages, so we can first slice array and then fetch only wanted headers
  703. if ($this->get_capability('SORT')) { // SORT searching result
  704. // reset search set if sorting field has been changed
  705. if ($this->sort_field && $this->search_sort_field != $this->sort_field)
  706. $msgs = $this->search('', $this->search_string, $this->search_charset, $this->sort_field);
  707. // return empty array if no messages found
  708. if (empty($msgs))
  709. return array();
  710. if ($sort_order == 'DESC')
  711. $msgs = array_reverse($msgs);
  712. // get messages uids for one page
  713. $msgs = array_slice(array_values($msgs), $start_msg, min(count($msgs)-$start_msg, $this->page_size));
  714. if ($slice)
  715. $msgs = array_slice($msgs, -$slice, $slice);
  716. // fetch headers
  717. $this->_fetch_headers($mailbox, join(',',$msgs), $a_msg_headers, NULL);
  718. $sorter = new crystal_header_sorter();
  719. $sorter->set_sequence_numbers($msgs);
  720. $sorter->sort_headers($a_msg_headers);
  721. return array_values($a_msg_headers);
  722. }
  723. else { // SEARCH result, need sorting
  724. $cnt = count($msgs);
  725. // 300: experimantal value for best result
  726. if (($cnt > 300 && $cnt > $this->page_size) || !$this->sort_field) {
  727. // use memory less expensive (and quick) method for big result set
  728. $a_index = $this->message_index('', $this->sort_field, $this->sort_order);
  729. // get messages uids for one page...
  730. $msgs = array_slice($a_index, $start_msg, min($cnt-$start_msg, $this->page_size));
  731. if ($slice)
  732. $msgs = array_slice($msgs, -$slice, $slice);
  733. // ...and fetch headers
  734. $this->_fetch_headers($mailbox, join(',', $msgs), $a_msg_headers, NULL);
  735. // return empty array if no messages found
  736. if (!is_array($a_msg_headers) || empty($a_msg_headers))
  737. return array();
  738. $sorter = new crystal_header_sorter();
  739. $sorter->set_sequence_numbers($msgs);
  740. $sorter->sort_headers($a_msg_headers);
  741. return array_values($a_msg_headers);
  742. }
  743. else {
  744. // for small result set we can fetch all messages headers
  745. $this->_fetch_headers($mailbox, join(',', $msgs), $a_msg_headers, NULL);
  746. // return empty array if no messages found
  747. if (!is_array($a_msg_headers) || empty($a_msg_headers))
  748. return array();
  749. // if not already sorted
  750. $a_msg_headers = $this->conn->sortHeaders(
  751. $a_msg_headers, $this->sort_field, $this->sort_order);
  752. // only return the requested part of the set
  753. $a_msg_headers = array_slice(array_values($a_msg_headers),
  754. $start_msg, min($cnt-$start_msg, $this->page_size));
  755. if ($slice)
  756. $a_msg_headers = array_slice($a_msg_headers, -$slice, $slice);
  757. return $a_msg_headers;
  758. }
  759. }
  760. }
  761. /**
  762. * Private method for listing a set of threaded message headers (search results)
  763. *
  764. * @param string Mailbox/folder name
  765. * @param int Current page to list
  766. * @param string Header field to sort by
  767. * @param string Sort order [ASC|DESC]
  768. * @param boolean Number of slice items to extract from result array
  769. * @return array Indexed array with message header objects
  770. * @access private
  771. * @see crystal_imap::list_header_set()
  772. */
  773. private function _list_thread_header_set($mailbox, $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
  774. {
  775. // update search_set if previous data was fetched with disabled threading
  776. if (!$this->search_threads)
  777. $this->search('', $this->search_string, $this->search_charset, $sort_field);
  778. $thread_tree = $this->search_set['tree'];
  779. $msg_depth = $this->search_set['depth'];
  780. $has_children = $this->search_set['children'];
  781. $a_msg_headers = array();
  782. $page = $page ? $page : $this->list_page;
  783. $start_msg = ($page-1) * $this->page_size;
  784. $this->_set_sort_order($sort_field, $sort_order);
  785. $msg_index = $this->_sort_threads($mailbox, $thread_tree, array_keys($msg_depth));
  786. return $this->_fetch_thread_headers($mailbox,
  787. $thread_tree, $msg_depth, $has_children, $msg_index, $page, $slice=0);
  788. }
  789. /**
  790. * Helper function to get first and last index of the requested set
  791. *
  792. * @param int message count
  793. * @param mixed page number to show, or string 'all'
  794. * @return array array with two values: first index, last index
  795. * @access private
  796. */
  797. private function _get_message_range($max, $page)
  798. {
  799. $start_msg = ($page-1) * $this->page_size;
  800. if ($page=='all') {
  801. $begin = 0;
  802. $end = $max;
  803. }
  804. else if ($this->sort_order=='DESC') {
  805. $begin = $max - $this->page_size - $start_msg;
  806. $end = $max - $start_msg;
  807. }
  808. else {
  809. $begin = $start_msg;
  810. $end = $start_msg + $this->page_size;
  811. }
  812. if ($begin < 0) $begin = 0;
  813. if ($end < 0) $end = $max;
  814. if ($end > $max) $end = $max;
  815. return array($begin, $end);
  816. }
  817. /**
  818. * Fetches message headers
  819. * Used for loop
  820. *
  821. * @param string Mailbox name
  822. * @param string Message index to fetch
  823. * @param array Reference to message headers array
  824. * @param array Array with cache index
  825. * @return int Messages count
  826. * @access private
  827. */
  828. private function _fetch_headers($mailbox, $msgs, &$a_msg_headers, $cache_key)
  829. {
  830. // fetch reqested headers from server
  831. $a_header_index = $this->conn->fetchHeaders(
  832. $mailbox, $msgs, false, false, $this->fetch_add_headers);
  833. if (empty($a_header_index))
  834. return 0;
  835. // cache is incomplete
  836. $cache_index = $this->get_message_cache_index($cache_key);
  837. foreach ($a_header_index as $i => $headers) {
  838. if ($this->caching_enabled && $cache_index[$headers->id] != $headers->uid) {
  839. // prevent index duplicates
  840. if ($cache_index[$headers->id]) {
  841. $this->remove_message_cache($cache_key, $headers->id, true);
  842. unset($cache_index[$headers->id]);
  843. }
  844. // add message to cache
  845. $this->add_message_cache($cache_key, $headers->id, $headers, NULL,
  846. !in_array($headers->uid, $cache_index));
  847. }
  848. $a_msg_headers[$headers->uid] = $headers;
  849. }
  850. return count($a_msg_headers);
  851. }
  852. /**
  853. * Fetches IDS of pseudo recent messages.
  854. *
  855. * We compare the maximum UID to determine the number of
  856. * new messages because the RECENT flag is not reliable.
  857. *
  858. * @param string Mailbox/folder name
  859. * @return array List of recent message UIDs
  860. */
  861. function recent_uids($mbox_name = null, $nofetch = false)
  862. {
  863. $mailbox = $mbox_name ? $this->mod_mailbox($mbox_name) : $this->mailbox;
  864. $old_maxuid = intval($_SESSION['maxuid'][$mailbox]);
  865. // refresh message count -> will update $_SESSION['maxuid'][$mailbox]
  866. $this->_messagecount($mailbox, 'ALL', true);
  867. if ($_SESSION['maxuid'][$mailbox] > $old_maxuid) {
  868. $maxuid = max(1, $old_maxuid+1);
  869. return array_values((array)$this->conn->fetchHeaderIndex(
  870. $mailbox, "$maxuid:*", 'UID', $this->skip_deleted, true));
  871. }
  872. return array();
  873. }
  874. /**
  875. * Return sorted array of message IDs (not UIDs)
  876. *
  877. * @param string Mailbox to get index from
  878. * @param string Sort column
  879. * @param string Sort order [ASC, DESC]
  880. * @return array Indexed array with message ids
  881. */
  882. function message_index($mbox_name='', $sort_field=NULL, $sort_order=NULL)
  883. {
  884. if ($this->threading)
  885. return $this->thread_index($mbox_name, $sort_field, $sort_order);
  886. $this->_set_sort_order($sort_field, $sort_order);
  887. $mailbox = $mbox_name ? $this->mod_mailbox($mbox_name) : $this->mailbox;
  888. $key = "{$mailbox}:{$this->sort_field}:{$this->sort_order}:{$this->search_string}.msgi";
  889. // we have a saved search result, get index from there
  890. if (!isset($this->cache[$key]) && $this->search_string
  891. && !$this->search_threads && $mailbox == $this->mailbox) {
  892. // use message index sort as default sorting
  893. if (!$this->sort_field) {
  894. $msgs = $this->search_set;
  895. if ($this->search_sort_field != 'date')
  896. sort($msgs);
  897. if ($this->sort_order == 'DESC')
  898. $this->cache[$key] = array_reverse($msgs);
  899. else
  900. $this->cache[$key] = $msgs;
  901. }
  902. // sort with SORT command
  903. else if ($this->get_capability('SORT')) {
  904. if ($this->sort_field && $this->search_sort_field != $this->sort_field)
  905. $this->search('', $this->search_string, $this->search_charset, $this->sort_field);
  906. if ($this->sort_order == 'DESC')
  907. $this->cache[$key] = array_reverse($this->search_set);
  908. else
  909. $this->cache[$key] = $this->search_set;
  910. }
  911. else {
  912. $a_index = $this->conn->fetchHeaderIndex($mailbox,
  913. join(',', $this->search_set), $this->sort_field, $this->skip_deleted);
  914. if (is_array($a_index)) {
  915. if ($this->sort_order=="ASC")
  916. asort($a_index);
  917. else if ($this->sort_order=="DESC")
  918. arsort($a_index);
  919. $this->cache[$key] = array_keys($a_index);
  920. }
  921. else {
  922. $this->cache[$key] = array();
  923. }
  924. }
  925. }
  926. // have stored it in RAM
  927. if (isset($this->cache[$key]))
  928. return $this->cache[$key];
  929. // check local cache
  930. $cache_key = $mailbox.'.msg';
  931. $cache_status = $this->check_cache_status($mailbox, $cache_key);
  932. // cache is OK
  933. if ($cache_status>0) {
  934. $a_index = $this->get_message_cache_index($cache_key,
  935. true, $this->sort_field, $this->sort_order);
  936. return array_keys($a_index);
  937. }
  938. // use message index sort as default sorting
  939. if (!$this->sort_field) {
  940. if ($this->skip_deleted) {
  941. $a_index = $this->_search_index($mailbox, 'ALL');
  942. } else if ($max = $this->_messagecount($mailbox)) {
  943. $a_index = range(1, $max);
  944. }
  945. if ($this->sort_order == 'DESC')
  946. $a_index = array_reverse($a_index);
  947. $this->cache[$key] = $a_index;
  948. }
  949. // fetch complete message index
  950. else if ($this->get_capability('SORT')) {
  951. if ($a_index = $this->conn->sort($mailbox,
  952. $this->sort_field, $this->skip_deleted ? 'UNDELETED' : '')) {
  953. if ($this->sort_order == 'DESC')
  954. $a_index = array_reverse($a_index);
  955. $this->cache[$key] = $a_index;
  956. }
  957. }
  958. else if ($a_index = $this->conn->fetchHeaderIndex(
  959. $mailbox, "1:*", $this->sort_field, $this->skip_deleted)) {
  960. if ($this->sort_order=="ASC")
  961. asort($a_index);
  962. else if ($this->sort_order=="DESC")
  963. arsort($a_index);
  964. $this->cache[$key] = array_keys($a_index);
  965. }
  966. return $this->cache[$key];
  967. }
  968. /**
  969. * Return sorted array of threaded message IDs (not UIDs)
  970. *
  971. * @param string Mailbox to get index from
  972. * @param string Sort column
  973. * @param string Sort order [ASC, DESC]
  974. * @return array Indexed array with message IDs
  975. */
  976. function thread_index($mbox_name='', $sort_field=NULL, $sort_order=NULL)
  977. {
  978. $this->_set_sort_order($sort_field, $sort_order);
  979. $mailbox = $mbox_name ? $this->mod_mailbox($mbox_name) : $this->mailbox;
  980. $key = "{$mailbox}:{$this->sort_field}:{$this->sort_order}:{$this->search_string}.thi";
  981. // we have a saved search result, get index from there
  982. if (!isset($this->cache[$key]) && $this->search_string
  983. && $this->search_threads && $mailbox == $this->mailbox) {
  984. // use message IDs for better performance
  985. $ids = array_keys_recursive($this->search_set['tree']);
  986. $this->cache[$key] = $this->_flatten_threads($mailbox, $this->search_set['tree'], $ids);
  987. }
  988. // have stored it in RAM
  989. if (isset($this->cache[$key]))
  990. return $this->cache[$key];
  991. /*
  992. // check local cache
  993. $cache_key = $mailbox.'.msg';
  994. $cache_status = $this->check_cache_status($mailbox, $cache_key);
  995. // cache is OK
  996. if ($cache_status>0) {
  997. $a_index = $this->get_message_cache_index($cache_key, true, $this->sort_field, $this->sort_order);
  998. return array_keys($a_index);
  999. }
  1000. */
  1001. // get all threads (default sort order)
  1002. list ($thread_tree) = $this->_fetch_threads($mailbox);
  1003. $this->cache[$key] = $this->_flatten_threads($mailbox, $thread_tree);
  1004. return $this->cache[$key];
  1005. }
  1006. /**
  1007. * Return array of threaded messages (all, not only roots)
  1008. *
  1009. * @param string Mailbox to get index from
  1010. * @param array Threaded messages array (see _fetch_threads())
  1011. * @param array Message IDs if we know what we need (e.g. search result)
  1012. * for better performance
  1013. * @return array Indexed array with message IDs
  1014. *
  1015. * @access private
  1016. */
  1017. private function _flatten_threads($mailbox, $thread_tree, $ids=null)
  1018. {
  1019. if (empty($thread_tree))
  1020. return array();
  1021. $msg_index = $this->_sort_threads($mailbox, $thread_tree, $ids);
  1022. if ($this->sort_order == 'DESC')
  1023. $msg_index = array_reverse($msg_index);
  1024. // flatten threads array
  1025. $all_ids = array();
  1026. foreach($msg_index as $root) {
  1027. $all_ids[] = $root;
  1028. if (!empty($thread_tree[$root]))
  1029. $all_ids = array_merge($all_ids, array_keys_recursive($thread_tree[$root]));
  1030. }
  1031. return $all_ids;
  1032. }
  1033. /**
  1034. * @access private
  1035. */
  1036. private function sync_header_index($mailbox)
  1037. {
  1038. $cache_key = $mailbox.'.msg';
  1039. $cache_index = $this->get_message_cache_index($cache_key);
  1040. // fetch complete message index
  1041. $a_message_index = $this->conn->fetchHeaderIndex($mailbox, "1:*", 'UID', $this->skip_deleted);
  1042. if ($a_message_index === false)
  1043. return false;
  1044. foreach ($a_message_index as $id => $uid) {
  1045. // message in cache at correct position
  1046. if ($cache_index[$id] == $uid) {
  1047. unset($cache_index[$id]);
  1048. continue;
  1049. }
  1050. // message in cache but in wrong position
  1051. if (in_array((string)$uid, $cache_index, true)) {
  1052. unset($cache_index[$id]);
  1053. }
  1054. // other message at this position
  1055. if (isset($cache_index[$id])) {
  1056. $for_remove[] = $cache_index[$id];
  1057. unset($cache_index[$id]);
  1058. }
  1059. $for_update[] = $id;
  1060. }
  1061. // clear messages at wrong positions and those deleted that are still in cache_index
  1062. if (!empty($for_remove))
  1063. $cache_index = array_merge($cache_index, $for_remove);
  1064. if (!empty($cache_index))
  1065. $this->remove_message_cache($cache_key, $cache_index);
  1066. // fetch complete headers and add to cache
  1067. if (!empty($for_update)) {
  1068. if ($headers = $this->conn->fetchHeader($mailbox,
  1069. join(',', $for_update), false, $this->fetch_add_headers)) {
  1070. foreach ($headers as $header) {
  1071. $this->add_message_cache($cache_key, $header->id, $header, NULL,
  1072. in_array($header->uid, (array)$for_remove));
  1073. }
  1074. }
  1075. }
  1076. }
  1077. /**
  1078. * Invoke search request to IMAP server
  1079. *
  1080. * @param string mailbox name to search in
  1081. * @param string search string
  1082. * @param string search string charset
  1083. * @param string header field to sort by
  1084. * @return array search results as list of message ids
  1085. * @access public
  1086. */
  1087. function search($mbox_name='', $str=NULL, $charset=NULL, $sort_field=NULL)
  1088. {
  1089. if (!$str)
  1090. return false;
  1091. $mailbox = $mbox_name ? $this->mod_mailbox($mbox_name) : $this->mailbox;
  1092. $results = $this->_search_index($mailbox, $str, $charset, $sort_field);
  1093. // try search with US-ASCII charset (should be supported by server)
  1094. // only if UTF-8 search is not supported
  1095. if (empty($results) && !is_array($results) && !empty($charset) && $charset != 'US-ASCII')
  1096. {
  1097. // convert strings to US_ASCII
  1098. if(preg_match_all('/\{([0-9]+)\}\r\n/', $str, $matches, PREG_OFFSET_CAPTURE)) {
  1099. $last = 0; $res = '';
  1100. foreach($matches[1] as $m)
  1101. {
  1102. $string_offset = $m[1] + strlen($m[0]) + 4; // {}\r\n
  1103. $string = substr($str, $string_offset - 1, $m[0]);
  1104. $string = crystal_charset_convert($string, $charset, 'US-ASCII');
  1105. if (!$string)
  1106. continue;
  1107. $res .= sprintf("%s{%d}\r\n%s", substr($str, $last, $m[1] - $last - 1), strlen($string), $string);
  1108. $last = $m[0] + $string_offset - 1;
  1109. }
  1110. if ($last < strlen($str))
  1111. $res .= substr($str, $last, strlen($str)-$last);
  1112. }
  1113. else // strings for conversion not found
  1114. $res = $str;
  1115. $results = $this->search($mbox_name, $res, NULL, $sort_field);
  1116. }
  1117. $this->set_search_set($str, $results, $charset, $sort_field, (bool)$this->threading);
  1118. return $results;
  1119. }
  1120. /**
  1121. * Private search method
  1122. *
  1123. * @return array search results as list of message ids
  1124. * @access private
  1125. * @see crystal_imap::search()
  1126. */
  1127. private function _search_index($mailbox, $criteria='ALL', $charset=NULL, $sort_field=NULL)
  1128. {
  1129. $orig_criteria = $criteria;
  1130. if ($this->skip_deleted && !preg_match('/UNDELETED/', $criteria))
  1131. $criteria = 'UNDELETED '.$criteria;
  1132. if ($this->threading) {
  1133. list ($thread_tree, $msg_depth, $has_children) = $this->conn->thread(
  1134. $mailbox, $this->threading, $criteria, $charset);
  1135. $a_messages = array(
  1136. 'tree' => $thread_tree,
  1137. 'depth' => $msg_depth,
  1138. 'children' => $has_children
  1139. );
  1140. }
  1141. else if ($sort_field && $this->get_capability('SORT')) {
  1142. $charset = $charset ? $charset : $this->default_charset;
  1143. $a_messages = $this->conn->sort($mailbox, $sort_field, $criteria, false, $charset);
  1144. if (!$a_messages)
  1145. return array();
  1146. }
  1147. else {
  1148. if ($orig_criteria == 'ALL') {
  1149. $max = $this->_messagecount($mailbox);
  1150. $a_messages = $max ? range(1, $max) : array();
  1151. }
  1152. else {
  1153. $a_messages = $this->conn->search($mailbox,
  1154. ($charset ? "CHARSET $charset " : '') . $criteria);
  1155. if (!$a_messages)
  1156. return array();
  1157. // I didn't found that SEARCH always returns sorted IDs
  1158. if (!$this->sort_field)
  1159. sort($a_messages);
  1160. }
  1161. }
  1162. // update messagecount cache ?
  1163. // $a_mailbox_cache = get_cache('messagecount');
  1164. // $a_mailbox_cache[$mailbox][$criteria] = sizeof($a_messages);
  1165. // $this->update_cache('messagecount', $a_mailbox_cache);
  1166. return $a_messages;
  1167. }
  1168. /**
  1169. * Direct (real and simple) SEARCH request to IMAP server,
  1170. * without result sorting and caching
  1171. *
  1172. * @param string Mailbox name to search in
  1173. * @param string Search string
  1174. * @param boolean True if UIDs should be returned
  1175. * @return array Search results as list of message IDs or UIDs
  1176. * @access public
  1177. */
  1178. function search_once($mbox_name='', $str=NULL, $ret_uid=false)
  1179. {
  1180. if (!$str)
  1181. return false;
  1182. $mailbox = $mbox_name ? $this->mod_mailbox($mbox_name) : $this->mailbox;
  1183. return $this->conn->search($mailbox, $str, $ret_uid);
  1184. }
  1185. /**
  1186. * Sort thread
  1187. *
  1188. * @param string Mailbox name
  1189. * @param array Unsorted thread tree (crystal_imap_generic::thread() result)
  1190. * @param array Message IDs if we know what we need (e.g. search result)
  1191. * @return array Sorted roots IDs
  1192. * @access private
  1193. */
  1194. private function _sort_threads($mailbox, $thread_tree, $ids=NULL)

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