PageRenderTime 43ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 1ms

/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
  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)
  1195. {
  1196. // THREAD=ORDEREDSUBJECT: sorting by sent date of root message
  1197. // THREAD=REFERENCES: sorting by sent date of root message
  1198. // THREAD=REFS: sorting by the most recent date in each thread
  1199. // default sorting
  1200. if (!$this->sort_field || ($this->sort_field == 'date' && $this->threading == 'REFS')) {
  1201. return array_keys((array)$thread_tree);
  1202. }
  1203. // here we'll implement REFS sorting, for performance reason
  1204. else { // ($sort_field == 'date' && $this->threading != 'REFS')
  1205. // use SORT command
  1206. if ($this->get_capability('SORT')) {
  1207. $a_index = $this->conn->sort($mailbox, $this->sort_field,
  1208. !empty($ids) ? $ids : ($this->skip_deleted ? 'UNDELETED' : ''));
  1209. // return unsorted tree if we've got no index data
  1210. if (!$a_index)
  1211. return array_keys((array)$thread_tree);
  1212. }
  1213. else {
  1214. // fetch specified headers for all messages and sort them
  1215. $a_index = $this->conn->fetchHeaderIndex($mailbox, !empty($ids) ? $ids : "1:*",
  1216. $this->sort_field, $this->skip_deleted);
  1217. // return unsorted tree if we've got no index data
  1218. if (!$a_index)
  1219. return array_keys((array)$thread_tree);
  1220. asort($a_index); // ASC
  1221. $a_index = array_values($a_index);
  1222. }
  1223. return $this->_sort_thread_refs($thread_tree, $a_index);
  1224. }
  1225. }
  1226. /**
  1227. * THREAD=REFS sorting implementation
  1228. *
  1229. * @param array Thread tree array (message identifiers as keys)
  1230. * @param array Array of sorted message identifiers
  1231. * @return array Array of sorted roots messages
  1232. * @access private
  1233. */
  1234. private function _sort_thread_refs($tree, $index)
  1235. {
  1236. if (empty($tree))
  1237. return array();
  1238. $index = array_combine(array_values($index), $index);
  1239. // assign roots
  1240. foreach ($tree as $idx => $val) {
  1241. $index[$idx] = $idx;
  1242. if (!empty($val)) {
  1243. $idx_arr = array_keys_recursive($tree[$idx]);
  1244. foreach ($idx_arr as $subidx)
  1245. $index[$subidx] = $idx;
  1246. }
  1247. }
  1248. $index = array_values($index);
  1249. // create sorted array of roots
  1250. $msg_index = array();
  1251. if ($this->sort_order != 'DESC') {
  1252. foreach ($index as $idx)
  1253. if (!isset($msg_index[$idx]))
  1254. $msg_index[$idx] = $idx;
  1255. $msg_index = array_values($msg_index);
  1256. }
  1257. else {
  1258. for ($x=count($index)-1; $x>=0; $x--)
  1259. if (!isset($msg_index[$index[$x]]))
  1260. $msg_index[$index[$x]] = $index[$x];
  1261. $msg_index = array_reverse($msg_index);
  1262. }
  1263. return $msg_index;
  1264. }
  1265. /**
  1266. * Refresh saved search set
  1267. *
  1268. * @return array Current search set
  1269. */
  1270. function refresh_search()
  1271. {
  1272. if (!empty($this->search_string))
  1273. $this->search_set = $this->search('', $this->search_string, $this->search_charset,
  1274. $this->search_sort_field, $this->search_threads);
  1275. return $this->get_search_set();
  1276. }
  1277. /**
  1278. * Check if the given message ID is part of the current search set
  1279. *
  1280. * @return boolean True on match or if no search request is stored
  1281. */
  1282. function in_searchset($msgid)
  1283. {
  1284. if (!empty($this->search_string)) {
  1285. if ($this->search_threads)
  1286. return isset($this->search_set['depth']["$msgid"]);
  1287. else
  1288. return in_array("$msgid", (array)$this->search_set, true);
  1289. }
  1290. else
  1291. return true;
  1292. }
  1293. /**
  1294. * Return message headers object of a specific message
  1295. *
  1296. * @param int Message ID
  1297. * @param string Mailbox to read from
  1298. * @param boolean True if $id is the message UID
  1299. * @param boolean True if we need also BODYSTRUCTURE in headers
  1300. * @return object Message headers representation
  1301. */
  1302. function get_headers($id, $mbox_name=NULL, $is_uid=true, $bodystr=false)
  1303. {
  1304. $mailbox = $mbox_name ? $this->mod_mailbox($mbox_name) : $this->mailbox;
  1305. $uid = $is_uid ? $id : $this->_id2uid($id);
  1306. // get cached headers
  1307. if ($uid && ($headers = &$this->get_cached_message($mailbox.'.msg', $uid)))
  1308. return $headers;
  1309. $headers = $this->conn->fetchHeader(
  1310. $mailbox, $id, $is_uid, $bodystr, $this->fetch_add_headers);
  1311. // write headers cache
  1312. if ($headers) {
  1313. if ($headers->uid && $headers->id)
  1314. $this->uid_id_map[$mailbox][$headers->uid] = $headers->id;
  1315. $this->add_message_cache($mailbox.'.msg', $headers->id, $headers, NULL, true);
  1316. }
  1317. return $headers;
  1318. }
  1319. /**
  1320. * Fetch body structure from the IMAP server and build
  1321. * an object structure similar to the one generated by PEAR::Mail_mimeDecode
  1322. *
  1323. * @param int Message UID to fetch
  1324. * @param string Message BODYSTRUCTURE string (optional)
  1325. * @return object crystal_message_part Message part tree or False on failure
  1326. */
  1327. function &get_structure($uid, $structure_str='')
  1328. {
  1329. $cache_key = $this->mailbox.'.msg';
  1330. $headers = &$this->get_cached_message($cache_key, $uid);
  1331. // return cached message structure
  1332. if (is_object($headers) && is_object($headers->structure)) {
  1333. return $headers->structure;
  1334. }
  1335. if (!$structure_str) {
  1336. $structure_str = $this->conn->fetchStructureString($this->mailbox, $uid, true);
  1337. }
  1338. $structure = crystal_mime_struct::parseStructure($structure_str);
  1339. $struct = false;
  1340. // parse structure and add headers
  1341. if (!empty($structure)) {
  1342. $headers = $this->get_headers($uid);
  1343. $this->_msg_id = $headers->id;
  1344. // set message charset from message headers
  1345. if ($headers->charset)
  1346. $this->struct_charset = $headers->charset;
  1347. else
  1348. $this->struct_charset = $this->_structure_charset($structure);
  1349. // Here we can recognize malformed BODYSTRUCTURE and
  1350. // 1. [@TODO] parse the message in other way to create our own message structure
  1351. // 2. or just show the raw message body.
  1352. // Example of structure for malformed MIME message:
  1353. // ("text" "plain" ("charset" "us-ascii") NIL NIL "7bit" 2154 70 NIL NIL NIL)
  1354. if ($headers->ctype && $headers->ctype != 'text/plain'
  1355. && $structure[0] == 'text' && $structure[1] == 'plain') {
  1356. return false;
  1357. }
  1358. $struct = &$this->_structure_part($structure);
  1359. $struct->headers = get_object_vars($headers);
  1360. // don't trust given content-type
  1361. if (empty($struct->parts) && !empty($struct->headers['ctype'])) {
  1362. $struct->mime_id = '1';
  1363. $struct->mimetype = strtolower($struct->headers['ctype']);
  1364. list($struct->ctype_primary, $struct->ctype_secondary) = explode('/', $struct->mimetype);
  1365. }
  1366. // write structure to cache
  1367. if ($this->caching_enabled)
  1368. $this->add_message_cache($cache_key, $this->_msg_id, $headers, $struct);
  1369. }
  1370. return $struct;
  1371. }
  1372. /**
  1373. * Build message part object
  1374. *
  1375. * @access private
  1376. */
  1377. function &_structure_part($part, $count=0, $parent='', $mime_headers=null, $raw_headers=null)
  1378. {
  1379. $struct = new crystal_message_part;
  1380. $struct->mime_id = empty($parent) ? (string)$count : "$parent.$count";
  1381. // multipart
  1382. if (is_array($part[0])) {
  1383. $struct->ctype_primary = 'multipart';
  1384. // find first non-array entry
  1385. for ($i=1; $i<count($part); $i++) {
  1386. if (!is_array($part[$i])) {
  1387. $struct->ctype_secondary = strtolower($part[$i]);
  1388. break;
  1389. }
  1390. }
  1391. $struct->mimetype = 'multipart/'.$struct->ctype_secondary;
  1392. // build parts list for headers pre-fetching
  1393. for ($i=0, $count=0; $i<count($part); $i++) {
  1394. if (is_array($part[$i]) && count($part[$i]) > 3) {
  1395. // fetch message headers if message/rfc822
  1396. // or named part (could contain Content-Location header)
  1397. if (!is_array($part[$i][0])) {
  1398. $tmp_part_id = $struct->mime_id ? $struct->mime_id.'.'.($i+1) : $i+1;
  1399. if (strtolower($part[$i][0]) == 'message' && strtolower($part[$i][1]) == 'rfc822') {
  1400. $raw_part_headers[] = $tmp_part_id;
  1401. $mime_part_headers[] = $tmp_part_id;
  1402. }
  1403. else if (in_array('name', (array)$part[$i][2]) && (empty($part[$i][3]) || $part[$i][3]=='NIL')) {
  1404. $mime_part_headers[] = $tmp_part_id;
  1405. }
  1406. }
  1407. }
  1408. }
  1409. // pre-fetch headers of all parts (in one command for better performance)
  1410. // @TODO: we could do this before _structure_part() call, to fetch
  1411. // headers for parts on all levels
  1412. if ($mime_part_headers) {
  1413. $mime_part_headers = $this->conn->fetchMIMEHeaders($this->mailbox,
  1414. $this->_msg_id, $mime_part_headers);
  1415. }
  1416. // we'll need a real content-type of message/rfc822 part
  1417. if ($raw_part_headers) {
  1418. $raw_part_headers = $this->conn->fetchMIMEHeaders($this->mailbox,
  1419. $this->_msg_id, $raw_part_headers, false);
  1420. }
  1421. $struct->parts = array();
  1422. for ($i=0, $count=0; $i<count($part); $i++) {
  1423. if (is_array($part[$i]) && count($part[$i]) > 3) {
  1424. $tmp_part_id = $struct->mime_id ? $struct->mime_id.'.'.($i+1) : $i+1;
  1425. $struct->parts[] = $this->_structure_part($part[$i], ++$count, $struct->mime_id,
  1426. $mime_part_headers[$tmp_part_id], $raw_part_headers[$tmp_part_id]);
  1427. }
  1428. }
  1429. return $struct;
  1430. }
  1431. // regular part
  1432. $struct->ctype_primary = strtolower($part[0]);
  1433. $struct->ctype_secondary = strtolower($part[1]);
  1434. $struct->mimetype = $struct->ctype_primary.'/'.$struct->ctype_secondary;
  1435. // read content type parameters
  1436. if (is_array($part[2])) {
  1437. $struct->ctype_parameters = array();
  1438. for ($i=0; $i<count($part[2]); $i+=2)
  1439. $struct->ctype_parameters[strtolower($part[2][$i])] = $part[2][$i+1];
  1440. if (isset($struct->ctype_parameters['charset']))
  1441. $struct->charset = $struct->ctype_parameters['charset'];
  1442. }
  1443. // read content encoding
  1444. if (!empty($part[5]) && $part[5]!='NIL') {
  1445. $struct->encoding = strtolower($part[5]);
  1446. $struct->headers['content-transfer-encoding'] = $struct->encoding;
  1447. }
  1448. // get part size
  1449. if (!empty($part[6]) && $part[6]!='NIL')
  1450. $struct->size = intval($part[6]);
  1451. // read part disposition
  1452. $di = count($part) - 2;
  1453. if ((is_array($part[$di]) && count($part[$di]) == 2 && is_array($part[$di][1])) ||
  1454. (is_array($part[--$di]) && count($part[$di]) == 2)) {
  1455. $struct->disposition = strtolower($part[$di][0]);
  1456. if (is_array($part[$di][1]))
  1457. for ($n=0; $n<count($part[$di][1]); $n+=2)
  1458. $struct->d_parameters[strtolower($part[$di][1][$n])] = $part[$di][1][$n+1];
  1459. }
  1460. // get child parts
  1461. if (is_array($part[8]) && $di != 8) {
  1462. $struct->parts = array();
  1463. for ($i=0, $count=0; $i<count($part[8]); $i++)
  1464. if (is_array($part[8][$i]) && count($part[8][$i]) > 5)
  1465. $struct->parts[] = $this->_structure_part($part[8][$i], ++$count, $struct->mime_id);
  1466. }
  1467. // get part ID
  1468. if (!empty($part[3]) && $part[3]!='NIL') {
  1469. $struct->content_id = $part[3];
  1470. $struct->headers['content-id'] = $part[3];
  1471. if (empty($struct->disposition))
  1472. $struct->disposition = 'inline';
  1473. }
  1474. // fetch message headers if message/rfc822 or named part (could contain Content-Location header)
  1475. if ($struct->ctype_primary == 'message' || ($struct->ctype_parameters['name'] && !$struct->content_id)) {
  1476. if (empty($mime_headers)) {
  1477. $mime_headers = $this->conn->fetchPartHeader(
  1478. $this->mailbox, $this->_msg_id, false, $struct->mime_id);
  1479. }
  1480. $struct->headers = $this->_parse_headers($mime_headers) + $struct->headers;
  1481. // get real headers for message of type 'message/rfc822'
  1482. if ($struct->mimetype == 'message/rfc822') {
  1483. if (empty($raw_headers)) {
  1484. $raw_headers = $this->conn->fetchMIMEHeaders(
  1485. $this->mailbox, $this->_msg_id, (array)$struct->mime_id, false);
  1486. }
  1487. $struct->real_headers = $this->_parse_headers($raw_headers);
  1488. // get real content-type of message/rfc822
  1489. if (preg_match('/^([a-z0-9_\/-]+)/i', $struct->real_headers['content-type'], $matches)) {
  1490. $struct->real_mimetype = strtolower($matches[1]);
  1491. }
  1492. }
  1493. }
  1494. if ($struct->ctype_primary=='message') {
  1495. if (is_array($part[8]) && $di != 8 && empty($struct->parts))
  1496. $struct->parts[] = $this->_structure_part($part[8], ++$count, $struct->mime_id);
  1497. }
  1498. // normalize filename property
  1499. $this->_set_part_filename($struct, $mime_headers);
  1500. return $struct;
  1501. }
  1502. /**
  1503. * Set attachment filename from message part structure
  1504. *
  1505. * @access private
  1506. * @param object crystal_message_part Part object
  1507. * @param string Part's raw headers
  1508. */
  1509. private function _set_part_filename(&$part, $headers=null)
  1510. {
  1511. if (!empty($part->d_parameters['filename']))
  1512. $filename_mime = $part->d_parameters['filename'];
  1513. else if (!empty($part->d_parameters['filename*']))
  1514. $filename_encoded = $part->d_parameters['filename*'];
  1515. else if (!empty($part->ctype_parameters['name*']))
  1516. $filename_encoded = $part->ctype_parameters['name*'];
  1517. // RFC2231 value continuations
  1518. // TODO: this should be rewrited to support RFC2231 4.1 combinations
  1519. else if (!empty($part->d_parameters['filename*0'])) {
  1520. $i = 0;
  1521. while (isset($part->d_parameters['filename*'.$i])) {
  1522. $filename_mime .= $part->d_parameters['filename*'.$i];
  1523. $i++;
  1524. }
  1525. // some servers (eg. dovecot-1.x) have no support for parameter value continuations
  1526. // we must fetch and parse headers "manually"
  1527. if ($i<2) {
  1528. if (!$headers) {
  1529. $headers = $this->conn->fetchPartHeader(
  1530. $this->mailbox, $this->_msg_id, false, $part->mime_id);
  1531. }
  1532. $filename_mime = '';
  1533. $i = 0;
  1534. while (preg_match('/filename\*'.$i.'\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
  1535. $filename_mime .= $matches[1];
  1536. $i++;
  1537. }
  1538. }
  1539. }
  1540. else if (!empty($part->d_parameters['filename*0*'])) {
  1541. $i = 0;
  1542. while (isset($part->d_parameters['filename*'.$i.'*'])) {
  1543. $filename_encoded .= $part->d_parameters['filename*'.$i.'*'];
  1544. $i++;
  1545. }
  1546. if ($i<2) {
  1547. if (!$headers) {
  1548. $headers = $this->conn->fetchPartHeader(
  1549. $this->mailbox, $this->_msg_id, false, $part->mime_id);
  1550. }
  1551. $filename_encoded = '';
  1552. $i = 0; $matches = array();
  1553. while (preg_match('/filename\*'.$i.'\*\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
  1554. $filename_encoded .= $matches[1];
  1555. $i++;
  1556. }
  1557. }
  1558. }
  1559. else if (!empty($part->ctype_parameters['name*0'])) {
  1560. $i = 0;
  1561. while (isset($part->ctype_parameters['name*'.$i])) {
  1562. $filename_mime .= $part->ctype_parameters['name*'.$i];
  1563. $i++;
  1564. }
  1565. if ($i<2) {
  1566. if (!$headers) {
  1567. $headers = $this->conn->fetchPartHeader(
  1568. $this->mailbox, $this->_msg_id, false, $part->mime_id);
  1569. }
  1570. $filename_mime = '';
  1571. $i = 0; $matches = array();
  1572. while (preg_match('/\s+name\*'.$i.'\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
  1573. $filename_mime .= $matches[1];
  1574. $i++;
  1575. }
  1576. }
  1577. }
  1578. else if (!empty($part->ctype_parameters['name*0*'])) {
  1579. $i = 0;
  1580. while (isset($part->ctype_parameters['name*'.$i.'*'])) {
  1581. $filename_encoded .= $part->ctype_parameters['name*'.$i.'*'];
  1582. $i++;
  1583. }
  1584. if ($i<2) {
  1585. if (!$headers) {
  1586. $headers = $this->conn->fetchPartHeader(
  1587. $this->mailbox, $this->_msg_id, false, $part->mime_id);
  1588. }
  1589. $filename_encoded = '';
  1590. $i = 0; $matches = array();
  1591. while (preg_match('/\s+name\*'.$i.'\*\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
  1592. $filename_encoded .= $matches[1];
  1593. $i++;
  1594. }
  1595. }
  1596. }
  1597. // read 'name' after rfc2231 parameters as it may contains truncated filename (from Thunderbird)
  1598. else if (!empty($part->ctype_parameters['name']))
  1599. $filename_mime = $part->ctype_parameters['name'];
  1600. // Content-Disposition
  1601. else if (!empty($part->headers['content-description']))
  1602. $filename_mime = $part->headers['content-description'];
  1603. else
  1604. return;
  1605. // decode filename
  1606. if (!empty($filename_mime)) {
  1607. $part->filename = crystal_imap::decode_mime_string($filename_mime,
  1608. $part->charset ? $part->charset : ($this->struct_charset ? $this->struct_charset :
  1609. rc_detect_encoding($filename_mime, $this->default_charset)));
  1610. }
  1611. else if (!empty($filename_encoded)) {
  1612. // decode filename according to RFC 2231, Section 4
  1613. if (preg_match("/^([^']*)'[^']*'(.*)$/", $filename_encoded, $fmatches)) {
  1614. $filename_charset = $fmatches[1];
  1615. $filename_encoded = $fmatches[2];
  1616. }
  1617. $part->filename = crystal_charset_convert(urldecode($filename_encoded), $filename_charset);
  1618. }
  1619. }
  1620. /**
  1621. * Get charset name from message structure (first part)
  1622. *
  1623. * @access private
  1624. * @param array Message structure
  1625. * @return string Charset name
  1626. */
  1627. function _structure_charset($structure)
  1628. {
  1629. while (is_array($structure)) {
  1630. if (is_array($structure[2]) && $structure[2][0] == 'charset')
  1631. return $structure[2][1];
  1632. $structure = $structure[0];
  1633. }
  1634. }
  1635. /**
  1636. * Fetch message body of a specific message from the server
  1637. *
  1638. * @param int Message UID
  1639. * @param string Part number
  1640. * @param object crystal_message_part Part object created by get_structure()
  1641. * @param mixed True to print part, ressource to write part contents in
  1642. * @param resource File pointer to save the message part
  1643. * @return string Message/part body if not printed
  1644. */
  1645. function &get_message_part($uid, $part=1, $o_part=NULL, $print=NULL, $fp=NULL)
  1646. {
  1647. // get part encoding if not provided
  1648. if (!is_object($o_part)) {
  1649. $structure_str = $this->conn->fetchStructureString($this->mailbox, $uid, true);
  1650. $structure = new crystal_mime_struct();
  1651. // error or message not found
  1652. if (!$structure->loadStructure($structure_str)) {
  1653. return false;
  1654. }
  1655. $o_part = new crystal_message_part;
  1656. $o_part->ctype_primary = strtolower($structure->getPartType($part));
  1657. $o_part->encoding = strtolower($structure->getPartEncoding($part));
  1658. $o_part->charset = $structure->getPartCharset($part);
  1659. }
  1660. // TODO: Add caching for message parts
  1661. if (!$part) $part = 'TEXT';
  1662. $body = $this->conn->handlePartBody($this->mailbox, $uid, true, $part,
  1663. $o_part->encoding, $print, $fp);
  1664. if ($fp || $print)
  1665. return true;
  1666. // convert charset (if text or message part)
  1667. if ($o_part->ctype_primary=='text' || $o_part->ctype_primary=='message') {
  1668. // assume default if no charset specified
  1669. if (empty($o_part->charset) || strtolower($o_part->charset) == 'us-ascii')
  1670. $o_part->charset = $this->default_charset;
  1671. $body = crystal_charset_convert($body, $o_part->charset);
  1672. }
  1673. return $body;
  1674. }
  1675. /**
  1676. * Fetch message body of a specific message from the server
  1677. *
  1678. * @param int Message UID
  1679. * @return string Message/part body
  1680. * @see crystal_imap::get_message_part()
  1681. */
  1682. function &get_body($uid, $part=1)
  1683. {
  1684. $headers = $this->get_headers($uid);
  1685. return crystal_charset_convert($this->get_message_part($uid, $part, NULL),
  1686. $headers->charset ? $headers->charset : $this->default_charset);
  1687. }
  1688. /**
  1689. * Returns the whole message source as string
  1690. *
  1691. * @param int Message UID
  1692. * @return string Message source string
  1693. */
  1694. function &get_raw_body($uid)
  1695. {
  1696. return $this->conn->handlePartBody($this->mailbox, $uid, true);
  1697. }
  1698. /**
  1699. * Returns the message headers as string
  1700. *
  1701. * @param int Message UID
  1702. * @return string Message headers string
  1703. */
  1704. function &get_raw_headers($uid)
  1705. {
  1706. return $this->conn->fetchPartHeader($this->mailbox, $uid, true);
  1707. }
  1708. /**
  1709. * Sends the whole message source to stdout
  1710. *
  1711. * @param int Message UID
  1712. */
  1713. function print_raw_body($uid)
  1714. {
  1715. $this->conn->handlePartBody($this->mailbox, $uid, true, NULL, NULL, true);
  1716. }
  1717. /**
  1718. * Set message flag to one or several messages
  1719. *
  1720. * @param mixed Message UIDs as array or comma-separated string, or '*'
  1721. * @param string Flag to set: SEEN, UNDELETED, DELETED, RECENT, ANSWERED, DRAFT, MDNSENT
  1722. * @param string Folder name
  1723. * @param boolean True to skip message cache clean up
  1724. * @return int Number of flagged messages, -1 on failure
  1725. */
  1726. function set_flag($uids, $flag, $mbox_name=NULL, $skip_cache=false)
  1727. {
  1728. $mailbox = $mbox_name ? $this->mod_mailbox($mbox_name) : $this->mailbox;
  1729. $flag = strtoupper($flag);
  1730. list($uids, $all_mode) = $this->_parse_uids($uids, $mailbox);
  1731. if (strpos($flag, 'UN') === 0)
  1732. $result = $this->conn->unflag($mailbox, $uids, substr($flag, 2));
  1733. else
  1734. $result = $this->conn->flag($mailbox, $uids, $flag);
  1735. if ($result >= 0) {
  1736. // reload message headers if cached
  1737. if ($this->caching_enabled && !$skip_cache) {
  1738. $cache_key = $mailbox.'.msg';
  1739. if ($all_mode)
  1740. $this->clear_message_cache($cache_key);
  1741. else
  1742. $this->remove_message_cache($cache_key, explode(',', $uids));
  1743. }
  1744. // update counters
  1745. if ($flag=='SEEN')
  1746. $this->_set_messagecount($mailbox, 'UNSEEN', $result*(-1));
  1747. else if ($flag=='UNSEEN')
  1748. $this->_set_messagecount($mailbox, 'UNSEEN', $result);
  1749. else if ($flag=='DELETED')
  1750. $this->_set_messagecount($mailbox, 'ALL', $result*(-1));
  1751. }
  1752. return $result;
  1753. }
  1754. /**
  1755. * Remove message flag for one or several messages
  1756. *
  1757. * @param mixed Message UIDs as array or comma-separated string, or '*'
  1758. * @param string Flag to unset: SEEN, DELETED, RECENT, ANSWERED, DRAFT, MDNSENT
  1759. * @param string Folder name
  1760. * @return int Number of flagged messages, -1 on failure
  1761. * @see set_flag
  1762. */
  1763. function unset_flag($uids, $flag, $mbox_name=NULL)
  1764. {
  1765. return $this->set_flag($uids, 'UN'.$flag, $mbox_name);
  1766. }
  1767. /**
  1768. * Append a mail message (source) to a specific mailbox
  1769. *
  1770. * @param string Target mailbox
  1771. * @param string The message source string or filename
  1772. * @param string Headers string if $message contains only the body
  1773. * @param boolean True if $message is a filename
  1774. *
  1775. * @return boolean True on success, False on error
  1776. */
  1777. function save_message($mbox_name, &$message, $headers='', $is_file=false)
  1778. {
  1779. $mailbox = $this->mod_mailbox($mbox_name);
  1780. // make sure mailbox exists
  1781. if ($this->mailbox_exists($mbox_name, true)) {
  1782. if ($is_file) {
  1783. $separator = cmail::get_instance()->config->header_delimiter();
  1784. $saved = $this->conn->appendFromFile($mailbox, $message,
  1785. $headers, $separator.$separator);
  1786. }
  1787. else
  1788. $saved = $this->conn->append($mailbox, $message);
  1789. }
  1790. if ($saved) {
  1791. // increase messagecount of the target mailbox
  1792. $this->_set_messagecount($mailbox, 'ALL', 1);
  1793. }
  1794. return $saved;
  1795. }
  1796. /**
  1797. * Move a message from one mailbox to another
  1798. *
  1799. * @param mixed Message UIDs as array or comma-separated string, or '*'
  1800. * @param string Target mailbox
  1801. * @param string Source mailbox
  1802. * @return boolean True on success, False on error
  1803. */
  1804. function move_message($uids, $to_mbox, $from_mbox='')
  1805. {
  1806. $fbox = $from_mbox;
  1807. $tbox = $to_mbox;
  1808. $to_mbox = $this->mod_mailbox($to_mbox);
  1809. $from_mbox = $from_mbox ? $this->mod_mailbox($from_mbox) : $this->mailbox;
  1810. list($uids, $all_mode) = $this->_parse_uids($uids, $from_mbox);
  1811. // exit if no message uids are specified
  1812. if (empty($uids))
  1813. return false;
  1814. // make sure mailbox exists
  1815. if ($to_mbox != 'INBOX' && !$this->mailbox_exists($tbox, true)) {
  1816. if (in_array($tbox, $this->default_folders))
  1817. $this->create_mailbox($tbox, true);
  1818. else
  1819. return false;
  1820. }
  1821. // flag messages as read before moving them
  1822. $config = cmail::get_instance()->config;
  1823. if ($config->get('read_when_deleted') && $tbox == $config->get('trash_mbox')) {
  1824. // don't flush cache (4th argument)
  1825. $this->set_flag($uids, 'SEEN', $fbox, true);
  1826. }
  1827. // move messages
  1828. $move = $this->conn->move($uids, $from_mbox, $to_mbox);
  1829. $moved = !($move === false || $move < 0);
  1830. // send expunge command in order to have the moved message
  1831. // really deleted from the source mailbox
  1832. if ($moved) {
  1833. $this->_expunge($from_mbox, false, $uids);
  1834. $this->_clear_messagecount($from_mbox);
  1835. $this->_clear_messagecount($to_mbox);
  1836. }
  1837. // moving failed
  1838. else if ($config->get('delete_always', false) && $tbox == $config->get('trash_mbox')) {
  1839. $moved = $this->delete_message($uids, $fbox);
  1840. }
  1841. if ($moved) {
  1842. // unset threads internal cache
  1843. unset($this->icache['threads']);
  1844. // remove message ids from search set
  1845. if ($this->search_set && $from_mbox == $this->mailbox) {
  1846. // threads are too complicated to just remove messages from set
  1847. if ($this->search_threads || $all_mode)
  1848. $this->refresh_search();
  1849. else {
  1850. $uids = explode(',', $uids);
  1851. foreach ($uids as $uid)
  1852. $a_mids[] = $this->_uid2id($uid, $from_mbox);
  1853. $this->search_set = array_diff($this->search_set, $a_mids);
  1854. }
  1855. }
  1856. // update cached message headers
  1857. $cache_key = $from_mbox.'.msg';
  1858. if ($all_mode || ($start_index = $this->get_message_cache_index_min($cache_key, $uids))) {
  1859. // clear cache from the lowest index on
  1860. $this->clear_message_cache($cache_key, $all_mode ? 1 : $start_index);
  1861. }
  1862. }
  1863. return $moved;
  1864. }
  1865. /**
  1866. * Copy a message from one mailbox to another
  1867. *
  1868. * @param mixed Message UIDs as array or comma-separated string, or '*'
  1869. * @param string Target mailbox
  1870. * @param string Source mailbox
  1871. * @return boolean True on success, False on error
  1872. */
  1873. function copy_message($uids, $to_mbox, $from_mbox='')
  1874. {
  1875. $fbox = $from_mbox;
  1876. $tbox = $to_mbox;
  1877. $to_mbox = $this->mod_mailbox($to_mbox);
  1878. $from_mbox = $from_mbox ? $this->mod_mailbox($from_mbox) : $this->mailbox;
  1879. list($uids, $all_mode) = $this->_parse_uids($uids, $from_mbox);
  1880. // exit if no message uids are specified
  1881. if (empty($uids))
  1882. return false;
  1883. // make sure mailbox exists
  1884. if ($to_mbox != 'INBOX' && !$this->mailbox_exists($tbox, true)) {
  1885. if (in_array($tbox, $this->default_folders))
  1886. $this->create_mailbox($tbox, true);
  1887. else
  1888. return false;
  1889. }
  1890. // copy messages
  1891. $copy = $this->conn->copy($uids, $from_mbox, $to_mbox);
  1892. $copied = !($copy === false || $copy < 0);
  1893. if ($copied) {
  1894. $this->_clear_messagecount($to_mbox);
  1895. }
  1896. return $copied;
  1897. }
  1898. /**
  1899. * Mark messages as deleted and expunge mailbox
  1900. *
  1901. * @param mixed Message UIDs as array or comma-separated string, or '*'
  1902. * @param string Source mailbox
  1903. * @return boolean True on success, False on error
  1904. */
  1905. function delete_message($uids, $mbox_name='')
  1906. {
  1907. $mailbox = $mbox_name ? $this->mod_mailbox($mbox_name) : $this->mailbox;
  1908. list($uids, $all_mode) = $this->_parse_uids($uids, $mailbox);
  1909. // exit if no message uids are specified
  1910. if (empty($uids))
  1911. return false;
  1912. $deleted = $this->conn->delete($mailbox, $uids);
  1913. if ($deleted) {
  1914. // send expunge command in order to have the deleted message
  1915. // really deleted from the mailbox
  1916. $this->_expunge($mailbox, false, $uids);
  1917. $this->_clear_messagecount($mailbox);
  1918. unset($this->uid_id_map[$mailbox]);
  1919. // unset threads internal cache
  1920. unset($this->icache['threads']);
  1921. // remove message ids from search set
  1922. if ($this->search_set && $mailbox == $this->mailbox) {
  1923. // threads are too complicated to just remove messages from set
  1924. if ($this->search_threads || $all_mode)
  1925. $this->refresh_search();
  1926. else {
  1927. $uids = explode(',', $uids);
  1928. foreach ($uids as $uid)
  1929. $a_mids[] = $this->_uid2id($uid, $mailbox);
  1930. $this->search_set = array_diff($this->search_set, $a_mids);
  1931. }
  1932. }
  1933. // remove deleted messages from cache
  1934. $cache_key = $mailbox.'.msg';
  1935. if ($all_mode || ($start_index = $this->get_message_cache_index_min($cache_key, $uids))) {
  1936. // clear cache from the lowest index on
  1937. $this->clear_message_cache($cache_key, $all_mode ? 1 : $start_index);
  1938. }
  1939. }
  1940. return $deleted;
  1941. }
  1942. /**
  1943. * Clear all messages in a specific mailbox
  1944. *
  1945. * @param string Mailbox name
  1946. * @return int Above 0 on success
  1947. */
  1948. function clear_mailbox($mbox_name=NULL)
  1949. {
  1950. $mailbox = !empty($mbox_name) ? $this->mod_mailbox($mbox_name) : $this->mailbox;
  1951. $msg_count = $this->_messagecount($mailbox, 'ALL');
  1952. if (!$msg_count) {
  1953. return 0;
  1954. }
  1955. $cleared = $this->conn->clearFolder($mailbox);
  1956. // make sure the message count cache is cleared as well
  1957. if ($cleared) {
  1958. $this->clear_message_cache($mailbox.'.msg');
  1959. $a_mailbox_cache = $this->get_cache('messagecount');
  1960. unset($a_mailbox_cache[$mailbox]);
  1961. $this->update_cache('messagecount', $a_mailbox_cache);
  1962. }
  1963. return $cleared;
  1964. }
  1965. /**
  1966. * Send IMAP expunge command and clear cache
  1967. *
  1968. * @param string Mailbox name
  1969. * @param boolean False if cache should not be cleared
  1970. * @return boolean True on success
  1971. */
  1972. function expunge($mbox_name='', $clear_cache=true)
  1973. {
  1974. $mailbox = $mbox_name ? $this->mod_mailbox($mbox_name) : $this->mailbox;
  1975. return $this->_expunge($mailbox, $clear_cache);
  1976. }
  1977. /**
  1978. * Send IMAP expunge command and clear cache
  1979. *
  1980. * @param string Mailbox name
  1981. * @param boolean False if cache should not be cleared
  1982. * @param mixed Message UIDs as array or comma-separated string, or '*'
  1983. * @return boolean True on success
  1984. * @access private
  1985. * @see crystal_imap::expunge()
  1986. */
  1987. private function _expunge($mailbox, $clear_cache=true, $uids=NULL)
  1988. {
  1989. if ($uids && $this->get_capability('UIDPLUS'))
  1990. $a_uids = is_array($uids) ? join(',', $uids) : $uids;
  1991. else
  1992. $a_uids = NULL;
  1993. $result = $this->conn->expunge($mailbox, $a_uids);
  1994. if ($result>=0 && $clear_cache) {
  1995. $this->clear_message_cache($mailbox.'.msg');
  1996. $this->_clear_messagecount($mailbox);
  1997. }
  1998. return $result;
  1999. }
  2000. /**
  2001. * Parse message UIDs input
  2002. *
  2003. * @param mixed UIDs array or comma-separated list or '*' or '1:*'
  2004. * @param string Mailbox name
  2005. * @return array Two elements array with UIDs converted to list and ALL flag
  2006. * @access private
  2007. */
  2008. private function _parse_uids($uids, $mailbox)
  2009. {
  2010. if ($uids === '*' || $uids === '1:*') {
  2011. if (empty($this->search_set)) {
  2012. $uids = '1:*';
  2013. $all = true;
  2014. }
  2015. // get UIDs from current search set
  2016. // @TODO: skip fetchUIDs() and work with IDs instead of UIDs (?)
  2017. else {
  2018. if ($this->search_threads)
  2019. $uids = $this->conn->fetchUIDs($mailbox, array_keys($this->search_set['depth']));
  2020. else
  2021. $uids = $this->conn->fetchUIDs($mailbox, $this->search_set);
  2022. // save ID-to-UID mapping in local cache
  2023. if (is_array($uids))
  2024. foreach ($uids as $id => $uid)
  2025. $this->uid_id_map[$mailbox][$uid] = $id;
  2026. $uids = join(',', $uids);
  2027. }
  2028. }
  2029. else {
  2030. if (is_array($uids))
  2031. $uids = join(',', $uids);
  2032. if (preg_match('/[^0-9,]/', $uids))
  2033. $uids = '';
  2034. }
  2035. return array($uids, (bool) $all);
  2036. }
  2037. /**
  2038. * Translate UID to message ID
  2039. *
  2040. * @param int Message UID
  2041. * @param string Mailbox name
  2042. * @return int Message ID
  2043. */
  2044. function get_id($uid, $mbox_name=NULL)
  2045. {
  2046. $mailbox = $mbox_name ? $this->mod_mailbox($mbox_name) : $this->mailbox;
  2047. return $this->_uid2id($uid, $mailbox);
  2048. }
  2049. /**
  2050. * Translate message number to UID
  2051. *
  2052. * @param int Message ID
  2053. * @param string Mailbox name
  2054. * @return int Message UID
  2055. */
  2056. function get_uid($id,$mbox_name=NULL)
  2057. {
  2058. $mailbox = $mbox_name ? $this->mod_mailbox($mbox_name) : $this->mailbox;
  2059. return $this->_id2uid($id, $mailbox);
  2060. }
  2061. /* --------------------------------
  2062. * folder managment
  2063. * --------------------------------*/
  2064. /**
  2065. * Public method for mailbox listing.
  2066. *
  2067. * Converts mailbox name with root dir first
  2068. *
  2069. * @param string Optional root folder
  2070. * @param string Optional filter for mailbox listing
  2071. * @return array List of mailboxes/folders
  2072. * @access public
  2073. */
  2074. function list_mailboxes($root='', $filter='*')
  2075. {
  2076. $a_out = array();
  2077. $a_mboxes = $this->_list_mailboxes($root, $filter);
  2078. foreach ($a_mboxes as $mbox_row) {
  2079. $name = $this->mod_mailbox($mbox_row, 'out');
  2080. if (strlen($name))
  2081. $a_out[] = $name;
  2082. }
  2083. // INBOX should always be available
  2084. if (!in_array('INBOX', $a_out))
  2085. array_unshift($a_out, 'INBOX');
  2086. // sort mailboxes
  2087. $a_out = $this->_sort_mailbox_list($a_out);
  2088. return $a_out;
  2089. }
  2090. /**
  2091. * Private method for mailbox listing
  2092. *
  2093. * @return array List of mailboxes/folders
  2094. * @see crystal_imap::list_mailboxes()
  2095. * @access private
  2096. */
  2097. private function _list_mailboxes($root='', $filter='*')
  2098. {
  2099. $a_defaults = $a_out = array();
  2100. // get cached folder list
  2101. $a_mboxes = $this->get_cache('mailboxes');
  2102. if (is_array($a_mboxes))
  2103. return $a_mboxes;
  2104. // Give plugins a chance to provide a list of mailboxes
  2105. $data = cmail::get_instance()->plugins->exec_hook('list_mailboxes',
  2106. array('root'=>$root,'filter'=>$filter));
  2107. if (isset($data['folders'])) {
  2108. $a_folders = $data['folders'];
  2109. }
  2110. else {
  2111. // retrieve list of folders from IMAP server
  2112. $a_folders = $this->conn->listSubscribed($this->mod_mailbox($root), $filter);
  2113. }
  2114. if (!is_array($a_folders) || !sizeof($a_folders))
  2115. $a_folders = array();
  2116. // write mailboxlist to cache
  2117. $this->update_cache('mailboxes', $a_folders);
  2118. return $a_folders;
  2119. }
  2120. /**
  2121. * Get a list of all folders available on the IMAP server
  2122. *
  2123. * @param string IMAP root dir
  2124. * @return array Indexed array with folder names
  2125. */
  2126. function list_unsubscribed($root='')
  2127. {
  2128. static $a_folders;
  2129. if (is_array($a_folders))
  2130. return $a_folders;
  2131. // retrieve list of folders from IMAP server
  2132. $a_mboxes = $this->conn->listMailboxes($this->mod_mailbox($root), '*');
  2133. // modify names with root dir
  2134. foreach ($a_mboxes as $mbox_name) {
  2135. if ($name = $this->mod_mailbox($mbox_name, 'out'))
  2136. $a_folders[] = $name;
  2137. }
  2138. // filter folders and sort them
  2139. $a_folders = $this->_sort_mailbox_list($a_folders);
  2140. return $a_folders;
  2141. }
  2142. /**
  2143. * Get mailbox quota information
  2144. * added by Nuny
  2145. *
  2146. * @return mixed Quota info or False if not supported
  2147. */
  2148. function get_quota()
  2149. {
  2150. if ($this->get_capability('QUOTA'))
  2151. return $this->conn->getQuota();
  2152. return false;
  2153. }
  2154. /**
  2155. * Subscribe to a specific mailbox(es)
  2156. *
  2157. * @param array Mailbox name(s)
  2158. * @return boolean True on success
  2159. */
  2160. function subscribe($a_mboxes)
  2161. {
  2162. if (!is_array($a_mboxes))
  2163. $a_mboxes = array($a_mboxes);
  2164. // let this common function do the main work
  2165. return $this->_change_subscription($a_mboxes, 'subscribe');
  2166. }
  2167. /**
  2168. * Unsubscribe mailboxes
  2169. *
  2170. * @param array Mailbox name(s)
  2171. * @return boolean True on success
  2172. */
  2173. function unsubscribe($a_mboxes)
  2174. {
  2175. if (!is_array($a_mboxes))
  2176. $a_mboxes = array($a_mboxes);
  2177. // let this common function do the main work
  2178. return $this->_change_subscription($a_mboxes, 'unsubscribe');
  2179. }
  2180. /**
  2181. * Create a new mailbox on the server and register it in local cache
  2182. *
  2183. * @param string New mailbox name (as utf-7 string)
  2184. * @param boolean True if the new mailbox should be subscribed
  2185. * @param string Name of the created mailbox, false on error
  2186. */
  2187. function create_mailbox($name, $subscribe=false)
  2188. {
  2189. $result = false;
  2190. // reduce mailbox name to 100 chars
  2191. $name = substr($name, 0, 100);
  2192. $abs_name = $this->mod_mailbox($name);
  2193. $result = $this->conn->createFolder($abs_name);
  2194. // try to subscribe it
  2195. if ($result && $subscribe)
  2196. $this->subscribe($name);
  2197. return $result ? $name : false;
  2198. }
  2199. /**
  2200. * Set a new name to an existing mailbox
  2201. *
  2202. * @param string Mailbox to rename (as utf-7 string)
  2203. * @param string New mailbox name (as utf-7 string)
  2204. * @return string Name of the renames mailbox, False on error
  2205. */
  2206. function rename_mailbox($mbox_name, $new_name)
  2207. {
  2208. $result = false;
  2209. // encode mailbox name and reduce it to 100 chars
  2210. $name = substr($new_name, 0, 100);
  2211. // make absolute path
  2212. $mailbox = $this->mod_mailbox($mbox_name);
  2213. $abs_name = $this->mod_mailbox($name);
  2214. // check if mailbox is subscribed
  2215. $a_subscribed = $this->_list_mailboxes();
  2216. $subscribed = in_array($mailbox, $a_subscribed);
  2217. // unsubscribe folder
  2218. if ($subscribed)
  2219. $this->conn->unsubscribe($mailbox);
  2220. if (strlen($abs_name))
  2221. $result = $this->conn->renameFolder($mailbox, $abs_name);
  2222. if ($result) {
  2223. $delm = $this->get_hierarchy_delimiter();
  2224. // check if mailbox children are subscribed
  2225. foreach ($a_subscribed as $c_subscribed)
  2226. if (preg_match('/^'.preg_quote($mailbox.$delm, '/').'/', $c_subscribed)) {
  2227. $this->conn->unsubscribe($c_subscribed);
  2228. $this->conn->subscribe(preg_replace('/^'.preg_quote($mailbox, '/').'/',
  2229. $abs_name, $c_subscribed));
  2230. }
  2231. // clear cache
  2232. $this->clear_message_cache($mailbox.'.msg');
  2233. $this->clear_cache('mailboxes');
  2234. }
  2235. // try to subscribe it
  2236. if ($result && $subscribed)
  2237. $this->conn->subscribe($abs_name);
  2238. return $result ? $name : false;
  2239. }
  2240. /**
  2241. * Remove mailboxes from server
  2242. *
  2243. * @param string Mailbox name(s) string/array
  2244. * @return boolean True on success
  2245. */
  2246. function delete_mailbox($mbox_name)
  2247. {
  2248. $deleted = false;
  2249. if (is_array($mbox_name))
  2250. $a_mboxes = $mbox_name;
  2251. else if (is_string($mbox_name) && strlen($mbox_name))
  2252. $a_mboxes = explode(',', $mbox_name);
  2253. if (is_array($a_mboxes)) {
  2254. foreach ($a_mboxes as $mbox_name) {
  2255. $mailbox = $this->mod_mailbox($mbox_name);
  2256. $sub_mboxes = $this->conn->listMailboxes($this->mod_mailbox(''),
  2257. $mbox_name . $this->delimiter . '*');
  2258. // unsubscribe mailbox before deleting
  2259. $this->conn->unsubscribe($mailbox);
  2260. // send delete command to server
  2261. $result = $this->conn->deleteFolder($mailbox);
  2262. if ($result >= 0) {
  2263. $deleted = true;
  2264. $this->clear_message_cache($mailbox.'.msg');
  2265. }
  2266. foreach ($sub_mboxes as $c_mbox) {
  2267. if ($c_mbox != 'INBOX') {
  2268. $this->conn->unsubscribe($c_mbox);
  2269. $result = $this->conn->deleteFolder($c_mbox);
  2270. if ($result >= 0) {
  2271. $deleted = true;
  2272. $this->clear_message_cache($c_mbox.'.msg');
  2273. }
  2274. }
  2275. }
  2276. }
  2277. }
  2278. // clear mailboxlist cache
  2279. if ($deleted)
  2280. $this->clear_cache('mailboxes');
  2281. return $deleted;
  2282. }
  2283. /**
  2284. * Create all folders specified as default
  2285. */
  2286. function create_default_folders()
  2287. {
  2288. // create default folders if they do not exist
  2289. foreach ($this->default_folders as $folder) {
  2290. if (!$this->mailbox_exists($folder))
  2291. $this->create_mailbox($folder, true);
  2292. else if (!$this->mailbox_exists($folder, true))
  2293. $this->subscribe($folder);
  2294. }
  2295. }
  2296. /**
  2297. * Checks if folder exists and is subscribed
  2298. *
  2299. * @param string Folder name
  2300. * @param boolean Enable subscription checking
  2301. * @return boolean TRUE or FALSE
  2302. */
  2303. function mailbox_exists($mbox_name, $subscription=false)
  2304. {
  2305. if ($mbox_name) {
  2306. if ($mbox_name == 'INBOX')
  2307. return true;
  2308. if ($subscription) {
  2309. if ($a_folders = $this->conn->listSubscribed($this->mod_mailbox(''), $mbox_name))
  2310. return true;
  2311. }
  2312. else {
  2313. $a_folders = $this->conn->listMailboxes($this->mod_mailbox(''), $mbox_mbox);
  2314. if (is_array($a_folders) && in_array($this->mod_mailbox($mbox_name), $a_folders))
  2315. return true;
  2316. }
  2317. }
  2318. return false;
  2319. }
  2320. /**
  2321. * Modify folder name for input/output according to root dir and namespace
  2322. *
  2323. * @param string Folder name
  2324. * @param string Mode
  2325. * @return string Folder name
  2326. */
  2327. function mod_mailbox($mbox_name, $mode='in')
  2328. {
  2329. if ($mbox_name == 'INBOX')
  2330. return $mbox_name;
  2331. if (!empty($this->root_dir)) {
  2332. if ($mode=='in')
  2333. $mbox_name = $this->root_dir.$this->delimiter.$mbox_name;
  2334. else if (!empty($mbox_name)) // $mode=='out'
  2335. $mbox_name = substr($mbox_name, strlen($this->root_dir)+1);
  2336. }
  2337. return $mbox_name;
  2338. }
  2339. /* --------------------------------
  2340. * internal caching methods
  2341. * --------------------------------*/
  2342. /**
  2343. * @access public
  2344. */
  2345. function set_caching($set)
  2346. {
  2347. if ($set && is_object($this->db))
  2348. $this->caching_enabled = true;
  2349. else
  2350. $this->caching_enabled = false;
  2351. }
  2352. /**
  2353. * @access public
  2354. */
  2355. function get_cache($key)
  2356. {
  2357. // read cache (if it was not read before)
  2358. if (!count($this->cache) && $this->caching_enabled) {
  2359. return $this->_read_cache_record($key);
  2360. }
  2361. return $this->cache[$key];
  2362. }
  2363. /**
  2364. * @access private
  2365. */
  2366. private function update_cache($key, $data)
  2367. {
  2368. $this->cache[$key] = $data;
  2369. $this->cache_changed = true;
  2370. $this->cache_changes[$key] = true;
  2371. }
  2372. /**
  2373. * @access private
  2374. */
  2375. private function write_cache()
  2376. {
  2377. if ($this->caching_enabled && $this->cache_changed) {
  2378. foreach ($this->cache as $key => $data) {
  2379. if ($this->cache_changes[$key])
  2380. $this->_write_cache_record($key, serialize($data));
  2381. }
  2382. }
  2383. }
  2384. /**
  2385. * @access public
  2386. */
  2387. function clear_cache($key=NULL)
  2388. {
  2389. if (!$this->caching_enabled)
  2390. return;
  2391. if ($key===NULL) {
  2392. foreach ($this->cache as $key => $data)
  2393. $this->_clear_cache_record($key);
  2394. $this->cache = array();
  2395. $this->cache_changed = false;
  2396. $this->cache_changes = array();
  2397. }
  2398. else {
  2399. $this->_clear_cache_record($key);
  2400. $this->cache_changes[$key] = false;
  2401. unset($this->cache[$key]);
  2402. }
  2403. }
  2404. /**
  2405. * @access private
  2406. */
  2407. private function _read_cache_record($key)
  2408. {
  2409. if ($this->db) {
  2410. // get cached data from DB
  2411. $sql_result = $this->db->query(
  2412. "SELECT cache_id, data, cache_key ".
  2413. "FROM ".get_table_name('cache').
  2414. " WHERE user_id=? ".
  2415. "AND cache_key LIKE 'IMAP.%'",
  2416. $_SESSION['user_id']);
  2417. while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
  2418. $sql_key = preg_replace('/^IMAP\./', '', $sql_arr['cache_key']);
  2419. $this->cache_keys[$sql_key] = $sql_arr['cache_id'];
  2420. if (!isset($this->cache[$sql_key]))
  2421. $this->cache[$sql_key] = $sql_arr['data'] ? unserialize($sql_arr['data']) : false;
  2422. }
  2423. }
  2424. return $this->cache[$key];
  2425. }
  2426. /**
  2427. * @access private
  2428. */
  2429. private function _write_cache_record($key, $data)
  2430. {
  2431. if (!$this->db)
  2432. return false;
  2433. // update existing cache record
  2434. if ($this->cache_keys[$key]) {
  2435. $this->db->query(
  2436. "UPDATE ".get_table_name('cache').
  2437. " SET created=". $this->db->now().", data=? ".
  2438. "WHERE user_id=? ".
  2439. "AND cache_key=?",
  2440. $data,
  2441. $_SESSION['user_id'],
  2442. 'IMAP.'.$key);
  2443. }
  2444. // add new cache record
  2445. else {
  2446. $this->db->query(
  2447. "INSERT INTO ".get_table_name('cache').
  2448. " (created, user_id, cache_key, data) ".
  2449. "VALUES (".$this->db->now().", ?, ?, ?)",
  2450. $_SESSION['user_id'],
  2451. 'IMAP.'.$key,
  2452. $data);
  2453. // get cache entry ID for this key
  2454. $sql_result = $this->db->query(
  2455. "SELECT cache_id ".
  2456. "FROM ".get_table_name('cache').
  2457. " WHERE user_id=? ".
  2458. "AND cache_key=?",
  2459. $_SESSION['user_id'],
  2460. 'IMAP.'.$key);
  2461. if ($sql_arr = $this->db->fetch_assoc($sql_result))
  2462. $this->cache_keys[$key] = $sql_arr['cache_id'];
  2463. }
  2464. }
  2465. /**
  2466. * @access private
  2467. */
  2468. private function _clear_cache_record($key)
  2469. {
  2470. $this->db->query(
  2471. "DELETE FROM ".get_table_name('cache').
  2472. " WHERE user_id=? ".
  2473. "AND cache_key=?",
  2474. $_SESSION['user_id'],
  2475. 'IMAP.'.$key);
  2476. unset($this->cache_keys[$key]);
  2477. }
  2478. /* --------------------------------
  2479. * message caching methods
  2480. * --------------------------------*/
  2481. /**
  2482. * Checks if the cache is up-to-date
  2483. *
  2484. * @param string Mailbox name
  2485. * @param string Internal cache key
  2486. * @return int Cache status: -3 = off, -2 = incomplete, -1 = dirty, 1 = OK
  2487. */
  2488. private function check_cache_status($mailbox, $cache_key)
  2489. {
  2490. if (!$this->caching_enabled)
  2491. return -3;
  2492. $cache_index = $this->get_message_cache_index($cache_key);
  2493. $msg_count = $this->_messagecount($mailbox);
  2494. $cache_count = count($cache_index);
  2495. // empty mailbox
  2496. if (!$msg_count)
  2497. return $cache_count ? -2 : 1;
  2498. // @TODO: We've got one big performance problem in cache status checking method
  2499. // E.g. mailbox contains 1000 messages, in cache table we've got first 100
  2500. // of them. Now if we want to display only that 100 (which we've got)
  2501. // check_cache_status returns 'incomplete' and messages are fetched
  2502. // from IMAP instead of DB.
  2503. if ($cache_count==$msg_count) {
  2504. if ($this->skip_deleted) {
  2505. $h_index = $this->conn->fetchHeaderIndex($mailbox, "1:*", 'UID', $this->skip_deleted);
  2506. if (empty($h_index))
  2507. return -2;
  2508. if (sizeof($h_index) == $cache_count) {
  2509. $cache_index = array_flip($cache_index);
  2510. foreach ($h_index as $idx => $uid)
  2511. unset($cache_index[$uid]);
  2512. if (empty($cache_index))
  2513. return 1;
  2514. }
  2515. return -2;
  2516. } else {
  2517. // get UID of message with highest index
  2518. $uid = $this->conn->ID2UID($mailbox, $msg_count);
  2519. $cache_uid = array_pop($cache_index);
  2520. // uids of highest message matches -> cache seems OK
  2521. if ($cache_uid == $uid)
  2522. return 1;
  2523. }
  2524. // cache is dirty
  2525. return -1;
  2526. }
  2527. // if cache count differs less than 10% report as dirty
  2528. else if (abs($msg_count - $cache_count) < $msg_count/10)
  2529. return -1;
  2530. else
  2531. return -2;
  2532. }
  2533. /**
  2534. * @access private
  2535. */
  2536. private function get_message_cache($key, $from, $to, $sort_field, $sort_order)
  2537. {
  2538. $cache_key = "$key:$from:$to:$sort_field:$sort_order";
  2539. // use idx sort as default sorting
  2540. if (!$sort_field || !in_array($sort_field, $this->db_header_fields)) {
  2541. $sort_field = 'idx';
  2542. }
  2543. if ($this->caching_enabled && !isset($this->cache[$cache_key])) {
  2544. $this->cache[$cache_key] = array();
  2545. $sql_result = $this->db->limitquery(
  2546. "SELECT idx, uid, headers".
  2547. " FROM ".get_table_name('messages').
  2548. " WHERE user_id=?".
  2549. " AND cache_key=?".
  2550. " ORDER BY ".$this->db->quoteIdentifier($sort_field)." ".strtoupper($sort_order),
  2551. $from,
  2552. $to - $from,
  2553. $_SESSION['user_id'],
  2554. $key);
  2555. while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
  2556. $uid = $sql_arr['uid'];
  2557. $this->cache[$cache_key][$uid] = $this->db->decode(unserialize($sql_arr['headers']));
  2558. // featch headers if unserialize failed
  2559. if (empty($this->cache[$cache_key][$uid]))
  2560. $this->cache[$cache_key][$uid] = $this->conn->fetchHeader(
  2561. preg_replace('/.msg$/', '', $key), $uid, true, $this->fetch_add_headers);
  2562. }
  2563. }
  2564. return $this->cache[$cache_key];
  2565. }
  2566. /**
  2567. * @access private
  2568. */
  2569. private function &get_cached_message($key, $uid)
  2570. {
  2571. $internal_key = 'message';
  2572. if ($this->caching_enabled && !isset($this->icache[$internal_key][$uid])) {
  2573. $sql_result = $this->db->query(
  2574. "SELECT idx, headers, structure".
  2575. " FROM ".get_table_name('messages').
  2576. " WHERE user_id=?".
  2577. " AND cache_key=?".
  2578. " AND uid=?",
  2579. $_SESSION['user_id'],
  2580. $key,
  2581. $uid);
  2582. if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
  2583. $this->uid_id_map[preg_replace('/\.msg$/', '', $key)][$uid] = $sql_arr['idx'];
  2584. $this->icache[$internal_key][$uid] = $this->db->decode(unserialize($sql_arr['headers']));
  2585. if (is_object($this->icache[$internal_key][$uid]) && !empty($sql_arr['structure']))
  2586. $this->icache[$internal_key][$uid]->structure = $this->db->decode(unserialize($sql_arr['structure']));
  2587. }
  2588. }
  2589. return $this->icache[$internal_key][$uid];
  2590. }
  2591. /**
  2592. * @access private
  2593. */
  2594. private function get_message_cache_index($key, $force=false, $sort_field='idx', $sort_order='ASC')
  2595. {
  2596. static $sa_message_index = array();
  2597. // empty key -> empty array
  2598. if (!$this->caching_enabled || empty($key))
  2599. return array();
  2600. if (!empty($sa_message_index[$key]) && !$force)
  2601. return $sa_message_index[$key];
  2602. // use idx sort as default
  2603. if (!$sort_field || !in_array($sort_field, $this->db_header_fields))
  2604. $sort_field = 'idx';
  2605. $sa_message_index[$key] = array();
  2606. $sql_result = $this->db->query(
  2607. "SELECT idx, uid".
  2608. " FROM ".get_table_name('messages').
  2609. " WHERE user_id=?".
  2610. " AND cache_key=?".
  2611. " ORDER BY ".$this->db->quote_identifier($sort_field)." ".$sort_order,
  2612. $_SESSION['user_id'],
  2613. $key);
  2614. while ($sql_arr = $this->db->fetch_assoc($sql_result))
  2615. $sa_message_index[$key][$sql_arr['idx']] = $sql_arr['uid'];
  2616. return $sa_message_index[$key];
  2617. }
  2618. /**
  2619. * @access private
  2620. */
  2621. private function add_message_cache($key, $index, $headers, $struct=null, $force=false)
  2622. {
  2623. if (empty($key) || !is_object($headers) || empty($headers->uid))
  2624. return;
  2625. // add to internal (fast) cache
  2626. $this->icache['message'][$headers->uid] = clone $headers;
  2627. $this->icache['message'][$headers->uid]->structure = $struct;
  2628. // no further caching
  2629. if (!$this->caching_enabled)
  2630. return;
  2631. // check for an existing record (probly headers are cached but structure not)
  2632. if (!$force) {
  2633. $sql_result = $this->db->query(
  2634. "SELECT message_id".
  2635. " FROM ".get_table_name('messages').
  2636. " WHERE user_id=?".
  2637. " AND cache_key=?".
  2638. " AND uid=?",
  2639. $_SESSION['user_id'],
  2640. $key,
  2641. $headers->uid);
  2642. if ($sql_arr = $this->db->fetch_assoc($sql_result))
  2643. $message_id = $sql_arr['message_id'];
  2644. }
  2645. // update cache record
  2646. if ($message_id) {
  2647. $this->db->query(
  2648. "UPDATE ".get_table_name('messages').
  2649. " SET idx=?, headers=?, structure=?".
  2650. " WHERE message_id=?",
  2651. $index,
  2652. serialize($this->db->encode(clone $headers)),
  2653. is_object($struct) ? serialize($this->db->encode(clone $struct)) : NULL,
  2654. $message_id
  2655. );
  2656. }
  2657. else { // insert new record
  2658. $this->db->query(
  2659. "INSERT INTO ".get_table_name('messages').
  2660. " (user_id, del, cache_key, created, idx, uid, subject, ".
  2661. $this->db->quoteIdentifier('from').", ".
  2662. $this->db->quoteIdentifier('to').", ".
  2663. "cc, date, size, headers, structure)".
  2664. " VALUES (?, 0, ?, ".$this->db->now().", ?, ?, ?, ?, ?, ?, ".
  2665. $this->db->fromunixtime($headers->timestamp).", ?, ?, ?)",
  2666. $_SESSION['user_id'],
  2667. $key,
  2668. $index,
  2669. $headers->uid,
  2670. (string)mb_substr($this->db->encode($this->decode_header($headers->subject, true)), 0, 128),
  2671. (string)mb_substr($this->db->encode($this->decode_header($headers->from, true)), 0, 128),
  2672. (string)mb_substr($this->db->encode($this->decode_header($headers->to, true)), 0, 128),
  2673. (string)mb_substr($this->db->encode($this->decode_header($headers->cc, true)), 0, 128),
  2674. (int)$headers->size,
  2675. serialize($this->db->encode(clone $headers)),
  2676. is_object($struct) ? serialize($this->db->encode(clone $struct)) : NULL
  2677. );
  2678. }
  2679. }
  2680. /**
  2681. * @access private
  2682. */
  2683. private function remove_message_cache($key, $ids, $idx=false)
  2684. {
  2685. if (!$this->caching_enabled)
  2686. return;
  2687. $this->db->query(
  2688. "DELETE FROM ".get_table_name('messages').
  2689. " WHERE user_id=?".
  2690. " AND cache_key=?".
  2691. " AND ".($idx ? "idx" : "uid")." IN (".$this->db->array2list($ids, 'integer').")",
  2692. $_SESSION['user_id'],
  2693. $key);
  2694. }
  2695. /**
  2696. * @access private
  2697. */
  2698. private function clear_message_cache($key, $start_index=1)
  2699. {
  2700. if (!$this->caching_enabled)
  2701. return;
  2702. $this->db->query(
  2703. "DELETE FROM ".get_table_name('messages').
  2704. " WHERE user_id=?".
  2705. " AND cache_key=?".
  2706. " AND idx>=?",
  2707. $_SESSION['user_id'], $key, $start_index);
  2708. }
  2709. /**
  2710. * @access private
  2711. */
  2712. private function get_message_cache_index_min($key, $uids=NULL)
  2713. {
  2714. if (!$this->caching_enabled)
  2715. return;
  2716. if (!empty($uids) && !is_array($uids)) {
  2717. if ($uids == '*' || $uids == '1:*')
  2718. $uids = NULL;
  2719. else
  2720. $uids = explode(',', $uids);
  2721. }
  2722. $sql_result = $this->db->query(
  2723. "SELECT MIN(idx) AS minidx".
  2724. " FROM ".get_table_name('messages').
  2725. " WHERE user_id=?".
  2726. " AND cache_key=?"
  2727. .(!empty($uids) ? " AND uid IN (".$this->db->array2list($uids, 'integer').")" : ''),
  2728. $_SESSION['user_id'],
  2729. $key);
  2730. if ($sql_arr = $this->db->fetch_assoc($sql_result))
  2731. return $sql_arr['minidx'];
  2732. else
  2733. return 0;
  2734. }
  2735. /* --------------------------------
  2736. * encoding/decoding methods
  2737. * --------------------------------*/
  2738. /**
  2739. * Split an address list into a structured array list
  2740. *
  2741. * @param string Input string
  2742. * @param int List only this number of addresses
  2743. * @param boolean Decode address strings
  2744. * @return array Indexed list of addresses
  2745. */
  2746. function decode_address_list($input, $max=null, $decode=true)
  2747. {
  2748. $a = $this->_parse_address_list($input, $decode);
  2749. $out = array();
  2750. // Special chars as defined by RFC 822 need to in quoted string (or escaped).
  2751. $special_chars = '[\(\)\<\>\\\.\[\]@,;:"]';
  2752. if (!is_array($a))
  2753. return $out;
  2754. $c = count($a);
  2755. $j = 0;
  2756. foreach ($a as $val) {
  2757. $j++;
  2758. $address = trim($val['address']);
  2759. $name = trim($val['name']);
  2760. if (preg_match('/^[\'"]/', $name) && preg_match('/[\'"]$/', $name))
  2761. $name = preg_replace(array('/^[\'"]/', '/[\'"]$/'), '', $name);
  2762. if ($name && $address && $name != $address)
  2763. $string = sprintf('%s <%s>', preg_match("/$special_chars/", $name) ? '"'.addcslashes($name, '"').'"' : $name, $address);
  2764. else if ($address)
  2765. $string = $address;
  2766. else if ($name)
  2767. $string = $name;
  2768. $out[$j] = array('name' => $name,
  2769. 'mailto' => $address,
  2770. 'string' => $string
  2771. );
  2772. if ($max && $j==$max)
  2773. break;
  2774. }
  2775. return $out;
  2776. }
  2777. /**
  2778. * Decode a Microsoft Outlook TNEF part (winmail.dat)
  2779. *
  2780. * @param object crystal_message_part Message part to decode
  2781. * @param string UID of the message
  2782. * @return array List of crystal_message_parts extracted from windmail.dat
  2783. */
  2784. function tnef_decode(&$part, $uid)
  2785. {
  2786. if (!isset($part->body))
  2787. $part->body = $this->get_message_part($uid, $part->mime_id, $part);
  2788. require_once('lib/tnef_decoder.inc');
  2789. $pid = 0;
  2790. $tnef_parts = array();
  2791. $tnef_arr = tnef_decode($part->body);
  2792. foreach ($tnef_arr as $winatt) {
  2793. $tpart = new crystal_message_part;
  2794. $tpart->filename = $winatt["name"];
  2795. $tpart->encoding = 'stream';
  2796. $tpart->ctype_primary = $winatt["type0"];
  2797. $tpart->ctype_secondary = $winatt["type1"];
  2798. $tpart->mimetype = strtolower($winatt["type0"] . "/" . $winatt["type1"]);
  2799. $tpart->mime_id = "winmail." . $part->mime_id . ".$pid";
  2800. $tpart->size = $winatt["size"];
  2801. $tpart->body = $winatt['stream'];
  2802. $tnef_parts[] = $tpart;
  2803. $pid++;
  2804. }
  2805. return $tnef_parts;
  2806. }
  2807. /**
  2808. * Decode a message header value
  2809. *
  2810. * @param string Header value
  2811. * @param boolean Remove quotes if necessary
  2812. * @return string Decoded string
  2813. */
  2814. function decode_header($input, $remove_quotes=false)
  2815. {
  2816. $str = crystal_imap::decode_mime_string((string)$input, $this->default_charset);
  2817. if ($str{0}=='"' && $remove_quotes)
  2818. $str = str_replace('"', '', $str);
  2819. return $str;
  2820. }
  2821. /**
  2822. * Decode a mime-encoded string to internal charset
  2823. *
  2824. * @param string $input Header value
  2825. * @param string $fallback Fallback charset if none specified
  2826. *
  2827. * @return string Decoded string
  2828. * @static
  2829. */
  2830. public static function decode_mime_string($input, $fallback=null)
  2831. {
  2832. // Initialize variable
  2833. $out = '';
  2834. // Iterate instead of recursing, this way if there are too many values we don't have stack overflows
  2835. // rfc: all line breaks or other characters not found
  2836. // in the Base64 Alphabet must be ignored by decoding software
  2837. // delete all blanks between MIME-lines, differently we can
  2838. // receive unnecessary blanks and broken utf-8 symbols
  2839. $input = preg_replace("/\?=\s+=\?/", '?==?', $input);
  2840. // Check if there is stuff to decode
  2841. if (strpos($input, '=?') !== false) {
  2842. // Loop through the string to decode all occurences of =? ?= into the variable $out
  2843. while(($pos = strpos($input, '=?')) !== false) {
  2844. // Append everything that is before the text to be decoded
  2845. $out .= substr($input, 0, $pos);
  2846. // Get the location of the text to decode
  2847. $end_cs_pos = strpos($input, "?", $pos+2);
  2848. $end_en_pos = strpos($input, "?", $end_cs_pos+1);
  2849. $end_pos = strpos($input, "?=", $end_en_pos+1);
  2850. // Extract the encoded string
  2851. $encstr = substr($input, $pos+2, ($end_pos-$pos-2));
  2852. // Extract the remaining string
  2853. $input = substr($input, $end_pos+2);
  2854. // Decode the string fragement
  2855. $out .= crystal_imap::_decode_mime_string_part($encstr);
  2856. }
  2857. // Deocde the rest (if any)
  2858. if (strlen($input) != 0)
  2859. $out .= crystal_imap::decode_mime_string($input, $fallback);
  2860. // return the results
  2861. return $out;
  2862. }
  2863. // no encoding information, use fallback
  2864. return crystal_charset_convert($input,
  2865. !empty($fallback) ? $fallback : cmail::get_instance()->config->get('default_charset', 'ISO-8859-1'));
  2866. }
  2867. /**
  2868. * Decode a part of a mime-encoded string
  2869. *
  2870. * @access private
  2871. */
  2872. private function _decode_mime_string_part($str)
  2873. {
  2874. $a = explode('?', $str);
  2875. $count = count($a);
  2876. // should be in format "charset?encoding?base64_string"
  2877. if ($count >= 3) {
  2878. for ($i=2; $i<$count; $i++)
  2879. $rest .= $a[$i];
  2880. if (($a[1]=='B') || ($a[1]=='b'))
  2881. $rest = base64_decode($rest);
  2882. else if (($a[1]=='Q') || ($a[1]=='q')) {
  2883. $rest = str_replace('_', ' ', $rest);
  2884. $rest = quoted_printable_decode($rest);
  2885. }
  2886. return crystal_charset_convert($rest, $a[0]);
  2887. }
  2888. // we dont' know what to do with this
  2889. return $str;
  2890. }
  2891. /**
  2892. * Decode a mime part
  2893. *
  2894. * @param string Input string
  2895. * @param string Part encoding
  2896. * @return string Decoded string
  2897. */
  2898. function mime_decode($input, $encoding='7bit')
  2899. {
  2900. switch (strtolower($encoding)) {
  2901. case 'quoted-printable':
  2902. return quoted_printable_decode($input);
  2903. case 'base64':
  2904. return base64_decode($input);
  2905. case 'x-uuencode':
  2906. case 'x-uue':
  2907. case 'uue':
  2908. case 'uuencode':
  2909. return convert_uudecode($input);
  2910. case '7bit':
  2911. default:
  2912. return $input;
  2913. }
  2914. }
  2915. /**
  2916. * Convert body charset to cmail_CHARSET according to the ctype_parameters
  2917. *
  2918. * @param string Part body to decode
  2919. * @param string Charset to convert from
  2920. * @return string Content converted to internal charset
  2921. */
  2922. function charset_decode($body, $ctype_param)
  2923. {
  2924. if (is_array($ctype_param) && !empty($ctype_param['charset']))
  2925. return crystal_charset_convert($body, $ctype_param['charset']);
  2926. // defaults to what is specified in the class header
  2927. return crystal_charset_convert($body, $this->default_charset);
  2928. }
  2929. /* --------------------------------
  2930. * private methods
  2931. * --------------------------------*/
  2932. /**
  2933. * Validate the given input and save to local properties
  2934. * @access private
  2935. */
  2936. private function _set_sort_order($sort_field, $sort_order)
  2937. {
  2938. if ($sort_field != null)
  2939. $this->sort_field = asciiwords($sort_field);
  2940. if ($sort_order != null)
  2941. $this->sort_order = strtoupper($sort_order) == 'DESC' ? 'DESC' : 'ASC';
  2942. }
  2943. /**
  2944. * Sort mailboxes first by default folders and then in alphabethical order
  2945. * @access private
  2946. */
  2947. private function _sort_mailbox_list($a_folders)
  2948. {
  2949. $a_out = $a_defaults = $folders = array();
  2950. $delimiter = $this->get_hierarchy_delimiter();
  2951. // find default folders and skip folders starting with '.'
  2952. foreach ($a_folders as $i => $folder) {
  2953. if ($folder[0] == '.')
  2954. continue;
  2955. if (($p = array_search($folder, $this->default_folders)) !== false && !$a_defaults[$p])
  2956. $a_defaults[$p] = $folder;
  2957. else
  2958. $folders[$folder] = crystal_charset_convert($folder, 'UTF7-IMAP');
  2959. }
  2960. // sort folders and place defaults on the top
  2961. asort($folders, SORT_LOCALE_STRING);
  2962. ksort($a_defaults);
  2963. $folders = array_merge($a_defaults, array_keys($folders));
  2964. // finally we must rebuild the list to move
  2965. // subfolders of default folders to their place...
  2966. // ...also do this for the rest of folders because
  2967. // asort() is not properly sorting case sensitive names
  2968. while (list($key, $folder) = each($folders)) {
  2969. // set the type of folder name variable (#1485527)
  2970. $a_out[] = (string) $folder;
  2971. unset($folders[$key]);
  2972. $this->_rsort($folder, $delimiter, $folders, $a_out);
  2973. }
  2974. return $a_out;
  2975. }
  2976. /**
  2977. * @access private
  2978. */
  2979. private function _rsort($folder, $delimiter, &$list, &$out)
  2980. {
  2981. while (list($key, $name) = each($list)) {
  2982. if (strpos($name, $folder.$delimiter) === 0) {
  2983. // set the type of folder name variable (#1485527)
  2984. $out[] = (string) $name;
  2985. unset($list[$key]);
  2986. $this->_rsort($name, $delimiter, $list, $out);
  2987. }
  2988. }
  2989. reset($list);
  2990. }
  2991. /**
  2992. * @access private
  2993. */
  2994. private function _uid2id($uid, $mbox_name=NULL)
  2995. {
  2996. if (!$mbox_name)
  2997. $mbox_name = $this->mailbox;
  2998. if (!isset($this->uid_id_map[$mbox_name][$uid]))
  2999. $this->uid_id_map[$mbox_name][$uid] = $this->conn->UID2ID($mbox_name, $uid);
  3000. return $this->uid_id_map[$mbox_name][$uid];
  3001. }
  3002. /**
  3003. * @access private
  3004. */
  3005. private function _id2uid($id, $mbox_name=NULL)
  3006. {
  3007. if (!$mbox_name)
  3008. $mbox_name = $this->mailbox;
  3009. if ($uid = array_search($id, (array)$this->uid_id_map[$mbox_name]))
  3010. return $uid;
  3011. $uid = $this->conn->ID2UID($mbox_name, $id);
  3012. $this->uid_id_map[$mbox_name][$uid] = $id;
  3013. return $uid;
  3014. }
  3015. /**
  3016. * Subscribe/unsubscribe a list of mailboxes and update local cache
  3017. * @access private
  3018. */
  3019. private function _change_subscription($a_mboxes, $mode)
  3020. {
  3021. $updated = false;
  3022. if (is_array($a_mboxes))
  3023. foreach ($a_mboxes as $i => $mbox_name) {
  3024. $mailbox = $this->mod_mailbox($mbox_name);
  3025. $a_mboxes[$i] = $mailbox;
  3026. if ($mode=='subscribe')
  3027. $updated = $this->conn->subscribe($mailbox);
  3028. else if ($mode=='unsubscribe')
  3029. $updated = $this->conn->unsubscribe($mailbox);
  3030. }
  3031. // get cached mailbox list
  3032. if ($updated) {
  3033. $a_mailbox_cache = $this->get_cache('mailboxes');
  3034. if (!is_array($a_mailbox_cache))
  3035. return $updated;
  3036. // modify cached list
  3037. if ($mode=='subscribe')
  3038. $a_mailbox_cache = array_merge($a_mailbox_cache, $a_mboxes);
  3039. else if ($mode=='unsubscribe')
  3040. $a_mailbox_cache = array_diff($a_mailbox_cache, $a_mboxes);
  3041. // write mailboxlist to cache
  3042. $this->update_cache('mailboxes', $this->_sort_mailbox_list($a_mailbox_cache));
  3043. }
  3044. return $updated;
  3045. }
  3046. /**
  3047. * Increde/decrese messagecount for a specific mailbox
  3048. * @access private
  3049. */
  3050. private function _set_messagecount($mbox_name, $mode, $increment)
  3051. {
  3052. $a_mailbox_cache = false;
  3053. $mailbox = $mbox_name ? $mbox_name : $this->mailbox;
  3054. $mode = strtoupper($mode);
  3055. $a_mailbox_cache = $this->get_cache('messagecount');
  3056. if (!is_array($a_mailbox_cache[$mailbox]) || !isset($a_mailbox_cache[$mailbox][$mode]) || !is_numeric($increment))
  3057. return false;
  3058. // add incremental value to messagecount
  3059. $a_mailbox_cache[$mailbox][$mode] += $increment;
  3060. // there's something wrong, delete from cache
  3061. if ($a_mailbox_cache[$mailbox][$mode] < 0)
  3062. unset($a_mailbox_cache[$mailbox][$mode]);
  3063. // write back to cache
  3064. $this->update_cache('messagecount', $a_mailbox_cache);
  3065. return true;
  3066. }
  3067. /**
  3068. * Remove messagecount of a specific mailbox from cache
  3069. * @access private
  3070. */
  3071. private function _clear_messagecount($mbox_name='')
  3072. {
  3073. $a_mailbox_cache = false;
  3074. $mailbox = $mbox_name ? $mbox_name : $this->mailbox;
  3075. $a_mailbox_cache = $this->get_cache('messagecount');
  3076. if (is_array($a_mailbox_cache[$mailbox])) {
  3077. unset($a_mailbox_cache[$mailbox]);
  3078. $this->update_cache('messagecount', $a_mailbox_cache);
  3079. }
  3080. }
  3081. /**
  3082. * Split RFC822 header string into an associative array
  3083. * @access private
  3084. */
  3085. private function _parse_headers($headers)
  3086. {
  3087. $a_headers = array();
  3088. $headers = preg_replace('/\r?\n(\t| )+/', ' ', $headers);
  3089. $lines = explode("\n", $headers);
  3090. $c = count($lines);
  3091. for ($i=0; $i<$c; $i++) {
  3092. if ($p = strpos($lines[$i], ': ')) {
  3093. $field = strtolower(substr($lines[$i], 0, $p));
  3094. $value = trim(substr($lines[$i], $p+1));
  3095. if (!empty($value))
  3096. $a_headers[$field] = $value;
  3097. }
  3098. }
  3099. return $a_headers;
  3100. }
  3101. /**
  3102. * @access private
  3103. */
  3104. private function _parse_address_list($str, $decode=true)
  3105. {
  3106. // remove any newlines and carriage returns before
  3107. $a = crystal_explode_quoted_string('[,;]', preg_replace( "/[\r\n]/", " ", $str));
  3108. $result = array();
  3109. foreach ($a as $key => $val) {
  3110. $val = preg_replace("/([\"\w])</", "$1 <", $val);
  3111. $sub_a = crystal_explode_quoted_string(' ', $decode ? $this->decode_header($val) : $val);
  3112. $result[$key]['name'] = '';
  3113. foreach ($sub_a as $k => $v) {
  3114. // use angle brackets in regexp to not handle names with @ sign
  3115. if (preg_match('/^<\S+@\S+>$/', $v))
  3116. $result[$key]['address'] = trim($v, '<>');
  3117. else
  3118. $result[$key]['name'] .= (empty($result[$key]['name'])?'':' ').str_replace("\"",'',stripslashes($v));
  3119. }
  3120. if (empty($result[$key]['name']))
  3121. $result[$key]['name'] = $result[$key]['address'];
  3122. elseif (empty($result[$key]['address']))
  3123. $result[$key]['address'] = $result[$key]['name'];
  3124. }
  3125. return $result;
  3126. }
  3127. } // end class crystal_imap
  3128. /**
  3129. * Class representing a message part
  3130. *
  3131. * @package Mail
  3132. */
  3133. class crystal_message_part
  3134. {
  3135. var $mime_id = '';
  3136. var $ctype_primary = 'text';
  3137. var $ctype_secondary = 'plain';
  3138. var $mimetype = 'text/plain';
  3139. var $disposition = '';
  3140. var $filename = '';
  3141. var $encoding = '8bit';
  3142. var $charset = '';
  3143. var $size = 0;
  3144. var $headers = array();
  3145. var $d_parameters = array();
  3146. var $ctype_parameters = array();
  3147. function __clone()
  3148. {
  3149. if (isset($this->parts))
  3150. foreach ($this->parts as $idx => $part)
  3151. if (is_object($part))
  3152. $this->parts[$idx] = clone $part;
  3153. }
  3154. }
  3155. /**
  3156. * Class for sorting an array of crystal_mail_header objects in a predetermined order.
  3157. *
  3158. * @package Mail
  3159. * @author Eric Stadtherr
  3160. */
  3161. class crystal_header_sorter
  3162. {
  3163. var $sequence_numbers = array();
  3164. /**
  3165. * Set the predetermined sort order.
  3166. *
  3167. * @param array Numerically indexed array of IMAP message sequence numbers
  3168. */
  3169. function set_sequence_numbers($seqnums)
  3170. {
  3171. $this->sequence_numbers = array_flip($seqnums);
  3172. }
  3173. /**
  3174. * Sort the array of header objects
  3175. *
  3176. * @param array Array of crystal_mail_header objects indexed by UID
  3177. */
  3178. function sort_headers(&$headers)
  3179. {
  3180. /*
  3181. * uksort would work if the keys were the sequence number, but unfortunately
  3182. * the keys are the UIDs. We'll use uasort instead and dereference the value
  3183. * to get the sequence number (in the "id" field).
  3184. *
  3185. * uksort($headers, array($this, "compare_seqnums"));
  3186. */
  3187. uasort($headers, array($this, "compare_seqnums"));
  3188. }
  3189. /**
  3190. * Sort method called by uasort()
  3191. */
  3192. function compare_seqnums($a, $b)
  3193. {
  3194. // First get the sequence number from the header object (the 'id' field).
  3195. $seqa = $a->id;
  3196. $seqb = $b->id;
  3197. // then find each sequence number in my ordered list
  3198. $posa = isset($this->sequence_numbers[$seqa]) ? intval($this->sequence_numbers[$seqa]) : -1;
  3199. $posb = isset($this->sequence_numbers[$seqb]) ? intval($this->sequence_numbers[$seqb]) : -1;
  3200. // return the relative position as the comparison value
  3201. return $posa - $posb;
  3202. }
  3203. }