4 +-----------------------------------------------------------------------+
5 | program/include/rcube_imap.php |
7 | This file is part of the Roundcube Webmail client |
8 | Copyright (C) 2005-2011, The Roundcube Dev Team |
9 | Copyright (C) 2011, Kolab Systems AG |
10 | Licensed under the GNU GPL |
15 +-----------------------------------------------------------------------+
16 | Author: Thomas Bruederli <roundcube@gmail.com> |
17 | Author: Aleksander Machniak <alec@alec.pl> |
18 +-----------------------------------------------------------------------+
20 $Id: rcube_imap.php 5638 2011-12-21 18:57:01Z alec $
26 * Interface class for accessing an IMAP server
29 * @author Thomas Bruederli <roundcube@gmail.com>
30 * @author Aleksander Machniak <alec@alec.pl>
35 public $skip_deleted = false;
36 public $page_size = 10;
37 public $list_page = 1;
38 public $threading = false;
39 public $fetch_add_headers = '';
40 public $get_all_headers = false;
43 * Instance of rcube_imap_generic
45 * @var rcube_imap_generic
50 * Instance of rcube_imap_cache
52 * @var rcube_imap_cache
57 * Instance of rcube_cache
64 * Internal (in-memory) cache
68 private $icache = array();
70 private $mailbox = 'INBOX';
71 private $delimiter = NULL;
72 private $namespace = NULL;
73 private $sort_field = '';
74 private $sort_order = 'DESC';
75 private $default_charset = 'ISO-8859-1';
76 private $struct_charset = NULL;
77 private $default_folders = array('INBOX');
78 private $uid_id_map = array();
79 private $msg_headers = array();
80 public $search_set = NULL;
81 public $search_string = '';
82 private $search_charset = '';
83 private $search_sort_field = '';
84 private $search_threads = false;
85 private $search_sorted = false;
86 private $options = array('auth_method' => 'check');
87 private $host, $user, $pass, $port, $ssl;
88 private $caching = false;
89 private $messages_caching = false;
92 * All (additional) headers used (in any way) by Roundcube
93 * Not listed here: DATE, FROM, TO, CC, REPLY-TO, SUBJECT, CONTENT-TYPE, LIST-POST
94 * (used for messages listing) are hardcoded in rcube_imap_generic::fetchHeaders()
97 * @see rcube_imap::fetch_add_headers
99 private $all_headers = array(
103 'CONTENT-TRANSFER-ENCODING',
117 const ALREADYEXISTS = 6;
118 const NONEXISTENT = 7;
119 const CONTACTADMIN = 8;
123 * Object constructor.
125 function __construct()
127 $this->conn = new rcube_imap_generic();
129 // Set namespace and delimiter from session,
130 // so some methods would work before connection
131 if (isset($_SESSION['imap_namespace']))
132 $this->namespace = $_SESSION['imap_namespace'];
133 if (isset($_SESSION['imap_delimiter']))
134 $this->delimiter = $_SESSION['imap_delimiter'];
139 * Connect to an IMAP server
141 * @param string $host Host to connect
142 * @param string $user Username for IMAP account
143 * @param string $pass Password for IMAP account
144 * @param integer $port Port to connect to
145 * @param string $use_ssl SSL schema (either ssl or tls) or null if plain connection
146 * @return boolean TRUE on success, FALSE on failure
149 function connect($host, $user, $pass, $port=143, $use_ssl=null)
151 // check for OpenSSL support in PHP build
152 if ($use_ssl && extension_loaded('openssl'))
153 $this->options['ssl_mode'] = $use_ssl == 'imaps' ? 'ssl' : $use_ssl;
155 raise_error(array('code' => 403, 'type' => 'imap',
156 'file' => __FILE__, 'line' => __LINE__,
157 'message' => "OpenSSL not available"), true, false);
161 $this->options['port'] = $port;
163 if ($this->options['debug']) {
164 $this->conn->setDebug(true, array($this, 'debug_handler'));
166 $this->options['ident'] = array(
167 'name' => 'Roundcube Webmail',
168 'version' => RCMAIL_VERSION,
169 'php' => PHP_VERSION,
171 'command' => $_SERVER['REQUEST_URI'],
177 $data = rcmail::get_instance()->plugins->exec_hook('imap_connect',
178 array_merge($this->options, array('host' => $host, 'user' => $user,
179 'attempt' => ++$attempt)));
181 if (!empty($data['pass']))
182 $pass = $data['pass'];
184 $this->conn->connect($data['host'], $data['user'], $pass, $data);
185 } while(!$this->conn->connected() && $data['retry']);
187 $this->host = $data['host'];
188 $this->user = $data['user'];
191 $this->ssl = $use_ssl;
193 if ($this->conn->connected()) {
194 // get namespace and delimiter
199 else if ($this->conn->error) {
200 if ($pass && $user) {
201 $message = sprintf("Login failed for %s from %s. %s",
202 $user, rcmail_remote_ip(), $this->conn->error);
204 raise_error(array('code' => 403, 'type' => 'imap',
205 'file' => __FILE__, 'line' => __LINE__,
206 'message' => $message), true, false);
215 * Close IMAP connection
216 * Usually done on script shutdown
222 $this->conn->closeConnection();
224 $this->mcache->close();
229 * Close IMAP connection and re-connect
230 * This is used to avoid some strange socket errors when talking to Courier IMAP
236 $this->conn->closeConnection();
237 $connected = $this->connect($this->host, $this->user, $this->pass, $this->port, $this->ssl);
239 // issue SELECT command to restore connection status
240 if ($connected && strlen($this->mailbox))
241 $this->conn->select($this->mailbox);
246 * Returns code of last error
248 * @return int Error code
250 function get_error_code()
252 return $this->conn->errornum;
257 * Returns message of last error
259 * @return string Error message
261 function get_error_str()
263 return $this->conn->error;
268 * Returns code of last command response
270 * @return int Response code
272 function get_response_code()
274 switch ($this->conn->resultcode) {
278 return self::READONLY;
280 return self::TRYCREATE;
284 return self::OVERQUOTA;
285 case 'ALREADYEXISTS':
286 return self::ALREADYEXISTS;
288 return self::NONEXISTENT;
290 return self::CONTACTADMIN;
292 return self::UNKNOWN;
298 * Returns last command response
300 * @return string Response
302 function get_response_str()
304 return $this->conn->result;
309 * Set options to be used in rcube_imap_generic::connect()
311 * @param array $opt Options array
313 function set_options($opt)
315 $this->options = array_merge($this->options, (array)$opt);
320 * Activate/deactivate debug mode
322 * @param boolean $dbg True if IMAP conversation should be logged
325 function set_debug($dbg = true)
327 $this->options['debug'] = $dbg;
328 $this->conn->setDebug($dbg, array($this, 'debug_handler'));
333 * Set default message charset
335 * This will be used for message decoding if a charset specification is not available
337 * @param string $cs Charset string
340 function set_charset($cs)
342 $this->default_charset = $cs;
347 * This list of folders will be listed above all other folders
349 * @param array $arr Indexed list of folder names
352 function set_default_mailboxes($arr)
354 if (is_array($arr)) {
355 $this->default_folders = $arr;
357 // add inbox if not included
358 if (!in_array('INBOX', $this->default_folders))
359 array_unshift($this->default_folders, 'INBOX');
365 * Set internal mailbox reference.
367 * All operations will be perfomed on this mailbox/folder
369 * @param string $mailbox Mailbox/Folder name
372 function set_mailbox($mailbox)
374 if ($this->mailbox == $mailbox)
377 $this->mailbox = $mailbox;
379 // clear messagecount cache for this mailbox
380 $this->_clear_messagecount($mailbox);
385 * Forces selection of a mailbox
387 * @param string $mailbox Mailbox/Folder name
390 function select_mailbox($mailbox=null)
392 if (!strlen($mailbox)) {
393 $mailbox = $this->mailbox;
396 $selected = $this->conn->select($mailbox);
398 if ($selected && $this->mailbox != $mailbox) {
399 // clear messagecount cache for this mailbox
400 $this->_clear_messagecount($mailbox);
401 $this->mailbox = $mailbox;
407 * Set internal list page
409 * @param number $page Page number to list
412 function set_page($page)
414 $this->list_page = (int)$page;
419 * Set internal page size
421 * @param number $size Number of messages to display on one page
424 function set_pagesize($size)
426 $this->page_size = (int)$size;
431 * Save a set of message ids for future message listing methods
433 * @param string IMAP Search query
434 * @param array List of message ids or NULL if empty
435 * @param string Charset of search string
436 * @param string Sorting field
437 * @param string True if set is sorted (SORT was used for searching)
439 function set_search_set($str=null, $msgs=null, $charset=null, $sort_field=null, $threads=false, $sorted=false)
441 if (is_array($str) && $msgs == null)
442 list($str, $msgs, $charset, $sort_field, $threads, $sorted) = $str;
445 else if ($msgs != null && !is_array($msgs))
446 $msgs = explode(',', $msgs);
448 $this->search_string = $str;
449 $this->search_set = $msgs;
450 $this->search_charset = $charset;
451 $this->search_sort_field = $sort_field;
452 $this->search_threads = $threads;
453 $this->search_sorted = $sorted;
458 * Return the saved search set as hash array
459 * @return array Search set
461 function get_search_set()
463 return array($this->search_string,
465 $this->search_charset,
466 $this->search_sort_field,
467 $this->search_threads,
468 $this->search_sorted,
474 * Returns the currently used mailbox name
476 * @return string Name of the mailbox/folder
479 function get_mailbox_name()
481 return $this->mailbox;
486 * Returns the IMAP server's capability
488 * @param string $cap Capability name
489 * @return mixed Capability value or TRUE if supported, FALSE if not
492 function get_capability($cap)
494 return $this->conn->getCapability(strtoupper($cap));
499 * Sets threading flag to the best supported THREAD algorithm
501 * @param boolean $enable TRUE to enable and FALSE
502 * @return string Algorithm or false if THREAD is not supported
505 function set_threading($enable=false)
507 $this->threading = false;
509 if ($enable && ($caps = $this->get_capability('THREAD'))) {
510 if (in_array('REFS', $caps))
511 $this->threading = 'REFS';
512 else if (in_array('REFERENCES', $caps))
513 $this->threading = 'REFERENCES';
514 else if (in_array('ORDEREDSUBJECT', $caps))
515 $this->threading = 'ORDEREDSUBJECT';
518 return $this->threading;
523 * Checks the PERMANENTFLAGS capability of the current mailbox
524 * and returns true if the given flag is supported by the IMAP server
526 * @param string $flag Permanentflag name
527 * @return boolean True if this flag is supported
530 function check_permflag($flag)
532 $flag = strtoupper($flag);
533 $imap_flag = $this->conn->flags[$flag];
534 return (in_array_nocase($imap_flag, $this->conn->data['PERMANENTFLAGS']));
539 * Returns the delimiter that is used by the IMAP server for folder separation
541 * @return string Delimiter string
544 function get_hierarchy_delimiter()
546 return $this->delimiter;
553 * @param string $name Namespace array index: personal, other, shared, prefix
555 * @return array Namespace data
558 function get_namespace($name=null)
560 $ns = $this->namespace;
563 return isset($ns[$name]) ? $ns[$name] : null;
566 unset($ns['prefix']);
572 * Sets delimiter and namespaces
576 private function set_env()
578 if ($this->delimiter !== null && $this->namespace !== null) {
582 $config = rcmail::get_instance()->config;
583 $imap_personal = $config->get('imap_ns_personal');
584 $imap_other = $config->get('imap_ns_other');
585 $imap_shared = $config->get('imap_ns_shared');
586 $imap_delimiter = $config->get('imap_delimiter');
588 if (!$this->conn->connected())
591 $ns = $this->conn->getNamespace();
593 // Set namespaces (NAMESPACE supported)
595 $this->namespace = $ns;
598 $this->namespace = array(
605 if ($imap_delimiter) {
606 $this->delimiter = $imap_delimiter;
608 if (empty($this->delimiter)) {
609 $this->delimiter = $this->namespace['personal'][0][1];
611 if (empty($this->delimiter)) {
612 $this->delimiter = $this->conn->getHierarchyDelimiter();
614 if (empty($this->delimiter)) {
615 $this->delimiter = '/';
618 // Overwrite namespaces
619 if ($imap_personal !== null) {
620 $this->namespace['personal'] = NULL;
621 foreach ((array)$imap_personal as $dir) {
622 $this->namespace['personal'][] = array($dir, $this->delimiter);
625 if ($imap_other !== null) {
626 $this->namespace['other'] = NULL;
627 foreach ((array)$imap_other as $dir) {
629 $this->namespace['other'][] = array($dir, $this->delimiter);
633 if ($imap_shared !== null) {
634 $this->namespace['shared'] = NULL;
635 foreach ((array)$imap_shared as $dir) {
637 $this->namespace['shared'][] = array($dir, $this->delimiter);
642 // Find personal namespace prefix for mod_mailbox()
643 // Prefix can be removed when there is only one personal namespace
644 if (is_array($this->namespace['personal']) && count($this->namespace['personal']) == 1) {
645 $this->namespace['prefix'] = $this->namespace['personal'][0][0];
648 $_SESSION['imap_namespace'] = $this->namespace;
649 $_SESSION['imap_delimiter'] = $this->delimiter;
654 * Get message count for a specific mailbox
656 * @param string $mailbox Mailbox/folder name
657 * @param string $mode Mode for count [ALL|THREADS|UNSEEN|RECENT]
658 * @param boolean $force Force reading from server and update cache
659 * @param boolean $status Enables storing folder status info (max UID/count),
660 * required for mailbox_status()
661 * @return int Number of messages
664 function messagecount($mailbox='', $mode='ALL', $force=false, $status=true)
666 if (!strlen($mailbox)) {
667 $mailbox = $this->mailbox;
670 return $this->_messagecount($mailbox, $mode, $force, $status);
675 * Private method for getting nr of messages
677 * @param string $mailbox Mailbox name
678 * @param string $mode Mode for count [ALL|THREADS|UNSEEN|RECENT]
679 * @param boolean $force Force reading from server and update cache
680 * @param boolean $status Enables storing folder status info (max UID/count),
681 * required for mailbox_status()
682 * @return int Number of messages
684 * @see rcube_imap::messagecount()
686 private function _messagecount($mailbox, $mode='ALL', $force=false, $status=true)
688 $mode = strtoupper($mode);
691 if ($this->search_string && $mailbox == $this->mailbox && ($mode == 'ALL' || $mode == 'THREADS') && !$force) {
692 if ($this->search_threads)
693 return $mode == 'ALL' ? count((array)$this->search_set['depth']) : count((array)$this->search_set['tree']);
695 return count((array)$this->search_set);
698 $a_mailbox_cache = $this->get_cache('messagecount');
700 // return cached value
701 if (!$force && is_array($a_mailbox_cache[$mailbox]) && isset($a_mailbox_cache[$mailbox][$mode]))
702 return $a_mailbox_cache[$mailbox][$mode];
704 if (!is_array($a_mailbox_cache[$mailbox]))
705 $a_mailbox_cache[$mailbox] = array();
707 if ($mode == 'THREADS') {
708 $res = $this->_threadcount($mailbox, $msg_count);
709 $count = $res['count'];
712 $this->set_folder_stats($mailbox, 'cnt', $res['msgcount']);
713 $this->set_folder_stats($mailbox, 'maxuid', $res['maxuid'] ? $this->id2uid($res['maxuid'], $mailbox) : 0);
716 // RECENT count is fetched a bit different
717 else if ($mode == 'RECENT') {
718 $count = $this->conn->countRecent($mailbox);
720 // use SEARCH for message counting
721 else if ($this->skip_deleted) {
722 $search_str = "ALL UNDELETED";
723 $keys = array('COUNT');
726 if ($mode == 'UNSEEN') {
727 $search_str .= " UNSEEN";
730 if ($this->messages_caching) {
739 // get message count using (E)SEARCH
740 // not very performant but more precise (using UNDELETED)
741 $index = $this->conn->search($mailbox, $search_str, $need_uid, $keys);
743 $count = is_array($index) ? $index['COUNT'] : 0;
745 if ($mode == 'ALL') {
746 if ($this->messages_caching) {
747 // Save additional info required by cache status check
748 $this->icache['undeleted_idx'] = array($mailbox, $index['ALL'], $index['COUNT']);
751 $this->set_folder_stats($mailbox, 'cnt', $count);
752 $this->set_folder_stats($mailbox, 'maxuid', is_array($index) ? $index['MAX'] : 0);
757 if ($mode == 'UNSEEN')
758 $count = $this->conn->countUnseen($mailbox);
760 $count = $this->conn->countMessages($mailbox);
762 $this->set_folder_stats($mailbox,'cnt', $count);
763 $this->set_folder_stats($mailbox, 'maxuid', $count ? $this->id2uid($count, $mailbox) : 0);
768 $a_mailbox_cache[$mailbox][$mode] = (int)$count;
770 // write back to cache
771 $this->update_cache('messagecount', $a_mailbox_cache);
778 * Private method for getting nr of threads
780 * @param string $mailbox Folder name
782 * @returns array Array containing items: 'count' - threads count,
783 * 'msgcount' = messages count, 'maxuid' = max. UID in the set
786 private function _threadcount($mailbox)
790 if (!empty($this->icache['threads'])) {
791 $dcount = count($this->icache['threads']['depth']);
793 'count' => count($this->icache['threads']['tree']),
794 'msgcount' => $dcount,
795 'maxuid' => $dcount ? max(array_keys($this->icache['threads']['depth'])) : 0,
798 else if (is_array($result = $this->fetch_threads($mailbox))) {
799 $dcount = count($result[1]);
801 'count' => count($result[0]),
802 'msgcount' => $dcount,
803 'maxuid' => $dcount ? max(array_keys($result[1])) : 0,
812 * Public method for listing headers
813 * convert mailbox name with root dir first
815 * @param string $mailbox Mailbox/folder name
816 * @param int $page Current page to list
817 * @param string $sort_field Header field to sort by
818 * @param string $sort_order Sort order [ASC|DESC]
819 * @param int $slice Number of slice items to extract from result array
820 * @return array Indexed array with message header objects
823 function list_headers($mailbox='', $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
825 if (!strlen($mailbox)) {
826 $mailbox = $this->mailbox;
829 return $this->_list_headers($mailbox, $page, $sort_field, $sort_order, $slice);
834 * Private method for listing message headers
836 * @param string $mailbox Mailbox name
837 * @param int $page Current page to list
838 * @param string $sort_field Header field to sort by
839 * @param string $sort_order Sort order [ASC|DESC]
840 * @param int $slice Number of slice items to extract from result array
842 * @return array Indexed array with message header objects
843 * @see rcube_imap::list_headers
845 private function _list_headers($mailbox='', $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
847 if (!strlen($mailbox))
850 // use saved message set
851 if ($this->search_string && $mailbox == $this->mailbox)
852 return $this->_list_header_set($mailbox, $page, $sort_field, $sort_order, $slice);
854 if ($this->threading)
855 return $this->_list_thread_headers($mailbox, $page, $sort_field, $sort_order, $slice);
857 $this->_set_sort_order($sort_field, $sort_order);
859 $page = $page ? $page : $this->list_page;
861 // Use messages cache
862 if ($mcache = $this->get_mcache_engine()) {
863 $msg_index = $mcache->get_index($mailbox, $this->sort_field, $this->sort_order);
865 if (empty($msg_index))
868 $from = ($page-1) * $this->page_size;
869 $to = $from + $this->page_size;
870 $msg_index = array_values($msg_index); // UIDs
875 $msg_index = array_slice($msg_index, $from, $to - $from);
878 $msg_index = array_slice($msg_index, -$slice, $slice);
880 $a_msg_headers = $mcache->get_messages($mailbox, $msg_index);
882 // retrieve headers from IMAP
883 // use message index sort as default sorting (for better performance)
884 else if (!$this->sort_field) {
885 if ($this->skip_deleted) {
886 // @TODO: this could be cached
887 if ($msg_index = $this->_search_index($mailbox, 'ALL UNDELETED')) {
888 list($begin, $end) = $this->_get_message_range(count($msg_index), $page);
889 $msg_index = array_slice($msg_index, $begin, $end-$begin);
892 else if ($max = $this->conn->countMessages($mailbox)) {
893 list($begin, $end) = $this->_get_message_range($max, $page);
894 $msg_index = range($begin+1, $end);
897 $msg_index = array();
899 if ($slice && $msg_index)
900 $msg_index = array_slice($msg_index, ($this->sort_order == 'DESC' ? 0 : -$slice), $slice);
902 // fetch reqested headers from server
904 $a_msg_headers = $this->fetch_headers($mailbox, $msg_index);
907 else if ($this->get_capability('SORT') &&
908 // Courier-IMAP provides SORT capability but allows to disable it by admin (#1486959)
909 ($msg_index = $this->conn->sort($mailbox, $this->sort_field,
910 $this->skip_deleted ? 'UNDELETED' : '', true)) !== false
912 if (!empty($msg_index)) {
913 list($begin, $end) = $this->_get_message_range(count($msg_index), $page);
914 $msg_index = array_slice($msg_index, $begin, $end-$begin);
918 $msg_index = array_slice($msg_index, ($this->sort_order == 'DESC' ? 0 : -$slice), $slice);
920 // fetch reqested headers from server
921 $a_msg_headers = $this->fetch_headers($mailbox, $msg_index, true);
924 // fetch specified header for all messages and sort
925 else if ($msg_index = $this->conn->fetchHeaderIndex($mailbox, "1:*",
926 $this->sort_field, $this->skip_deleted)
928 asort($msg_index); // ASC
929 $msg_index = array_keys($msg_index);
930 list($begin, $end) = $this->_get_message_range(count($msg_index), $page);
931 $msg_index = array_slice($msg_index, $begin, $end-$begin);
934 $msg_index = array_slice($msg_index, ($this->sort_order == 'DESC' ? 0 : -$slice), $slice);
936 // fetch reqested headers from server
937 $a_msg_headers = $this->fetch_headers($mailbox, $msg_index);
940 // return empty array if no messages found
941 if (!is_array($a_msg_headers) || empty($a_msg_headers))
944 // use this class for message sorting
945 $sorter = new rcube_header_sorter();
946 $sorter->set_index($msg_index, $is_uid);
947 $sorter->sort_headers($a_msg_headers);
949 if ($this->sort_order == 'DESC' && !$sorted)
950 $a_msg_headers = array_reverse($a_msg_headers);
952 return array_values($a_msg_headers);
957 * Private method for listing message headers using threads
959 * @param string $mailbox Mailbox/folder name
960 * @param int $page Current page to list
961 * @param string $sort_field Header field to sort by
962 * @param string $sort_order Sort order [ASC|DESC]
963 * @param int $slice Number of slice items to extract from result array
965 * @return array Indexed array with message header objects
966 * @see rcube_imap::list_headers
968 private function _list_thread_headers($mailbox, $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
970 $this->_set_sort_order($sort_field, $sort_order);
972 $page = $page ? $page : $this->list_page;
973 $mcache = $this->get_mcache_engine();
975 // get all threads (not sorted)
977 list ($thread_tree, $msg_depth, $has_children) = $mcache->get_thread($mailbox);
979 list ($thread_tree, $msg_depth, $has_children) = $this->fetch_threads($mailbox);
981 if (empty($thread_tree))
984 $msg_index = $this->sort_threads($mailbox, $thread_tree);
986 return $this->_fetch_thread_headers($mailbox,
987 $thread_tree, $msg_depth, $has_children, $msg_index, $page, $slice);
992 * Method for fetching threads data
994 * @param string $mailbox Folder name
995 * @param bool $force Use IMAP server, no cache
997 * @return array Array with thread data
999 function fetch_threads($mailbox, $force = false)
1001 if (!$force && ($mcache = $this->get_mcache_engine())) {
1002 // don't store in self's internal cache, cache has it's own internal cache
1003 return $mcache->get_thread($mailbox);
1006 if (empty($this->icache['threads'])) {
1008 $result = $this->conn->thread($mailbox, $this->threading,
1009 $this->skip_deleted ? 'UNDELETED' : '');
1011 // add to internal (fast) cache
1012 $this->icache['threads'] = array();
1013 $this->icache['threads']['tree'] = is_array($result) ? $result[0] : array();
1014 $this->icache['threads']['depth'] = is_array($result) ? $result[1] : array();
1015 $this->icache['threads']['has_children'] = is_array($result) ? $result[2] : array();
1019 $this->icache['threads']['tree'],
1020 $this->icache['threads']['depth'],
1021 $this->icache['threads']['has_children'],
1027 * Private method for fetching threaded messages headers
1029 * @param string $mailbox Mailbox name
1030 * @param array $thread_tree Thread tree data
1031 * @param array $msg_depth Thread depth data
1032 * @param array $has_children Thread children data
1033 * @param array $msg_index Messages index
1034 * @param int $page List page number
1035 * @param int $slice Number of threads to slice
1037 * @return array Messages headers
1040 private function _fetch_thread_headers($mailbox, $thread_tree, $msg_depth, $has_children, $msg_index, $page, $slice=0)
1042 // now get IDs for current page
1043 list($begin, $end) = $this->_get_message_range(count($msg_index), $page);
1044 $msg_index = array_slice($msg_index, $begin, $end-$begin);
1047 $msg_index = array_slice($msg_index, ($this->sort_order == 'DESC' ? 0 : -$slice), $slice);
1049 if ($this->sort_order == 'DESC')
1050 $msg_index = array_reverse($msg_index);
1052 // flatten threads array
1053 // @TODO: fetch children only in expanded mode (?)
1055 foreach ($msg_index as $root) {
1057 if (!empty($thread_tree[$root]))
1058 $all_ids = array_merge($all_ids, array_keys_recursive($thread_tree[$root]));
1061 // fetch reqested headers from server
1062 $a_msg_headers = $this->fetch_headers($mailbox, $all_ids);
1064 // return empty array if no messages found
1065 if (!is_array($a_msg_headers) || empty($a_msg_headers))
1068 // use this class for message sorting
1069 $sorter = new rcube_header_sorter();
1070 $sorter->set_index($all_ids);
1071 $sorter->sort_headers($a_msg_headers);
1073 // Set depth, has_children and unread_children fields in headers
1074 $this->_set_thread_flags($a_msg_headers, $msg_depth, $has_children);
1076 return array_values($a_msg_headers);
1081 * Private method for setting threaded messages flags:
1082 * depth, has_children and unread_children
1084 * @param array $headers Reference to headers array indexed by message ID
1085 * @param array $msg_depth Array of messages depth indexed by message ID
1086 * @param array $msg_children Array of messages children flags indexed by message ID
1087 * @return array Message headers array indexed by message ID
1090 private function _set_thread_flags(&$headers, $msg_depth, $msg_children)
1094 foreach ($headers as $idx => $header) {
1096 $depth = $msg_depth[$id];
1097 $parents = array_slice($parents, 0, $depth);
1099 if (!empty($parents)) {
1100 $headers[$idx]->parent_uid = end($parents);
1101 if (empty($header->flags['SEEN']))
1102 $headers[$parents[0]]->unread_children++;
1104 array_push($parents, $header->uid);
1106 $headers[$idx]->depth = $depth;
1107 $headers[$idx]->has_children = $msg_children[$id];
1113 * Private method for listing a set of message headers (search results)
1115 * @param string $mailbox Mailbox/folder name
1116 * @param int $page Current page to list
1117 * @param string $sort_field Header field to sort by
1118 * @param string $sort_order Sort order [ASC|DESC]
1119 * @param int $slice Number of slice items to extract from result array
1120 * @return array Indexed array with message header objects
1122 * @see rcube_imap::list_header_set()
1124 private function _list_header_set($mailbox, $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
1126 if (!strlen($mailbox) || empty($this->search_set))
1129 // use saved messages from searching
1130 if ($this->threading)
1131 return $this->_list_thread_header_set($mailbox, $page, $sort_field, $sort_order, $slice);
1133 // search set is threaded, we need a new one
1134 if ($this->search_threads) {
1135 if (empty($this->search_set['tree']))
1137 $this->search('', $this->search_string, $this->search_charset, $sort_field);
1140 $msgs = $this->search_set;
1141 $a_msg_headers = array();
1142 $page = $page ? $page : $this->list_page;
1143 $start_msg = ($page-1) * $this->page_size;
1145 $this->_set_sort_order($sort_field, $sort_order);
1147 // quickest method (default sorting)
1148 if (!$this->search_sort_field && !$this->sort_field) {
1149 if ($sort_order == 'DESC')
1150 $msgs = array_reverse($msgs);
1152 // get messages uids for one page
1153 $msgs = array_slice(array_values($msgs), $start_msg, min(count($msgs)-$start_msg, $this->page_size));
1156 $msgs = array_slice($msgs, -$slice, $slice);
1159 $a_msg_headers = $this->fetch_headers($mailbox, $msgs);
1161 // I didn't found in RFC that FETCH always returns messages sorted by index
1162 $sorter = new rcube_header_sorter();
1163 $sorter->set_index($msgs);
1164 $sorter->sort_headers($a_msg_headers);
1166 return array_values($a_msg_headers);
1169 // sorted messages, so we can first slice array and then fetch only wanted headers
1170 if ($this->search_sorted) { // SORT searching result
1171 // reset search set if sorting field has been changed
1172 if ($this->sort_field && $this->search_sort_field != $this->sort_field)
1173 $msgs = $this->search('', $this->search_string, $this->search_charset, $this->sort_field);
1175 // return empty array if no messages found
1179 if ($sort_order == 'DESC')
1180 $msgs = array_reverse($msgs);
1182 // get messages uids for one page
1183 $msgs = array_slice(array_values($msgs), $start_msg, min(count($msgs)-$start_msg, $this->page_size));
1186 $msgs = array_slice($msgs, -$slice, $slice);
1189 $a_msg_headers = $this->fetch_headers($mailbox, $msgs);
1191 $sorter = new rcube_header_sorter();
1192 $sorter->set_index($msgs);
1193 $sorter->sort_headers($a_msg_headers);
1195 return array_values($a_msg_headers);
1197 else { // SEARCH result, need sorting
1198 $cnt = count($msgs);
1199 // 300: experimantal value for best result
1200 if (($cnt > 300 && $cnt > $this->page_size) || !$this->sort_field) {
1201 // use memory less expensive (and quick) method for big result set
1202 $a_index = $this->message_index('', $this->sort_field, $this->sort_order);
1203 // get messages uids for one page...
1204 $msgs = array_slice($a_index, $start_msg, min($cnt-$start_msg, $this->page_size));
1206 $msgs = array_slice($msgs, -$slice, $slice);
1207 // ...and fetch headers
1208 $a_msg_headers = $this->fetch_headers($mailbox, $msgs);
1211 // return empty array if no messages found
1212 if (!is_array($a_msg_headers) || empty($a_msg_headers))
1215 $sorter = new rcube_header_sorter();
1216 $sorter->set_index($msgs);
1217 $sorter->sort_headers($a_msg_headers);
1219 return array_values($a_msg_headers);
1222 // for small result set we can fetch all messages headers
1223 $a_msg_headers = $this->fetch_headers($mailbox, $msgs);
1225 // return empty array if no messages found
1226 if (!is_array($a_msg_headers) || empty($a_msg_headers))
1229 // if not already sorted
1230 $a_msg_headers = $this->conn->sortHeaders(
1231 $a_msg_headers, $this->sort_field, $this->sort_order);
1233 // only return the requested part of the set
1234 $a_msg_headers = array_slice(array_values($a_msg_headers),
1235 $start_msg, min($cnt-$start_msg, $this->page_size));
1238 $a_msg_headers = array_slice($a_msg_headers, -$slice, $slice);
1240 return $a_msg_headers;
1247 * Private method for listing a set of threaded message headers (search results)
1249 * @param string $mailbox Mailbox/folder name
1250 * @param int $page Current page to list
1251 * @param string $sort_field Header field to sort by
1252 * @param string $sort_order Sort order [ASC|DESC]
1253 * @param int $slice Number of slice items to extract from result array
1254 * @return array Indexed array with message header objects
1256 * @see rcube_imap::list_header_set()
1258 private function _list_thread_header_set($mailbox, $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
1260 // update search_set if previous data was fetched with disabled threading
1261 if (!$this->search_threads) {
1262 if (empty($this->search_set))
1264 $this->search('', $this->search_string, $this->search_charset, $sort_field);
1268 if (empty($this->search_set['tree']))
1271 $thread_tree = $this->search_set['tree'];
1272 $msg_depth = $this->search_set['depth'];
1273 $has_children = $this->search_set['children'];
1274 $a_msg_headers = array();
1276 $page = $page ? $page : $this->list_page;
1277 $start_msg = ($page-1) * $this->page_size;
1279 $this->_set_sort_order($sort_field, $sort_order);
1281 $msg_index = $this->sort_threads($mailbox, $thread_tree, array_keys($msg_depth));
1283 return $this->_fetch_thread_headers($mailbox,
1284 $thread_tree, $msg_depth, $has_children, $msg_index, $page, $slice=0);
1289 * Helper function to get first and last index of the requested set
1291 * @param int $max Messages count
1292 * @param mixed $page Page number to show, or string 'all'
1293 * @return array Array with two values: first index, last index
1296 private function _get_message_range($max, $page)
1298 $start_msg = ($page-1) * $this->page_size;
1304 else if ($this->sort_order=='DESC') {
1305 $begin = $max - $this->page_size - $start_msg;
1306 $end = $max - $start_msg;
1309 $begin = $start_msg;
1310 $end = $start_msg + $this->page_size;
1313 if ($begin < 0) $begin = 0;
1314 if ($end < 0) $end = $max;
1315 if ($end > $max) $end = $max;
1317 return array($begin, $end);
1322 * Fetches messages headers
1324 * @param string $mailbox Mailbox name
1325 * @param array $msgs Messages sequence numbers
1326 * @param bool $is_uid Enable if $msgs numbers are UIDs
1327 * @param bool $force Disables cache use
1329 * @return array Messages headers indexed by UID
1332 function fetch_headers($mailbox, $msgs, $is_uid = false, $force = false)
1337 if (!$force && ($mcache = $this->get_mcache_engine())) {
1338 return $mcache->get_messages($mailbox, $msgs, $is_uid);
1341 // fetch reqested headers from server
1342 $index = $this->conn->fetchHeaders(
1343 $mailbox, $msgs, $is_uid, false, $this->get_fetch_headers());
1348 foreach ($index as $headers) {
1349 $a_msg_headers[$headers->uid] = $headers;
1352 return $a_msg_headers;
1357 * Returns current status of mailbox
1359 * We compare the maximum UID to determine the number of
1360 * new messages because the RECENT flag is not reliable.
1362 * @param string $mailbox Mailbox/folder name
1363 * @return int Folder status
1365 function mailbox_status($mailbox = null)
1367 if (!strlen($mailbox)) {
1368 $mailbox = $this->mailbox;
1370 $old = $this->get_folder_stats($mailbox);
1372 // refresh message count -> will update
1373 $this->_messagecount($mailbox, 'ALL', true);
1381 $new = $this->get_folder_stats($mailbox);
1384 if ($new['maxuid'] > $old['maxuid'])
1386 // some messages has been deleted
1387 if ($new['cnt'] < $old['cnt'])
1390 // @TODO: optional checking for messages flags changes (?)
1391 // @TODO: UIDVALIDITY checking
1398 * Stores folder statistic data in session
1399 * @TODO: move to separate DB table (cache?)
1401 * @param string $mailbox Mailbox name
1402 * @param string $name Data name
1403 * @param mixed $data Data value
1405 private function set_folder_stats($mailbox, $name, $data)
1407 $_SESSION['folders'][$mailbox][$name] = $data;
1412 * Gets folder statistic data
1414 * @param string $mailbox Mailbox name
1416 * @return array Stats data
1418 private function get_folder_stats($mailbox)
1420 if ($_SESSION['folders'][$mailbox])
1421 return (array) $_SESSION['folders'][$mailbox];
1428 * Return sorted array of message IDs (not UIDs)
1430 * @param string $mailbox Mailbox to get index from
1431 * @param string $sort_field Sort column
1432 * @param string $sort_order Sort order [ASC, DESC]
1433 * @return array Indexed array with message IDs
1435 function message_index($mailbox='', $sort_field=NULL, $sort_order=NULL)
1437 if ($this->threading)
1438 return $this->thread_index($mailbox, $sort_field, $sort_order);
1440 $this->_set_sort_order($sort_field, $sort_order);
1442 if (!strlen($mailbox)) {
1443 $mailbox = $this->mailbox;
1445 $key = "{$mailbox}:{$this->sort_field}:{$this->sort_order}:{$this->search_string}.msgi";
1447 // we have a saved search result, get index from there
1448 if (!isset($this->icache[$key]) && $this->search_string
1449 && !$this->search_threads && $mailbox == $this->mailbox) {
1450 // use message index sort as default sorting
1451 if (!$this->sort_field) {
1452 $msgs = $this->search_set;
1454 if ($this->search_sort_field != 'date')
1457 if ($this->sort_order == 'DESC')
1458 $this->icache[$key] = array_reverse($msgs);
1460 $this->icache[$key] = $msgs;
1462 // sort with SORT command
1463 else if ($this->search_sorted) {
1464 if ($this->sort_field && $this->search_sort_field != $this->sort_field)
1465 $this->search('', $this->search_string, $this->search_charset, $this->sort_field);
1467 if ($this->sort_order == 'DESC')
1468 $this->icache[$key] = array_reverse($this->search_set);
1470 $this->icache[$key] = $this->search_set;
1473 $a_index = $this->conn->fetchHeaderIndex($mailbox,
1474 join(',', $this->search_set), $this->sort_field, $this->skip_deleted);
1476 if (is_array($a_index)) {
1477 if ($this->sort_order=="ASC")
1479 else if ($this->sort_order=="DESC")
1482 $this->icache[$key] = array_keys($a_index);
1485 $this->icache[$key] = array();
1490 // have stored it in RAM
1491 if (isset($this->icache[$key]))
1492 return $this->icache[$key];
1494 // check local cache
1495 if ($mcache = $this->get_mcache_engine()) {
1496 $a_index = $mcache->get_index($mailbox, $this->sort_field, $this->sort_order);
1497 $this->icache[$key] = array_keys($a_index);
1499 // fetch from IMAP server
1501 $this->icache[$key] = $this->message_index_direct(
1502 $mailbox, $this->sort_field, $this->sort_order);
1505 return $this->icache[$key];
1510 * Return sorted array of message IDs (not UIDs) directly from IMAP server.
1511 * Doesn't use cache and ignores current search settings.
1513 * @param string $mailbox Mailbox to get index from
1514 * @param string $sort_field Sort column
1515 * @param string $sort_order Sort order [ASC, DESC]
1517 * @return array Indexed array with message IDs
1519 function message_index_direct($mailbox, $sort_field = null, $sort_order = null)
1521 // use message index sort as default sorting
1523 if ($this->skip_deleted) {
1524 $a_index = $this->conn->search($mailbox, 'ALL UNDELETED');
1525 // I didn't found that SEARCH should return sorted IDs
1526 if (is_array($a_index))
1528 } else if ($max = $this->_messagecount($mailbox, 'ALL', true, false)) {
1529 $a_index = range(1, $max);
1532 if ($a_index !== false && $sort_order == 'DESC')
1533 $a_index = array_reverse($a_index);
1535 // fetch complete message index
1536 else if ($this->get_capability('SORT') &&
1537 ($a_index = $this->conn->sort($mailbox,
1538 $sort_field, $this->skip_deleted ? 'UNDELETED' : '')) !== false
1540 if ($sort_order == 'DESC')
1541 $a_index = array_reverse($a_index);
1543 else if ($a_index = $this->conn->fetchHeaderIndex(
1544 $mailbox, "1:*", $sort_field, $skip_deleted)) {
1545 if ($sort_order=="ASC")
1547 else if ($sort_order=="DESC")
1550 $a_index = array_keys($a_index);
1553 return $a_index !== false ? $a_index : array();
1558 * Return sorted array of threaded message IDs (not UIDs)
1560 * @param string $mailbox Mailbox to get index from
1561 * @param string $sort_field Sort column
1562 * @param string $sort_order Sort order [ASC, DESC]
1563 * @return array Indexed array with message IDs
1565 function thread_index($mailbox='', $sort_field=NULL, $sort_order=NULL)
1567 $this->_set_sort_order($sort_field, $sort_order);
1569 if (!strlen($mailbox)) {
1570 $mailbox = $this->mailbox;
1572 $key = "{$mailbox}:{$this->sort_field}:{$this->sort_order}:{$this->search_string}.thi";
1574 // we have a saved search result, get index from there
1575 if (!isset($this->icache[$key]) && $this->search_string
1576 && $this->search_threads && $mailbox == $this->mailbox) {
1577 // use message IDs for better performance
1578 $ids = array_keys_recursive($this->search_set['tree']);
1579 $this->icache[$key] = $this->_flatten_threads($mailbox, $this->search_set['tree'], $ids);
1582 // have stored it in RAM
1583 if (isset($this->icache[$key]))
1584 return $this->icache[$key];
1586 // get all threads (default sort order)
1587 list ($thread_tree) = $this->fetch_threads($mailbox);
1589 $this->icache[$key] = $this->_flatten_threads($mailbox, $thread_tree);
1591 return $this->icache[$key];
1596 * Return array of threaded messages (all, not only roots)
1598 * @param string $mailbox Mailbox to get index from
1599 * @param array $thread_tree Threaded messages array (see fetch_threads())
1600 * @param array $ids Message IDs if we know what we need (e.g. search result)
1601 * for better performance
1602 * @return array Indexed array with message IDs
1606 private function _flatten_threads($mailbox, $thread_tree, $ids=null)
1608 if (empty($thread_tree))
1611 $msg_index = $this->sort_threads($mailbox, $thread_tree, $ids);
1613 if ($this->sort_order == 'DESC')
1614 $msg_index = array_reverse($msg_index);
1616 // flatten threads array
1618 foreach ($msg_index as $root) {
1620 if (!empty($thread_tree[$root])) {
1621 foreach (array_keys_recursive($thread_tree[$root]) as $val)
1631 * Invoke search request to IMAP server
1633 * @param string $mailbox Mailbox name to search in
1634 * @param string $str Search criteria
1635 * @param string $charset Search charset
1636 * @param string $sort_field Header field to sort by
1637 * @return array search results as list of message IDs
1639 * @todo: Search criteria should be provided in non-IMAP format, eg. array
1641 function search($mailbox='', $str=NULL, $charset=NULL, $sort_field=NULL)
1646 if (!strlen($mailbox)) {
1647 $mailbox = $this->mailbox;
1650 $results = $this->_search_index($mailbox, $str, $charset, $sort_field);
1652 $this->set_search_set($str, $results, $charset, $sort_field, (bool)$this->threading,
1653 $this->threading || $this->search_sorted ? true : false);
1660 * Private search method
1662 * @param string $mailbox Mailbox name
1663 * @param string $criteria Search criteria
1664 * @param string $charset Charset
1665 * @param string $sort_field Sorting field
1667 * @return array search results as list of message ids
1668 * @see rcube_imap::search()
1670 private function _search_index($mailbox, $criteria='ALL', $charset=NULL, $sort_field=NULL)
1672 $orig_criteria = $criteria;
1674 if ($this->skip_deleted && !preg_match('/UNDELETED/', $criteria))
1675 $criteria = 'UNDELETED '.$criteria;
1677 if ($this->threading) {
1678 $a_messages = $this->conn->thread($mailbox, $this->threading, $criteria, $charset);
1680 // Error, try with US-ASCII (RFC5256: SORT/THREAD must support US-ASCII and UTF-8,
1681 // but I've seen that Courier doesn't support UTF-8)
1682 if ($a_messages === false && $charset && $charset != 'US-ASCII')
1683 $a_messages = $this->conn->thread($mailbox, $this->threading,
1684 $this->convert_criteria($criteria, $charset), 'US-ASCII');
1686 if ($a_messages !== false) {
1687 list ($thread_tree, $msg_depth, $has_children) = $a_messages;
1688 $a_messages = array(
1689 'tree' => $thread_tree,
1690 'depth'=> $msg_depth,
1691 'children' => $has_children
1698 if ($sort_field && $this->get_capability('SORT')) {
1699 $charset = $charset ? $charset : $this->default_charset;
1700 $a_messages = $this->conn->sort($mailbox, $sort_field, $criteria, false, $charset);
1702 // Error, try with US-ASCII (RFC5256: SORT/THREAD must support US-ASCII and UTF-8,
1703 // but I've seen Courier with disabled UTF-8 support)
1704 if ($a_messages === false && $charset && $charset != 'US-ASCII')
1705 $a_messages = $this->conn->sort($mailbox, $sort_field,
1706 $this->convert_criteria($criteria, $charset), false, 'US-ASCII');
1708 if ($a_messages !== false) {
1709 $this->search_sorted = true;
1714 if ($orig_criteria == 'ALL') {
1715 $max = $this->_messagecount($mailbox, 'ALL', true, false);
1716 $a_messages = $max ? range(1, $max) : array();
1719 $a_messages = $this->conn->search($mailbox,
1720 ($charset ? "CHARSET $charset " : '') . $criteria);
1722 // Error, try with US-ASCII (some servers may support only US-ASCII)
1723 if ($a_messages === false && $charset && $charset != 'US-ASCII')
1724 $a_messages = $this->conn->search($mailbox,
1725 $this->convert_criteria($criteria, $charset));
1727 // I didn't found that SEARCH should return sorted IDs
1728 if (is_array($a_messages) && !$this->sort_field)
1732 $this->search_sorted = false;
1739 * Direct (real and simple) SEARCH request to IMAP server,
1740 * without result sorting and caching
1742 * @param string $mailbox Mailbox name to search in
1743 * @param string $str Search string
1744 * @param boolean $ret_uid True if UIDs should be returned
1746 * @return array Search results as list of message IDs or UIDs
1748 function search_once($mailbox='', $str=NULL, $ret_uid=false)
1753 if (!strlen($mailbox)) {
1754 $mailbox = $this->mailbox;
1757 return $this->conn->search($mailbox, $str, $ret_uid);
1762 * Converts charset of search criteria string
1764 * @param string $str Search string
1765 * @param string $charset Original charset
1766 * @param string $dest_charset Destination charset (default US-ASCII)
1767 * @return string Search string
1770 private function convert_criteria($str, $charset, $dest_charset='US-ASCII')
1772 // convert strings to US_ASCII
1773 if (preg_match_all('/\{([0-9]+)\}\r\n/', $str, $matches, PREG_OFFSET_CAPTURE)) {
1774 $last = 0; $res = '';
1775 foreach ($matches[1] as $m) {
1776 $string_offset = $m[1] + strlen($m[0]) + 4; // {}\r\n
1777 $string = substr($str, $string_offset - 1, $m[0]);
1778 $string = rcube_charset_convert($string, $charset, $dest_charset);
1779 if ($string === false)
1781 $res .= substr($str, $last, $m[1] - $last - 1) . rcube_imap_generic::escape($string);
1782 $last = $m[0] + $string_offset - 1;
1784 if ($last < strlen($str))
1785 $res .= substr($str, $last, strlen($str)-$last);
1787 else // strings for conversion not found
1797 * @param string $mailbox Mailbox name
1798 * @param array $thread_tree Unsorted thread tree (rcube_imap_generic::thread() result)
1799 * @param array $ids Message IDs if we know what we need (e.g. search result)
1801 * @return array Sorted roots IDs
1803 function sort_threads($mailbox, $thread_tree, $ids = null)
1805 // THREAD=ORDEREDSUBJECT: sorting by sent date of root message
1806 // THREAD=REFERENCES: sorting by sent date of root message
1807 // THREAD=REFS: sorting by the most recent date in each thread
1810 if (!$this->sort_field || ($this->sort_field == 'date' && $this->threading == 'REFS')) {
1811 return array_keys((array)$thread_tree);
1813 // here we'll implement REFS sorting
1815 if ($mcache = $this->get_mcache_engine()) {
1816 $a_index = $mcache->get_index($mailbox, $this->sort_field, 'ASC');
1817 if (is_array($a_index)) {
1818 $a_index = array_keys($a_index);
1819 // now we must remove IDs that doesn't exist in $ids
1821 $a_index = array_intersect($a_index, $ids);
1825 else if ($this->get_capability('SORT') &&
1826 ($a_index = $this->conn->sort($mailbox, $this->sort_field,
1827 !empty($ids) ? $ids : ($this->skip_deleted ? 'UNDELETED' : ''))) !== false
1832 // fetch specified headers for all messages and sort them
1833 $a_index = $this->conn->fetchHeaderIndex($mailbox, !empty($ids) ? $ids : "1:*",
1834 $this->sort_field, $this->skip_deleted);
1836 // return unsorted tree if we've got no index data
1837 if (!empty($a_index)) {
1838 asort($a_index); // ASC
1839 $a_index = array_values($a_index);
1843 if (empty($a_index))
1844 return array_keys((array)$thread_tree);
1846 return $this->_sort_thread_refs($thread_tree, $a_index);
1852 * THREAD=REFS sorting implementation
1854 * @param array $tree Thread tree array (message identifiers as keys)
1855 * @param array $index Array of sorted message identifiers
1857 * @return array Array of sorted roots messages
1859 private function _sort_thread_refs($tree, $index)
1864 $index = array_combine(array_values($index), $index);
1867 foreach ($tree as $idx => $val) {
1868 $index[$idx] = $idx;
1870 $idx_arr = array_keys_recursive($tree[$idx]);
1871 foreach ($idx_arr as $subidx)
1872 $index[$subidx] = $idx;
1876 $index = array_values($index);
1878 // create sorted array of roots
1879 $msg_index = array();
1880 if ($this->sort_order != 'DESC') {
1881 foreach ($index as $idx)
1882 if (!isset($msg_index[$idx]))
1883 $msg_index[$idx] = $idx;
1884 $msg_index = array_values($msg_index);
1887 for ($x=count($index)-1; $x>=0; $x--)
1888 if (!isset($msg_index[$index[$x]]))
1889 $msg_index[$index[$x]] = $index[$x];
1890 $msg_index = array_reverse($msg_index);
1898 * Refresh saved search set
1900 * @return array Current search set
1902 function refresh_search()
1904 if (!empty($this->search_string))
1905 $this->search_set = $this->search('', $this->search_string, $this->search_charset,
1906 $this->search_sort_field, $this->search_threads, $this->search_sorted);
1908 return $this->get_search_set();
1913 * Check if the given message ID is part of the current search set
1915 * @param string $msgid Message id
1916 * @return boolean True on match or if no search request is stored
1918 function in_searchset($msgid)
1920 if (!empty($this->search_string)) {
1921 if ($this->search_threads)
1922 return isset($this->search_set['depth']["$msgid"]);
1924 return in_array("$msgid", (array)$this->search_set, true);
1932 * Return message headers object of a specific message
1934 * @param int $id Message sequence ID or UID
1935 * @param string $mailbox Mailbox to read from
1936 * @param bool $force True to skip cache
1938 * @return rcube_mail_header Message headers
1940 function get_headers($uid, $mailbox = null, $force = false)
1942 if (!strlen($mailbox)) {
1943 $mailbox = $this->mailbox;
1946 // get cached headers
1947 if (!$force && $uid && ($mcache = $this->get_mcache_engine())) {
1948 $headers = $mcache->get_message($mailbox, $uid);
1951 $headers = $this->conn->fetchHeader(
1952 $mailbox, $uid, true, true, $this->get_fetch_headers());
1960 * Fetch message headers and body structure from the IMAP server and build
1961 * an object structure similar to the one generated by PEAR::Mail_mimeDecode
1963 * @param int $uid Message UID to fetch
1964 * @param string $mailbox Mailbox to read from
1966 * @return object rcube_mail_header Message data
1968 function get_message($uid, $mailbox = null)
1970 if (!strlen($mailbox)) {
1971 $mailbox = $this->mailbox;
1974 // Check internal cache
1975 if (!empty($this->icache['message'])) {
1976 if (($headers = $this->icache['message']) && $headers->uid == $uid) {
1981 $headers = $this->get_headers($uid, $mailbox);
1983 // message doesn't exist?
1984 if (empty($headers))
1987 // structure might be cached
1988 if (!empty($headers->structure))
1991 $this->_msg_uid = $uid;
1993 if (empty($headers->bodystructure)) {
1994 $headers->bodystructure = $this->conn->getStructure($mailbox, $uid, true);
1997 $structure = $headers->bodystructure;
1999 if (empty($structure))
2002 // set message charset from message headers
2003 if ($headers->charset)
2004 $this->struct_charset = $headers->charset;
2006 $this->struct_charset = $this->_structure_charset($structure);
2008 $headers->ctype = strtolower($headers->ctype);
2010 // Here we can recognize malformed BODYSTRUCTURE and
2011 // 1. [@TODO] parse the message in other way to create our own message structure
2012 // 2. or just show the raw message body.
2013 // Example of structure for malformed MIME message:
2014 // ("text" "plain" NIL NIL NIL "7bit" 2154 70 NIL NIL NIL)
2015 if ($headers->ctype && !is_array($structure[0]) && $headers->ctype != 'text/plain'
2016 && strtolower($structure[0].'/'.$structure[1]) == 'text/plain') {
2017 // we can handle single-part messages, by simple fix in structure (#1486898)
2018 if (preg_match('/^(text|application)\/(.*)/', $headers->ctype, $m)) {
2019 $structure[0] = $m[1];
2020 $structure[1] = $m[2];
2026 $struct = &$this->_structure_part($structure, 0, '', $headers);
2028 // don't trust given content-type
2029 if (empty($struct->parts) && !empty($headers->ctype)) {
2030 $struct->mime_id = '1';
2031 $struct->mimetype = strtolower($headers->ctype);
2032 list($struct->ctype_primary, $struct->ctype_secondary) = explode('/', $struct->mimetype);
2035 $headers->structure = $struct;
2037 return $this->icache['message'] = $headers;
2042 * Build message part object
2044 * @param array $part
2046 * @param string $parent
2049 function &_structure_part($part, $count=0, $parent='', $mime_headers=null)
2051 $struct = new rcube_message_part;
2052 $struct->mime_id = empty($parent) ? (string)$count : "$parent.$count";
2055 if (is_array($part[0])) {
2056 $struct->ctype_primary = 'multipart';
2058 /* RFC3501: BODYSTRUCTURE fields of multipart part
2064 2. parameters (optional)
2065 3. description (optional)
2066 4. language (optional)
2067 5. location (optional)
2070 // find first non-array entry
2071 for ($i=1; $i<count($part); $i++) {
2072 if (!is_array($part[$i])) {
2073 $struct->ctype_secondary = strtolower($part[$i]);
2078 $struct->mimetype = 'multipart/'.$struct->ctype_secondary;
2080 // build parts list for headers pre-fetching
2081 for ($i=0; $i<count($part); $i++) {
2082 if (!is_array($part[$i]))
2084 // fetch message headers if message/rfc822
2085 // or named part (could contain Content-Location header)
2086 if (!is_array($part[$i][0])) {
2087 $tmp_part_id = $struct->mime_id ? $struct->mime_id.'.'.($i+1) : $i+1;
2088 if (strtolower($part[$i][0]) == 'message' && strtolower($part[$i][1]) == 'rfc822') {
2089 $mime_part_headers[] = $tmp_part_id;
2091 else if (in_array('name', (array)$part[$i][2]) && empty($part[$i][3])) {
2092 $mime_part_headers[] = $tmp_part_id;
2097 // pre-fetch headers of all parts (in one command for better performance)
2098 // @TODO: we could do this before _structure_part() call, to fetch
2099 // headers for parts on all levels
2100 if ($mime_part_headers) {
2101 $mime_part_headers = $this->conn->fetchMIMEHeaders($this->mailbox,
2102 $this->_msg_uid, $mime_part_headers);
2105 $struct->parts = array();
2106 for ($i=0, $count=0; $i<count($part); $i++) {
2107 if (!is_array($part[$i]))
2109 $tmp_part_id = $struct->mime_id ? $struct->mime_id.'.'.($i+1) : $i+1;
2110 $struct->parts[] = $this->_structure_part($part[$i], ++$count, $struct->mime_id,
2111 $mime_part_headers[$tmp_part_id]);
2117 /* RFC3501: BODYSTRUCTURE fields of non-multipart part
2128 7. envelope structure
2133 x. disposition (optional)
2134 x. language (optional)
2135 x. location (optional)
2139 $struct->ctype_primary = strtolower($part[0]);
2140 $struct->ctype_secondary = strtolower($part[1]);
2141 $struct->mimetype = $struct->ctype_primary.'/'.$struct->ctype_secondary;
2143 // read content type parameters
2144 if (is_array($part[2])) {
2145 $struct->ctype_parameters = array();
2146 for ($i=0; $i<count($part[2]); $i+=2)
2147 $struct->ctype_parameters[strtolower($part[2][$i])] = $part[2][$i+1];
2149 if (isset($struct->ctype_parameters['charset']))
2150 $struct->charset = $struct->ctype_parameters['charset'];
2153 // #1487700: workaround for lack of charset in malformed structure
2154 if (empty($struct->charset) && !empty($mime_headers) && $mime_headers->charset) {
2155 $struct->charset = $mime_headers->charset;
2158 // read content encoding
2159 if (!empty($part[5])) {
2160 $struct->encoding = strtolower($part[5]);
2161 $struct->headers['content-transfer-encoding'] = $struct->encoding;
2165 if (!empty($part[6]))
2166 $struct->size = intval($part[6]);
2168 // read part disposition
2170 if ($struct->ctype_primary == 'text') $di += 1;
2171 else if ($struct->mimetype == 'message/rfc822') $di += 3;
2173 if (is_array($part[$di]) && count($part[$di]) == 2) {
2174 $struct->disposition = strtolower($part[$di][0]);
2176 if (is_array($part[$di][1]))
2177 for ($n=0; $n<count($part[$di][1]); $n+=2)
2178 $struct->d_parameters[strtolower($part[$di][1][$n])] = $part[$di][1][$n+1];
2181 // get message/rfc822's child-parts
2182 if (is_array($part[8]) && $di != 8) {
2183 $struct->parts = array();
2184 for ($i=0, $count=0; $i<count($part[8]); $i++) {
2185 if (!is_array($part[8][$i]))
2187 $struct->parts[] = $this->_structure_part($part[8][$i], ++$count, $struct->mime_id);
2192 if (!empty($part[3])) {
2193 $struct->content_id = $part[3];
2194 $struct->headers['content-id'] = $part[3];
2196 if (empty($struct->disposition))
2197 $struct->disposition = 'inline';
2200 // fetch message headers if message/rfc822 or named part (could contain Content-Location header)
2201 if ($struct->ctype_primary == 'message' || ($struct->ctype_parameters['name'] && !$struct->content_id)) {
2202 if (empty($mime_headers)) {
2203 $mime_headers = $this->conn->fetchPartHeader(
2204 $this->mailbox, $this->_msg_uid, true, $struct->mime_id);
2207 if (is_string($mime_headers))
2208 $struct->headers = $this->_parse_headers($mime_headers) + $struct->headers;
2209 else if (is_object($mime_headers))
2210 $struct->headers = get_object_vars($mime_headers) + $struct->headers;
2212 // get real content-type of message/rfc822
2213 if ($struct->mimetype == 'message/rfc822') {
2215 if (!is_array($part[8][0]))
2216 $struct->real_mimetype = strtolower($part[8][0] . '/' . $part[8][1]);
2219 for ($n=0; $n<count($part[8]); $n++)
2220 if (!is_array($part[8][$n]))
2222 $struct->real_mimetype = 'multipart/' . strtolower($part[8][$n]);
2226 if ($struct->ctype_primary == 'message' && empty($struct->parts)) {
2227 if (is_array($part[8]) && $di != 8)
2228 $struct->parts[] = $this->_structure_part($part[8], ++$count, $struct->mime_id);
2232 // normalize filename property
2233 $this->_set_part_filename($struct, $mime_headers);
2240 * Set attachment filename from message part structure
2242 * @param rcube_message_part $part Part object
2243 * @param string $headers Part's raw headers
2246 private function _set_part_filename(&$part, $headers=null)
2248 if (!empty($part->d_parameters['filename']))
2249 $filename_mime = $part->d_parameters['filename'];
2250 else if (!empty($part->d_parameters['filename*']))
2251 $filename_encoded = $part->d_parameters['filename*'];
2252 else if (!empty($part->ctype_parameters['name*']))
2253 $filename_encoded = $part->ctype_parameters['name*'];
2254 // RFC2231 value continuations
2255 // TODO: this should be rewrited to support RFC2231 4.1 combinations
2256 else if (!empty($part->d_parameters['filename*0'])) {
2258 while (isset($part->d_parameters['filename*'.$i])) {
2259 $filename_mime .= $part->d_parameters['filename*'.$i];
2262 // some servers (eg. dovecot-1.x) have no support for parameter value continuations
2263 // we must fetch and parse headers "manually"
2266 $headers = $this->conn->fetchPartHeader(
2267 $this->mailbox, $this->_msg_uid, true, $part->mime_id);
2269 $filename_mime = '';
2271 while (preg_match('/filename\*'.$i.'\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
2272 $filename_mime .= $matches[1];
2277 else if (!empty($part->d_parameters['filename*0*'])) {
2279 while (isset($part->d_parameters['filename*'.$i.'*'])) {
2280 $filename_encoded .= $part->d_parameters['filename*'.$i.'*'];
2285 $headers = $this->conn->fetchPartHeader(
2286 $this->mailbox, $this->_msg_uid, true, $part->mime_id);
2288 $filename_encoded = '';
2289 $i = 0; $matches = array();
2290 while (preg_match('/filename\*'.$i.'\*\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
2291 $filename_encoded .= $matches[1];
2296 else if (!empty($part->ctype_parameters['name*0'])) {
2298 while (isset($part->ctype_parameters['name*'.$i])) {
2299 $filename_mime .= $part->ctype_parameters['name*'.$i];
2304 $headers = $this->conn->fetchPartHeader(
2305 $this->mailbox, $this->_msg_uid, true, $part->mime_id);
2307 $filename_mime = '';
2308 $i = 0; $matches = array();
2309 while (preg_match('/\s+name\*'.$i.'\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
2310 $filename_mime .= $matches[1];
2315 else if (!empty($part->ctype_parameters['name*0*'])) {
2317 while (isset($part->ctype_parameters['name*'.$i.'*'])) {
2318 $filename_encoded .= $part->ctype_parameters['name*'.$i.'*'];
2323 $headers = $this->conn->fetchPartHeader(
2324 $this->mailbox, $this->_msg_uid, true, $part->mime_id);
2326 $filename_encoded = '';
2327 $i = 0; $matches = array();
2328 while (preg_match('/\s+name\*'.$i.'\*\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
2329 $filename_encoded .= $matches[1];
2334 // read 'name' after rfc2231 parameters as it may contains truncated filename (from Thunderbird)
2335 else if (!empty($part->ctype_parameters['name']))
2336 $filename_mime = $part->ctype_parameters['name'];
2337 // Content-Disposition
2338 else if (!empty($part->headers['content-description']))
2339 $filename_mime = $part->headers['content-description'];
2344 if (!empty($filename_mime)) {
2345 if (!empty($part->charset))
2346 $charset = $part->charset;
2347 else if (!empty($this->struct_charset))
2348 $charset = $this->struct_charset;
2350 $charset = rc_detect_encoding($filename_mime, $this->default_charset);
2352 $part->filename = rcube_imap::decode_mime_string($filename_mime, $charset);
2354 else if (!empty($filename_encoded)) {
2355 // decode filename according to RFC 2231, Section 4
2356 if (preg_match("/^([^']*)'[^']*'(.*)$/", $filename_encoded, $fmatches)) {
2357 $filename_charset = $fmatches[1];
2358 $filename_encoded = $fmatches[2];
2361 $part->filename = rcube_charset_convert(urldecode($filename_encoded), $filename_charset);
2367 * Get charset name from message structure (first part)
2369 * @param array $structure Message structure
2370 * @return string Charset name
2373 private function _structure_charset($structure)
2375 while (is_array($structure)) {
2376 if (is_array($structure[2]) && $structure[2][0] == 'charset')
2377 return $structure[2][1];
2378 $structure = $structure[0];
2384 * Fetch message body of a specific message from the server
2386 * @param int $uid Message UID
2387 * @param string $part Part number
2388 * @param rcube_message_part $o_part Part object created by get_structure()
2389 * @param mixed $print True to print part, ressource to write part contents in
2390 * @param resource $fp File pointer to save the message part
2391 * @param boolean $skip_charset_conv Disables charset conversion
2393 * @return string Message/part body if not printed
2395 function &get_message_part($uid, $part=1, $o_part=NULL, $print=NULL, $fp=NULL, $skip_charset_conv=false)
2397 // get part data if not provided
2398 if (!is_object($o_part)) {
2399 $structure = $this->conn->getStructure($this->mailbox, $uid, true);
2400 $part_data = rcube_imap_generic::getStructurePartData($structure, $part);
2402 $o_part = new rcube_message_part;
2403 $o_part->ctype_primary = $part_data['type'];
2404 $o_part->encoding = $part_data['encoding'];
2405 $o_part->charset = $part_data['charset'];
2406 $o_part->size = $part_data['size'];
2409 if ($o_part && $o_part->size) {
2410 $body = $this->conn->handlePartBody($this->mailbox, $uid, true,
2411 $part ? $part : 'TEXT', $o_part->encoding, $print, $fp);
2414 if ($fp || $print) {
2418 // convert charset (if text or message part)
2419 if ($body && preg_match('/^(text|message)$/', $o_part->ctype_primary)) {
2420 // Remove NULL characters (#1486189)
2421 $body = str_replace("\x00", '', $body);
2423 if (!$skip_charset_conv) {
2424 if (!$o_part->charset || strtoupper($o_part->charset) == 'US-ASCII') {
2425 // try to extract charset information from HTML meta tag (#1488125)
2426 if ($o_part->ctype_secondary == 'html' && preg_match('/<meta[^>]+charset=([a-z0-9-_]+)/i', $body, $m))
2427 $o_part->charset = strtoupper($m[1]);
2429 $o_part->charset = $this->default_charset;
2431 $body = rcube_charset_convert($body, $o_part->charset);
2440 * Fetch message body of a specific message from the server
2442 * @param int $uid Message UID
2443 * @return string $part Message/part body
2444 * @see rcube_imap::get_message_part()
2446 function &get_body($uid, $part=1)
2448 $headers = $this->get_headers($uid);
2449 return rcube_charset_convert($this->get_message_part($uid, $part, NULL),
2450 $headers->charset ? $headers->charset : $this->default_charset);
2455 * Returns the whole message source as string (or saves to a file)
2457 * @param int $uid Message UID
2458 * @param resource $fp File pointer to save the message
2460 * @return string Message source string
2462 function &get_raw_body($uid, $fp=null)
2464 return $this->conn->handlePartBody($this->mailbox, $uid,
2465 true, null, null, false, $fp);
2470 * Returns the message headers as string
2472 * @param int $uid Message UID
2473 * @return string Message headers string
2475 function &get_raw_headers($uid)
2477 return $this->conn->fetchPartHeader($this->mailbox, $uid, true);
2482 * Sends the whole message source to stdout
2484 * @param int $uid Message UID
2486 function print_raw_body($uid)
2488 $this->conn->handlePartBody($this->mailbox, $uid, true, NULL, NULL, true);
2493 * Set message flag to one or several messages
2495 * @param mixed $uids Message UIDs as array or comma-separated string, or '*'
2496 * @param string $flag Flag to set: SEEN, UNDELETED, DELETED, RECENT, ANSWERED, DRAFT, MDNSENT
2497 * @param string $mailbox Folder name
2498 * @param boolean $skip_cache True to skip message cache clean up
2500 * @return boolean Operation status
2502 function set_flag($uids, $flag, $mailbox=null, $skip_cache=false)
2504 if (!strlen($mailbox)) {
2505 $mailbox = $this->mailbox;
2508 $flag = strtoupper($flag);
2509 list($uids, $all_mode) = $this->_parse_uids($uids, $mailbox);
2511 if (strpos($flag, 'UN') === 0)
2512 $result = $this->conn->unflag($mailbox, $uids, substr($flag, 2));
2514 $result = $this->conn->flag($mailbox, $uids, $flag);
2517 // reload message headers if cached
2518 // @TODO: update flags instead removing from cache
2519 if (!$skip_cache && ($mcache = $this->get_mcache_engine())) {
2520 $status = strpos($flag, 'UN') !== 0;
2521 $mflag = preg_replace('/^UN/', '', $flag);
2522 $mcache->change_flag($mailbox, $all_mode ? null : explode(',', $uids),
2526 // clear cached counters
2527 if ($flag == 'SEEN' || $flag == 'UNSEEN') {
2528 $this->_clear_messagecount($mailbox, 'SEEN');
2529 $this->_clear_messagecount($mailbox, 'UNSEEN');
2531 else if ($flag == 'DELETED') {
2532 $this->_clear_messagecount($mailbox, 'DELETED');
2541 * Remove message flag for one or several messages
2543 * @param mixed $uids Message UIDs as array or comma-separated string, or '*'
2544 * @param string $flag Flag to unset: SEEN, DELETED, RECENT, ANSWERED, DRAFT, MDNSENT
2545 * @param string $mailbox Folder name
2547 * @return int Number of flagged messages, -1 on failure
2550 function unset_flag($uids, $flag, $mailbox=null)
2552 return $this->set_flag($uids, 'UN'.$flag, $mailbox);
2557 * Append a mail message (source) to a specific mailbox
2559 * @param string $mailbox Target mailbox
2560 * @param string $message The message source string or filename
2561 * @param string $headers Headers string if $message contains only the body
2562 * @param boolean $is_file True if $message is a filename
2564 * @return int|bool Appended message UID or True on success, False on error
2566 function save_message($mailbox, &$message, $headers='', $is_file=false)
2568 if (!strlen($mailbox)) {
2569 $mailbox = $this->mailbox;
2572 // make sure mailbox exists
2573 if ($this->mailbox_exists($mailbox)) {
2575 $saved = $this->conn->appendFromFile($mailbox, $message, $headers);
2577 $saved = $this->conn->append($mailbox, $message);
2581 // increase messagecount of the target mailbox
2582 $this->_set_messagecount($mailbox, 'ALL', 1);
2590 * Move a message from one mailbox to another
2592 * @param mixed $uids Message UIDs as array or comma-separated string, or '*'
2593 * @param string $to_mbox Target mailbox
2594 * @param string $from_mbox Source mailbox
2595 * @return boolean True on success, False on error
2597 function move_message($uids, $to_mbox, $from_mbox='')
2599 if (!strlen($from_mbox)) {
2600 $from_mbox = $this->mailbox;
2603 if ($to_mbox === $from_mbox) {
2607 list($uids, $all_mode) = $this->_parse_uids($uids, $from_mbox);
2609 // exit if no message uids are specified
2613 // make sure mailbox exists
2614 if ($to_mbox != 'INBOX' && !$this->mailbox_exists($to_mbox)) {
2615 if (in_array($to_mbox, $this->default_folders)) {
2616 if (!$this->create_mailbox($to_mbox, true)) {
2625 $config = rcmail::get_instance()->config;
2626 $to_trash = $to_mbox == $config->get('trash_mbox');
2628 // flag messages as read before moving them
2629 if ($to_trash && $config->get('read_when_deleted')) {
2630 // don't flush cache (4th argument)
2631 $this->set_flag($uids, 'SEEN', $from_mbox, true);
2635 $moved = $this->conn->move($uids, $from_mbox, $to_mbox);
2637 // send expunge command in order to have the moved message
2638 // really deleted from the source mailbox
2640 $this->_expunge($from_mbox, false, $uids);
2641 $this->_clear_messagecount($from_mbox);
2642 $this->_clear_messagecount($to_mbox);
2645 else if ($to_trash && $config->get('delete_always', false)) {
2646 $moved = $this->delete_message($uids, $from_mbox);
2650 // unset threads internal cache
2651 unset($this->icache['threads']);
2653 // remove message ids from search set
2654 if ($this->search_set && $from_mbox == $this->mailbox) {
2655 // threads are too complicated to just remove messages from set
2656 if ($this->search_threads || $all_mode)
2657 $this->refresh_search();
2659 $a_uids = explode(',', $uids);
2660 foreach ($a_uids as $uid)
2661 $a_mids[] = $this->uid2id($uid, $from_mbox);
2662 $this->search_set = array_diff($this->search_set, $a_mids);
2668 // remove cached messages
2669 // @TODO: do cache update instead of clearing it
2670 $this->clear_message_cache($from_mbox, $all_mode ? null : explode(',', $uids));
2678 * Copy a message from one mailbox to another
2680 * @param mixed $uids Message UIDs as array or comma-separated string, or '*'
2681 * @param string $to_mbox Target mailbox
2682 * @param string $from_mbox Source mailbox
2683 * @return boolean True on success, False on error
2685 function copy_message($uids, $to_mbox, $from_mbox='')
2687 if (!strlen($from_mbox)) {
2688 $from_mbox = $this->mailbox;
2691 list($uids, $all_mode) = $this->_parse_uids($uids, $from_mbox);
2693 // exit if no message uids are specified
2698 // make sure mailbox exists
2699 if ($to_mbox != 'INBOX' && !$this->mailbox_exists($to_mbox)) {
2700 if (in_array($to_mbox, $this->default_folders)) {
2701 if (!$this->create_mailbox($to_mbox, true)) {
2711 $copied = $this->conn->copy($uids, $from_mbox, $to_mbox);
2714 $this->_clear_messagecount($to_mbox);
2722 * Mark messages as deleted and expunge mailbox
2724 * @param mixed $uids Message UIDs as array or comma-separated string, or '*'
2725 * @param string $mailbox Source mailbox
2727 * @return boolean True on success, False on error
2729 function delete_message($uids, $mailbox='')
2731 if (!strlen($mailbox)) {
2732 $mailbox = $this->mailbox;
2735 list($uids, $all_mode) = $this->_parse_uids($uids, $mailbox);
2737 // exit if no message uids are specified
2741 $deleted = $this->conn->delete($mailbox, $uids);
2744 // send expunge command in order to have the deleted message
2745 // really deleted from the mailbox
2746 $this->_expunge($mailbox, false, $uids);
2747 $this->_clear_messagecount($mailbox);
2748 unset($this->uid_id_map[$mailbox]);
2750 // unset threads internal cache
2751 unset($this->icache['threads']);
2753 // remove message ids from search set
2754 if ($this->search_set && $mailbox == $this->mailbox) {
2755 // threads are too complicated to just remove messages from set
2756 if ($this->search_threads || $all_mode)
2757 $this->refresh_search();
2759 $a_uids = explode(',', $uids);
2760 foreach ($a_uids as $uid)
2761 $a_mids[] = $this->uid2id($uid, $mailbox);
2762 $this->search_set = array_diff($this->search_set, $a_mids);
2768 // remove cached messages
2769 $this->clear_message_cache($mailbox, $all_mode ? null : explode(',', $uids));
2777 * Clear all messages in a specific mailbox
2779 * @param string $mailbox Mailbox name
2781 * @return int Above 0 on success
2783 function clear_mailbox($mailbox=null)
2785 if (!strlen($mailbox)) {
2786 $mailbox = $this->mailbox;
2789 // SELECT will set messages count for clearFolder()
2790 if ($this->conn->select($mailbox)) {
2791 $cleared = $this->conn->clearFolder($mailbox);
2794 // make sure the cache is cleared as well
2796 $this->clear_message_cache($mailbox);
2797 $a_mailbox_cache = $this->get_cache('messagecount');
2798 unset($a_mailbox_cache[$mailbox]);
2799 $this->update_cache('messagecount', $a_mailbox_cache);
2807 * Send IMAP expunge command and clear cache
2809 * @param string $mailbox Mailbox name
2810 * @param boolean $clear_cache False if cache should not be cleared
2812 * @return boolean True on success
2814 function expunge($mailbox='', $clear_cache=true)
2816 if (!strlen($mailbox)) {
2817 $mailbox = $this->mailbox;
2820 return $this->_expunge($mailbox, $clear_cache);
2825 * Send IMAP expunge command and clear cache
2827 * @param string $mailbox Mailbox name
2828 * @param boolean $clear_cache False if cache should not be cleared
2829 * @param mixed $uids Message UIDs as array or comma-separated string, or '*'
2830 * @return boolean True on success
2832 * @see rcube_imap::expunge()
2834 private function _expunge($mailbox, $clear_cache=true, $uids=NULL)
2836 if ($uids && $this->get_capability('UIDPLUS'))
2837 list($uids, $all_mode) = $this->_parse_uids($uids, $mailbox);
2841 // force mailbox selection and check if mailbox is writeable
2842 // to prevent a situation when CLOSE is executed on closed
2843 // or EXPUNGE on read-only mailbox
2844 $result = $this->conn->select($mailbox);
2848 if (!$this->conn->data['READ-WRITE']) {
2849 $this->conn->setError(rcube_imap_generic::ERROR_READONLY, "Mailbox is read-only");
2853 // CLOSE(+SELECT) should be faster than EXPUNGE
2854 if (empty($uids) || $all_mode)
2855 $result = $this->conn->close();
2857 $result = $this->conn->expunge($mailbox, $uids);
2859 if ($result && $clear_cache) {
2860 $this->clear_message_cache($mailbox, $all_mode ? null : explode(',', $uids));
2861 $this->_clear_messagecount($mailbox);
2869 * Parse message UIDs input
2871 * @param mixed $uids UIDs array or comma-separated list or '*' or '1:*'
2872 * @param string $mailbox Mailbox name
2873 * @return array Two elements array with UIDs converted to list and ALL flag
2876 private function _parse_uids($uids, $mailbox)
2878 if ($uids === '*' || $uids === '1:*') {
2879 if (empty($this->search_set)) {
2883 // get UIDs from current search set
2884 // @TODO: skip fetchUIDs() and work with IDs instead of UIDs (?)
2886 if ($this->search_threads)
2887 $uids = $this->conn->fetchUIDs($mailbox, array_keys($this->search_set['depth']));
2889 $uids = $this->conn->fetchUIDs($mailbox, $this->search_set);
2891 // save ID-to-UID mapping in local cache
2892 if (is_array($uids))
2893 foreach ($uids as $id => $uid)
2894 $this->uid_id_map[$mailbox][$uid] = $id;
2896 $uids = join(',', $uids);
2900 if (is_array($uids))
2901 $uids = join(',', $uids);
2903 if (preg_match('/[^0-9,]/', $uids))
2907 return array($uids, (bool) $all);
2912 * Translate UID to message ID
2914 * @param int $uid Message UID
2915 * @param string $mailbox Mailbox name
2917 * @return int Message ID
2919 function get_id($uid, $mailbox=null)
2921 if (!strlen($mailbox)) {
2922 $mailbox = $this->mailbox;
2925 return $this->uid2id($uid, $mailbox);
2930 * Translate message number to UID
2932 * @param int $id Message ID
2933 * @param string $mailbox Mailbox name
2935 * @return int Message UID
2937 function get_uid($id, $mailbox=null)
2939 if (!strlen($mailbox)) {
2940 $mailbox = $this->mailbox;
2943 return $this->id2uid($id, $mailbox);
2948 /* --------------------------------
2950 * --------------------------------*/
2953 * Public method for listing subscribed folders
2955 * @param string $root Optional root folder
2956 * @param string $name Optional name pattern
2957 * @param string $filter Optional filter
2958 * @param string $rights Optional ACL requirements
2959 * @param bool $skip_sort Enable to return unsorted list (for better performance)
2961 * @return array List of folders
2964 function list_mailboxes($root='', $name='*', $filter=null, $rights=null, $skip_sort=false)
2966 $cache_key = $root.':'.$name;
2967 if (!empty($filter)) {
2968 $cache_key .= ':'.(is_string($filter) ? $filter : serialize($filter));
2970 $cache_key .= ':'.$rights;
2971 $cache_key = 'mailboxes.'.md5($cache_key);
2973 // get cached folder list
2974 $a_mboxes = $this->get_cache($cache_key);
2975 if (is_array($a_mboxes)) {
2979 $a_mboxes = $this->_list_mailboxes($root, $name, $filter, $rights);
2981 if (!is_array($a_mboxes)) {
2985 // filter folders list according to rights requirements
2986 if ($rights && $this->get_capability('ACL')) {
2987 $a_mboxes = $this->filter_rights($a_mboxes, $rights);
2990 // INBOX should always be available
2991 if ((!$filter || $filter == 'mail') && !in_array('INBOX', $a_mboxes)) {
2992 array_unshift($a_mboxes, 'INBOX');
2995 // sort mailboxes (always sort for cache)
2996 if (!$skip_sort || $this->cache) {
2997 $a_mboxes = $this->_sort_mailbox_list($a_mboxes);
3000 // write mailboxlist to cache
3001 $this->update_cache($cache_key, $a_mboxes);
3008 * Private method for mailbox listing (LSUB)
3010 * @param string $root Optional root folder
3011 * @param string $name Optional name pattern
3012 * @param mixed $filter Optional filter
3013 * @param string $rights Optional ACL requirements
3015 * @return array List of subscribed folders
3016 * @see rcube_imap::list_mailboxes()
3019 private function _list_mailboxes($root='', $name='*', $filter=null, $rights=null)
3021 $a_defaults = $a_out = array();
3023 // Give plugins a chance to provide a list of mailboxes
3024 $data = rcmail::get_instance()->plugins->exec_hook('mailboxes_list',
3025 array('root' => $root, 'name' => $name, 'filter' => $filter, 'mode' => 'LSUB'));
3027 if (isset($data['folders'])) {
3028 $a_folders = $data['folders'];
3030 else if (!$this->conn->connected()) {
3034 // Server supports LIST-EXTENDED, we can use selection options
3035 $config = rcmail::get_instance()->config;
3036 // #1486225: Some dovecot versions returns wrong result using LIST-EXTENDED
3037 if (!$config->get('imap_force_lsub') && $this->get_capability('LIST-EXTENDED')) {
3038 // This will also set mailbox options, LSUB doesn't do that
3039 $a_folders = $this->conn->listMailboxes($root, $name,
3040 NULL, array('SUBSCRIBED'));
3042 // unsubscribe non-existent folders, remove from the list
3043 if (is_array($a_folders) && $name == '*') {
3044 foreach ($a_folders as $idx => $folder) {
3045 if ($this->conn->data['LIST'] && ($opts = $this->conn->data['LIST'][$folder])
3046 && in_array('\\NonExistent', $opts)
3048 $this->conn->unsubscribe($folder);
3049 unset($a_folders[$idx]);
3054 // retrieve list of folders from IMAP server using LSUB
3056 $a_folders = $this->conn->listSubscribed($root, $name);
3058 // unsubscribe non-existent folders, remove from the list
3059 if (is_array($a_folders) && $name == '*') {
3060 foreach ($a_folders as $idx => $folder) {
3061 if ($this->conn->data['LIST'] && ($opts = $this->conn->data['LIST'][$folder])
3062 && in_array('\\Noselect', $opts)
3064 // Some servers returns \Noselect for existing folders
3065 if (!$this->mailbox_exists($folder)) {
3066 $this->conn->unsubscribe($folder);
3067 unset($a_folders[$idx]);
3075 if (!is_array($a_folders) || !sizeof($a_folders)) {
3076 $a_folders = array();
3084 * Get a list of all folders available on the IMAP server
3086 * @param string $root IMAP root dir
3087 * @param string $name Optional name pattern
3088 * @param mixed $filter Optional filter
3089 * @param string $rights Optional ACL requirements
3090 * @param bool $skip_sort Enable to return unsorted list (for better performance)
3092 * @return array Indexed array with folder names
3094 function list_unsubscribed($root='', $name='*', $filter=null, $rights=null, $skip_sort=false)
3096 $cache_key = $root.':'.$name;
3097 if (!empty($filter)) {
3098 $cache_key .= ':'.(is_string($filter) ? $filter : serialize($filter));
3100 $cache_key .= ':'.$rights;
3101 $cache_key = 'mailboxes.list.'.md5($cache_key);
3103 // get cached folder list
3104 $a_mboxes = $this->get_cache($cache_key);
3105 if (is_array($a_mboxes)) {
3109 // Give plugins a chance to provide a list of mailboxes
3110 $data = rcmail::get_instance()->plugins->exec_hook('mailboxes_list',
3111 array('root' => $root, 'name' => $name, 'filter' => $filter, 'mode' => 'LIST'));
3113 if (isset($data['folders'])) {
3114 $a_mboxes = $data['folders'];
3117 // retrieve list of folders from IMAP server
3118 $a_mboxes = $this->_list_unsubscribed($root, $name);
3121 if (!is_array($a_mboxes)) {
3122 $a_mboxes = array();
3125 // INBOX should always be available
3126 if ((!$filter || $filter == 'mail') && !in_array('INBOX', $a_mboxes)) {
3127 array_unshift($a_mboxes, 'INBOX');
3130 // cache folder attributes
3131 if ($root == '' && $name == '*' && empty($filter)) {
3132 $this->update_cache('mailboxes.attributes', $this->conn->data['LIST']);
3135 // filter folders list according to rights requirements
3136 if ($rights && $this->get_capability('ACL')) {
3137 $a_folders = $this->filter_rights($a_folders, $rights);
3140 // filter folders and sort them
3142 $a_mboxes = $this->_sort_mailbox_list($a_mboxes);
3145 // write mailboxlist to cache
3146 $this->update_cache($cache_key, $a_mboxes);
3153 * Private method for mailbox listing (LIST)
3155 * @param string $root Optional root folder
3156 * @param string $name Optional name pattern
3158 * @return array List of folders
3159 * @see rcube_imap::list_unsubscribed()
3161 private function _list_unsubscribed($root='', $name='*')
3163 $result = $this->conn->listMailboxes($root, $name);
3165 if (!is_array($result)) {
3169 // #1486796: some server configurations doesn't
3170 // return folders in all namespaces, we'll try to detect that situation
3171 // and ask for these namespaces separately
3172 if ($root == '' && $name == '*') {
3173 $delim = $this->get_hierarchy_delimiter();
3174 $namespace = $this->get_namespace();
3177 // build list of namespace prefixes
3178 foreach ((array)$namespace as $ns) {
3179 if (is_array($ns)) {
3180 foreach ($ns as $ns_data) {
3181 if (strlen($ns_data[0])) {
3182 $search[] = $ns_data[0];
3188 if (!empty($search)) {
3189 // go through all folders detecting namespace usage
3190 foreach ($result as $folder) {
3191 foreach ($search as $idx => $prefix) {
3192 if (strpos($folder, $prefix) === 0) {
3193 unset($search[$idx]);
3196 if (empty($search)) {
3201 // get folders in hidden namespaces and add to the result
3202 foreach ($search as $prefix) {
3203 $list = $this->conn->listMailboxes($prefix, $name);
3205 if (!empty($list)) {
3206 $result = array_merge($result, $list);
3217 * Filter the given list of folders according to access rights
3219 private function filter_rights($a_folders, $rights)
3221 $regex = '/('.$rights.')/';
3222 foreach ($a_folders as $idx => $folder) {
3223 $myrights = join('', (array)$this->my_rights($folder));
3224 if ($myrights !== null && !preg_match($regex, $myrights))
3225 unset($a_folders[$idx]);
3233 * Get mailbox quota information
3236 * @return mixed Quota info or False if not supported
3238 function get_quota()
3240 if ($this->get_capability('QUOTA'))
3241 return $this->conn->getQuota();
3248 * Get mailbox size (size of all messages in a mailbox)
3250 * @param string $mailbox Mailbox name
3252 * @return int Mailbox size in bytes, False on error
3254 function get_mailbox_size($mailbox)
3256 // @TODO: could we try to use QUOTA here?
3257 $result = $this->conn->fetchHeaderIndex($mailbox, '1:*', 'SIZE', false);
3259 if (is_array($result))
3260 $result = array_sum($result);
3267 * Subscribe to a specific mailbox(es)
3269 * @param array $a_mboxes Mailbox name(s)
3270 * @return boolean True on success
3272 function subscribe($a_mboxes)
3274 if (!is_array($a_mboxes))
3275 $a_mboxes = array($a_mboxes);
3277 // let this common function do the main work
3278 return $this->_change_subscription($a_mboxes, 'subscribe');
3283 * Unsubscribe mailboxes
3285 * @param array $a_mboxes Mailbox name(s)
3286 * @return boolean True on success
3288 function unsubscribe($a_mboxes)
3290 if (!is_array($a_mboxes))
3291 $a_mboxes = array($a_mboxes);
3293 // let this common function do the main work
3294 return $this->_change_subscription($a_mboxes, 'unsubscribe');
3299 * Create a new mailbox on the server and register it in local cache
3301 * @param string $mailbox New mailbox name
3302 * @param boolean $subscribe True if the new mailbox should be subscribed
3304 * @return boolean True on success
3306 function create_mailbox($mailbox, $subscribe=false)
3308 $result = $this->conn->createFolder($mailbox);
3310 // try to subscribe it
3313 $this->clear_cache('mailboxes', true);
3316 $this->subscribe($mailbox);
3324 * Set a new name to an existing mailbox
3326 * @param string $mailbox Mailbox to rename
3327 * @param string $new_name New mailbox name
3329 * @return boolean True on success
3331 function rename_mailbox($mailbox, $new_name)
3333 if (!strlen($new_name)) {
3337 $delm = $this->get_hierarchy_delimiter();
3339 // get list of subscribed folders
3340 if ((strpos($mailbox, '%') === false) && (strpos($mailbox, '*') === false)) {
3341 $a_subscribed = $this->_list_mailboxes('', $mailbox . $delm . '*');
3342 $subscribed = $this->mailbox_exists($mailbox, true);
3345 $a_subscribed = $this->_list_mailboxes();
3346 $subscribed = in_array($mailbox, $a_subscribed);
3349 $result = $this->conn->renameFolder($mailbox, $new_name);
3352 // unsubscribe the old folder, subscribe the new one
3354 $this->conn->unsubscribe($mailbox);
3355 $this->conn->subscribe($new_name);
3358 // check if mailbox children are subscribed
3359 foreach ($a_subscribed as $c_subscribed) {
3360 if (preg_match('/^'.preg_quote($mailbox.$delm, '/').'/', $c_subscribed)) {
3361 $this->conn->unsubscribe($c_subscribed);
3362 $this->conn->subscribe(preg_replace('/^'.preg_quote($mailbox, '/').'/',
3363 $new_name, $c_subscribed));
3366 $this->clear_message_cache($c_subscribed);
3371 $this->clear_message_cache($mailbox);
3372 $this->clear_cache('mailboxes', true);
3380 * Remove mailbox from server
3382 * @param string $mailbox Mailbox name
3384 * @return boolean True on success
3386 function delete_mailbox($mailbox)
3388 $delm = $this->get_hierarchy_delimiter();
3390 // get list of folders
3391 if ((strpos($mailbox, '%') === false) && (strpos($mailbox, '*') === false))
3392 $sub_mboxes = $this->list_unsubscribed('', $mailbox . $delm . '*');
3394 $sub_mboxes = $this->list_unsubscribed();
3396 // send delete command to server
3397 $result = $this->conn->deleteFolder($mailbox);
3400 // unsubscribe mailbox
3401 $this->conn->unsubscribe($mailbox);
3403 foreach ($sub_mboxes as $c_mbox) {
3404 if (preg_match('/^'.preg_quote($mailbox.$delm, '/').'/', $c_mbox)) {
3405 $this->conn->unsubscribe($c_mbox);
3406 if ($this->conn->deleteFolder($c_mbox)) {
3407 $this->clear_message_cache($c_mbox);
3412 // clear mailbox-related cache
3413 $this->clear_message_cache($mailbox);
3414 $this->clear_cache('mailboxes', true);
3422 * Create all folders specified as default
3424 function create_default_folders()
3426 // create default folders if they do not exist
3427 foreach ($this->default_folders as $folder) {
3428 if (!$this->mailbox_exists($folder))
3429 $this->create_mailbox($folder, true);
3430 else if (!$this->mailbox_exists($folder, true))
3431 $this->subscribe($folder);
3437 * Checks if folder exists and is subscribed
3439 * @param string $mailbox Folder name
3440 * @param boolean $subscription Enable subscription checking
3442 * @return boolean TRUE or FALSE
3444 function mailbox_exists($mailbox, $subscription=false)
3446 if ($mailbox == 'INBOX') {
3450 $key = $subscription ? 'subscribed' : 'existing';
3452 if (is_array($this->icache[$key]) && in_array($mailbox, $this->icache[$key]))
3455 if ($subscription) {
3456 $a_folders = $this->conn->listSubscribed('', $mailbox);
3459 $a_folders = $this->conn->listMailboxes('', $mailbox);
3462 if (is_array($a_folders) && in_array($mailbox, $a_folders)) {
3463 $this->icache[$key][] = $mailbox;
3472 * Returns the namespace where the folder is in
3474 * @param string $mailbox Folder name
3476 * @return string One of 'personal', 'other' or 'shared'
3479 function mailbox_namespace($mailbox)
3481 if ($mailbox == 'INBOX') {
3485 foreach ($this->namespace as $type => $namespace) {
3486 if (is_array($namespace)) {
3487 foreach ($namespace as $ns) {
3488 if ($len = strlen($ns[0])) {
3489 if (($len > 1 && $mailbox == substr($ns[0], 0, -1))
3490 || strpos($mailbox, $ns[0]) === 0
3504 * Modify folder name according to namespace.
3505 * For output it removes prefix of the personal namespace if it's possible.
3506 * For input it adds the prefix. Use it before creating a folder in root
3507 * of the folders tree.
3509 * @param string $mailbox Folder name
3510 * @param string $mode Mode name (out/in)
3512 * @return string Folder name
3514 function mod_mailbox($mailbox, $mode = 'out')
3516 if (!strlen($mailbox)) {
3520 $prefix = $this->namespace['prefix']; // see set_env()
3521 $prefix_len = strlen($prefix);
3527 // remove prefix for output
3528 if ($mode == 'out') {
3529 if (substr($mailbox, 0, $prefix_len) === $prefix) {
3530 return substr($mailbox, $prefix_len);
3533 // add prefix for input (e.g. folder creation)
3535 return $prefix . $mailbox;
3543 * Gets folder attributes from LIST response, e.g. \Noselect, \Noinferiors
3545 * @param string $mailbox Folder name
3546 * @param bool $force Set to True if attributes should be refreshed
3548 * @return array Options list
3550 function mailbox_attributes($mailbox, $force=false)
3552 // get attributes directly from LIST command
3553 if (!empty($this->conn->data['LIST']) && is_array($this->conn->data['LIST'][$mailbox])) {
3554 $opts = $this->conn->data['LIST'][$mailbox];
3556 // get cached folder attributes
3558 $opts = $this->get_cache('mailboxes.attributes');
3559 $opts = $opts[$mailbox];
3562 if (!is_array($opts)) {
3563 $this->conn->listMailboxes('', $mailbox);
3564 $opts = $this->conn->data['LIST'][$mailbox];
3567 return is_array($opts) ? $opts : array();
3572 * Gets connection (and current mailbox) data: UIDVALIDITY, EXISTS, RECENT,
3573 * PERMANENTFLAGS, UIDNEXT, UNSEEN
3575 * @param string $mailbox Folder name
3577 * @return array Data
3579 function mailbox_data($mailbox)
3581 if (!strlen($mailbox))
3582 $mailbox = $this->mailbox !== null ? $this->mailbox : 'INBOX';
3584 if ($this->conn->selected != $mailbox) {
3585 if ($this->conn->select($mailbox))
3586 $this->mailbox = $mailbox;
3591 $data = $this->conn->data;
3593 // add (E)SEARCH result for ALL UNDELETED query
3594 if (!empty($this->icache['undeleted_idx']) && $this->icache['undeleted_idx'][0] == $mailbox) {
3595 $data['ALL_UNDELETED'] = $this->icache['undeleted_idx'][1];
3596 $data['COUNT_UNDELETED'] = $this->icache['undeleted_idx'][2];
3604 * Returns extended information about the folder
3606 * @param string $mailbox Folder name
3608 * @return array Data
3610 function mailbox_info($mailbox)
3612 if ($this->icache['options'] && $this->icache['options']['name'] == $mailbox) {
3613 return $this->icache['options'];
3616 $acl = $this->get_capability('ACL');
3617 $namespace = $this->get_namespace();
3620 // check if the folder is a namespace prefix
3621 if (!empty($namespace)) {
3622 $mbox = $mailbox . $this->delimiter;
3623 foreach ($namespace as $ns) {
3625 foreach ($ns as $item) {
3626 if ($item[0] === $mbox) {
3627 $options['is_root'] = true;
3634 // check if the folder is other user virtual-root
3635 if (!$options['is_root'] && !empty($namespace) && !empty($namespace['other'])) {
3636 $parts = explode($this->delimiter, $mailbox);
3637 if (count($parts) == 2) {
3638 $mbox = $parts[0] . $this->delimiter;
3639 foreach ($namespace['other'] as $item) {
3640 if ($item[0] === $mbox) {
3641 $options['is_root'] = true;
3648 $options['name'] = $mailbox;
3649 $options['attributes'] = $this->mailbox_attributes($mailbox, true);
3650 $options['namespace'] = $this->mailbox_namespace($mailbox);
3651 $options['rights'] = $acl && !$options['is_root'] ? (array)$this->my_rights($mailbox) : array();
3652 $options['special'] = in_array($mailbox, $this->default_folders);
3654 // Set 'noselect' and 'norename' flags
3655 if (is_array($options['attributes'])) {
3656 foreach ($options['attributes'] as $attrib) {
3657 $attrib = strtolower($attrib);
3658 if ($attrib == '\noselect' || $attrib == '\nonexistent') {
3659 $options['noselect'] = true;
3664 $options['noselect'] = true;
3667 if (!empty($options['rights'])) {
3668 $options['norename'] = !in_array('x', $options['rights']) && !in_array('d', $options['rights']);
3670 if (!$options['noselect']) {
3671 $options['noselect'] = !in_array('r', $options['rights']);
3675 $options['norename'] = $options['is_root'] || $options['namespace'] != 'personal';
3678 $this->icache['options'] = $options;
3685 * Synchronizes messages cache.
3687 * @param string $mailbox Folder name
3689 public function mailbox_sync($mailbox)
3691 if ($mcache = $this->get_mcache_engine()) {
3692 $mcache->synchronize($mailbox);
3698 * Get message header names for rcube_imap_generic::fetchHeader(s)
3700 * @return string Space-separated list of header names
3702 private function get_fetch_headers()
3704 $headers = explode(' ', $this->fetch_add_headers);
3705 $headers = array_map('strtoupper', $headers);
3707 if ($this->messages_caching || $this->get_all_headers)
3708 $headers = array_merge($headers, $this->all_headers);
3710 return implode(' ', array_unique($headers));
3714 /* -----------------------------------------
3715 * ACL and METADATA/ANNOTATEMORE methods
3716 * ----------------------------------------*/
3719 * Changes the ACL on the specified mailbox (SETACL)
3721 * @param string $mailbox Mailbox name
3722 * @param string $user User name
3723 * @param string $acl ACL string
3725 * @return boolean True on success, False on failure
3730 function set_acl($mailbox, $user, $acl)
3732 if ($this->get_capability('ACL'))
3733 return $this->conn->setACL($mailbox, $user, $acl);
3740 * Removes any <identifier,rights> pair for the
3741 * specified user from the ACL for the specified
3742 * mailbox (DELETEACL)
3744 * @param string $mailbox Mailbox name
3745 * @param string $user User name
3747 * @return boolean True on success, False on failure
3752 function delete_acl($mailbox, $user)
3754 if ($this->get_capability('ACL'))
3755 return $this->conn->deleteACL($mailbox, $user);
3762 * Returns the access control list for mailbox (GETACL)
3764 * @param string $mailbox Mailbox name
3766 * @return array User-rights array on success, NULL on error
3770 function get_acl($mailbox)
3772 if ($this->get_capability('ACL'))
3773 return $this->conn->getACL($mailbox);
3780 * Returns information about what rights can be granted to the
3781 * user (identifier) in the ACL for the mailbox (LISTRIGHTS)
3783 * @param string $mailbox Mailbox name
3784 * @param string $user User name
3786 * @return array List of user rights
3790 function list_rights($mailbox, $user)
3792 if ($this->get_capability('ACL'))
3793 return $this->conn->listRights($mailbox, $user);
3800 * Returns the set of rights that the current user has to
3801 * mailbox (MYRIGHTS)
3803 * @param string $mailbox Mailbox name
3805 * @return array MYRIGHTS response on success, NULL on error
3809 function my_rights($mailbox)
3811 if ($this->get_capability('ACL'))
3812 return $this->conn->myRights($mailbox);
3819 * Sets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
3821 * @param string $mailbox Mailbox name (empty for server metadata)
3822 * @param array $entries Entry-value array (use NULL value as NIL)
3824 * @return boolean True on success, False on failure
3828 function set_metadata($mailbox, $entries)
3830 if ($this->get_capability('METADATA') ||
3831 (!strlen($mailbox) && $this->get_capability('METADATA-SERVER'))
3833 return $this->conn->setMetadata($mailbox, $entries);
3835 else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
3836 foreach ((array)$entries as $entry => $value) {
3837 list($ent, $attr) = $this->md2annotate($entry);
3838 $entries[$entry] = array($ent, $attr, $value);
3840 return $this->conn->setAnnotation($mailbox, $entries);
3848 * Unsets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
3850 * @param string $mailbox Mailbox name (empty for server metadata)
3851 * @param array $entries Entry names array
3853 * @return boolean True on success, False on failure
3858 function delete_metadata($mailbox, $entries)
3860 if ($this->get_capability('METADATA') ||
3861 (!strlen($mailbox) && $this->get_capability('METADATA-SERVER'))
3863 return $this->conn->deleteMetadata($mailbox, $entries);
3865 else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
3866 foreach ((array)$entries as $idx => $entry) {
3867 list($ent, $attr) = $this->md2annotate($entry);
3868 $entries[$idx] = array($ent, $attr, NULL);
3870 return $this->conn->setAnnotation($mailbox, $entries);
3878 * Returns IMAP metadata/annotations (GETMETADATA/GETANNOTATION)
3880 * @param string $mailbox Mailbox name (empty for server metadata)
3881 * @param array $entries Entries
3882 * @param array $options Command options (with MAXSIZE and DEPTH keys)
3884 * @return array Metadata entry-value hash array on success, NULL on error
3889 function get_metadata($mailbox, $entries, $options=array())
3891 if ($this->get_capability('METADATA') ||
3892 (!strlen($mailbox) && $this->get_capability('METADATA-SERVER'))
3894 return $this->conn->getMetadata($mailbox, $entries, $options);
3896 else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
3900 // Convert entry names
3901 foreach ((array)$entries as $entry) {
3902 list($ent, $attr) = $this->md2annotate($entry);
3903 $queries[$attr][] = $ent;
3906 // @TODO: Honor MAXSIZE and DEPTH options
3907 foreach ($queries as $attrib => $entry)
3908 if ($result = $this->conn->getAnnotation($mailbox, $entry, $attrib))
3909 $res = array_merge_recursive($res, $result);
3919 * Converts the METADATA extension entry name into the correct
3920 * entry-attrib names for older ANNOTATEMORE version.
3922 * @param string $entry Entry name
3924 * @return array Entry-attribute list, NULL if not supported (?)
3926 private function md2annotate($entry)
3928 if (substr($entry, 0, 7) == '/shared') {
3929 return array(substr($entry, 7), 'value.shared');
3931 else if (substr($entry, 0, 8) == '/private') {
3932 return array(substr($entry, 8), 'value.priv');
3940 /* --------------------------------
3941 * internal caching methods
3942 * --------------------------------*/
3945 * Enable or disable indexes caching
3947 * @param string $type Cache type (@see rcmail::get_cache)
3950 function set_caching($type)
3953 $this->caching = $type;
3957 $this->cache->close();
3958 $this->cache = null;
3959 $this->caching = false;
3964 * Getter for IMAP cache object
3966 private function get_cache_engine()
3968 if ($this->caching && !$this->cache) {
3969 $rcmail = rcmail::get_instance();
3970 $this->cache = $rcmail->get_cache('IMAP', $this->caching);
3973 return $this->cache;
3977 * Returns cached value
3979 * @param string $key Cache key
3983 function get_cache($key)
3985 if ($cache = $this->get_cache_engine()) {
3986 return $cache->get($key);
3993 * @param string $key Cache key
3994 * @param mixed $data Data
3997 function update_cache($key, $data)
3999 if ($cache = $this->get_cache_engine()) {
4000 $cache->set($key, $data);
4007 * @param string $key Cache key name or pattern
4008 * @param boolean $prefix_mode Enable it to clear all keys starting
4009 * with prefix specified in $key
4012 function clear_cache($key=null, $prefix_mode=false)
4014 if ($cache = $this->get_cache_engine()) {
4015 $cache->remove($key, $prefix_mode);
4020 /* --------------------------------
4021 * message caching methods
4022 * --------------------------------*/
4025 * Enable or disable messages caching
4027 * @param boolean $set Flag
4029 function set_messages_caching($set)
4032 $this->messages_caching = true;
4036 $this->mcache->close();
4037 $this->mcache = null;
4038 $this->messages_caching = false;
4043 * Getter for messages cache object
4045 private function get_mcache_engine()
4047 if ($this->messages_caching && !$this->mcache) {
4048 $rcmail = rcmail::get_instance();
4049 if ($dbh = $rcmail->get_dbh()) {
4050 $this->mcache = new rcube_imap_cache(
4051 $dbh, $this, $rcmail->user->ID, $this->skip_deleted);
4055 return $this->mcache;
4059 * Clears the messages cache.
4061 * @param string $mailbox Folder name
4062 * @param array $uids Optional message UIDs to remove from cache
4064 function clear_message_cache($mailbox = null, $uids = null)
4066 if ($mcache = $this->get_mcache_engine()) {
4067 $mcache->clear($mailbox, $uids);
4073 /* --------------------------------
4074 * encoding/decoding methods
4075 * --------------------------------*/
4078 * Split an address list into a structured array list
4080 * @param string $input Input string
4081 * @param int $max List only this number of addresses
4082 * @param boolean $decode Decode address strings
4083 * @return array Indexed list of addresses
4085 function decode_address_list($input, $max=null, $decode=true)
4087 $a = $this->_parse_address_list($input, $decode);
4089 // Special chars as defined by RFC 822 need to in quoted string (or escaped).
4090 $special_chars = '[\(\)\<\>\\\.\[\]@,;:"]';
4098 foreach ($a as $val) {
4100 $address = trim($val['address']);
4101 $name = trim($val['name']);
4103 if ($name && $address && $name != $address)
4104 $string = sprintf('%s <%s>', preg_match("/$special_chars/", $name) ? '"'.addcslashes($name, '"').'"' : $name, $address);
4112 'mailto' => $address,
4116 if ($max && $j==$max)
4125 * Decode a message header value
4127 * @param string $input Header value
4128 * @param boolean $remove_quotas Remove quotes if necessary
4129 * @return string Decoded string
4131 function decode_header($input, $remove_quotes=false)
4133 $str = rcube_imap::decode_mime_string((string)$input, $this->default_charset);
4134 if ($str[0] == '"' && $remove_quotes)
4135 $str = str_replace('"', '', $str);
4142 * Decode a mime-encoded string to internal charset
4144 * @param string $input Header value
4145 * @param string $fallback Fallback charset if none specified
4147 * @return string Decoded string
4150 public static function decode_mime_string($input, $fallback=null)
4152 if (!empty($fallback)) {
4153 $default_charset = $fallback;
4156 $default_charset = rcmail::get_instance()->config->get('default_charset', 'ISO-8859-1');
4159 // rfc: all line breaks or other characters not found
4160 // in the Base64 Alphabet must be ignored by decoding software
4161 // delete all blanks between MIME-lines, differently we can
4162 // receive unnecessary blanks and broken utf-8 symbols
4163 $input = preg_replace("/\?=\s+=\?/", '?==?', $input);
4165 // encoded-word regexp
4166 $re = '/=\?([^?]+)\?([BbQq])\?([^\n]*?)\?=/';
4168 // Find all RFC2047's encoded words
4169 if (preg_match_all($re, $input, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) {
4170 // Initialize variables
4175 foreach ($matches as $idx => $m) {
4177 $charset = $m[1][0];
4178 $encoding = $m[2][0];
4180 $length = strlen($m[0][0]);
4182 // Append everything that is before the text to be decoded
4183 if ($start != $pos) {
4184 $substr = substr($input, $start, $pos-$start);
4185 $out .= rcube_charset_convert($substr, $default_charset);
4190 // Per RFC2047, each string part "MUST represent an integral number
4191 // of characters . A multi-octet character may not be split across
4192 // adjacent encoded-words." However, some mailers break this, so we
4193 // try to handle characters spanned across parts anyway by iterating
4194 // through and aggregating sequential encoded parts with the same
4195 // character set and encoding, then perform the decoding on the
4196 // aggregation as a whole.
4199 if ($next_match = $matches[$idx+1]) {
4200 if ($next_match[0][1] == $start
4201 && $next_match[1][0] == $charset
4202 && $next_match[2][0] == $encoding
4208 $count = count($tmp);
4211 // Decode and join encoded-word's chunks
4212 if ($encoding == 'B' || $encoding == 'b') {
4213 // base64 must be decoded a segment at a time
4214 for ($i=0; $i<$count; $i++)
4215 $text .= base64_decode($tmp[$i]);
4217 else { //if ($encoding == 'Q' || $encoding == 'q') {
4218 // quoted printable can be combined and processed at once
4219 for ($i=0; $i<$count; $i++)
4222 $text = str_replace('_', ' ', $text);
4223 $text = quoted_printable_decode($text);
4226 $out .= rcube_charset_convert($text, $charset);
4230 // add the last part of the input string
4231 if ($start != strlen($input)) {
4232 $out .= rcube_charset_convert(substr($input, $start), $default_charset);
4235 // return the results
4239 // no encoding information, use fallback
4240 return rcube_charset_convert($input, $default_charset);
4245 * Decode a mime part
4247 * @param string $input Input string
4248 * @param string $encoding Part encoding
4249 * @return string Decoded string
4251 function mime_decode($input, $encoding='7bit')
4253 switch (strtolower($encoding)) {
4254 case 'quoted-printable':
4255 return quoted_printable_decode($input);
4257 return base64_decode($input);
4262 return convert_uudecode($input);
4271 * Convert body charset to RCMAIL_CHARSET according to the ctype_parameters
4273 * @param string $body Part body to decode
4274 * @param string $ctype_param Charset to convert from
4275 * @return string Content converted to internal charset
4277 function charset_decode($body, $ctype_param)
4279 if (is_array($ctype_param) && !empty($ctype_param['charset']))
4280 return rcube_charset_convert($body, $ctype_param['charset']);
4282 // defaults to what is specified in the class header
4283 return rcube_charset_convert($body, $this->default_charset);
4287 /* --------------------------------
4289 * --------------------------------*/
4292 * Validate the given input and save to local properties
4294 * @param string $sort_field Sort column
4295 * @param string $sort_order Sort order
4298 private function _set_sort_order($sort_field, $sort_order)
4300 if ($sort_field != null)
4301 $this->sort_field = asciiwords($sort_field);
4302 if ($sort_order != null)
4303 $this->sort_order = strtoupper($sort_order) == 'DESC' ? 'DESC' : 'ASC';
4308 * Sort mailboxes first by default folders and then in alphabethical order
4310 * @param array $a_folders Mailboxes list
4313 private function _sort_mailbox_list($a_folders)
4315 $a_out = $a_defaults = $folders = array();
4317 $delimiter = $this->get_hierarchy_delimiter();
4319 // find default folders and skip folders starting with '.'
4320 foreach ($a_folders as $i => $folder) {
4321 if ($folder[0] == '.')
4324 if (($p = array_search($folder, $this->default_folders)) !== false && !$a_defaults[$p])
4325 $a_defaults[$p] = $folder;
4327 $folders[$folder] = rcube_charset_convert($folder, 'UTF7-IMAP');
4330 // sort folders and place defaults on the top
4331 asort($folders, SORT_LOCALE_STRING);
4333 $folders = array_merge($a_defaults, array_keys($folders));
4335 // finally we must rebuild the list to move
4336 // subfolders of default folders to their place...
4337 // ...also do this for the rest of folders because
4338 // asort() is not properly sorting case sensitive names
4339 while (list($key, $folder) = each($folders)) {
4340 // set the type of folder name variable (#1485527)
4341 $a_out[] = (string) $folder;
4342 unset($folders[$key]);
4343 $this->_rsort($folder, $delimiter, $folders, $a_out);
4353 private function _rsort($folder, $delimiter, &$list, &$out)
4355 while (list($key, $name) = each($list)) {
4356 if (strpos($name, $folder.$delimiter) === 0) {
4357 // set the type of folder name variable (#1485527)
4358 $out[] = (string) $name;
4360 $this->_rsort($name, $delimiter, $list, $out);
4368 * Finds message sequence ID for specified UID
4370 * @param int $uid Message UID
4371 * @param string $mailbox Mailbox name
4372 * @param bool $force True to skip cache
4374 * @return int Message (sequence) ID
4376 function uid2id($uid, $mailbox = null, $force = false)
4378 if (!strlen($mailbox)) {
4379 $mailbox = $this->mailbox;
4382 if (!empty($this->uid_id_map[$mailbox][$uid])) {
4383 return $this->uid_id_map[$mailbox][$uid];
4386 if (!$force && ($mcache = $this->get_mcache_engine()))
4387 $id = $mcache->uid2id($mailbox, $uid);
4390 $id = $this->conn->UID2ID($mailbox, $uid);
4392 $this->uid_id_map[$mailbox][$uid] = $id;
4399 * Find UID of the specified message sequence ID
4401 * @param int $id Message (sequence) ID
4402 * @param string $mailbox Mailbox name
4403 * @param bool $force True to skip cache
4405 * @return int Message UID
4407 function id2uid($id, $mailbox = null, $force = false)
4409 if (!strlen($mailbox)) {
4410 $mailbox = $this->mailbox;
4413 if ($uid = array_search($id, (array)$this->uid_id_map[$mailbox])) {
4417 if (!$force && ($mcache = $this->get_mcache_engine()))
4418 $uid = $mcache->id2uid($mailbox, $id);
4421 $uid = $this->conn->ID2UID($mailbox, $id);
4423 $this->uid_id_map[$mailbox][$uid] = $id;
4430 * Subscribe/unsubscribe a list of mailboxes and update local cache
4433 private function _change_subscription($a_mboxes, $mode)
4437 if (is_array($a_mboxes))
4438 foreach ($a_mboxes as $i => $mailbox) {
4439 $a_mboxes[$i] = $mailbox;
4441 if ($mode == 'subscribe')
4442 $updated = $this->conn->subscribe($mailbox);
4443 else if ($mode == 'unsubscribe')
4444 $updated = $this->conn->unsubscribe($mailbox);
4447 // clear cached mailbox list(s)
4449 $this->clear_cache('mailboxes', true);
4457 * Increde/decrese messagecount for a specific mailbox
4460 private function _set_messagecount($mailbox, $mode, $increment)
4462 $mode = strtoupper($mode);
4463 $a_mailbox_cache = $this->get_cache('messagecount');
4465 if (!is_array($a_mailbox_cache[$mailbox]) || !isset($a_mailbox_cache[$mailbox][$mode]) || !is_numeric($increment))
4468 // add incremental value to messagecount
4469 $a_mailbox_cache[$mailbox][$mode] += $increment;
4471 // there's something wrong, delete from cache
4472 if ($a_mailbox_cache[$mailbox][$mode] < 0)
4473 unset($a_mailbox_cache[$mailbox][$mode]);
4475 // write back to cache
4476 $this->update_cache('messagecount', $a_mailbox_cache);
4483 * Remove messagecount of a specific mailbox from cache
4486 private function _clear_messagecount($mailbox, $mode=null)
4488 $a_mailbox_cache = $this->get_cache('messagecount');
4490 if (is_array($a_mailbox_cache[$mailbox])) {
4492 unset($a_mailbox_cache[$mailbox][$mode]);
4495 unset($a_mailbox_cache[$mailbox]);
4497 $this->update_cache('messagecount', $a_mailbox_cache);
4503 * Split RFC822 header string into an associative array
4506 private function _parse_headers($headers)
4508 $a_headers = array();
4509 $headers = preg_replace('/\r?\n(\t| )+/', ' ', $headers);
4510 $lines = explode("\n", $headers);
4513 for ($i=0; $i<$c; $i++) {
4514 if ($p = strpos($lines[$i], ': ')) {
4515 $field = strtolower(substr($lines[$i], 0, $p));
4516 $value = trim(substr($lines[$i], $p+1));
4518 $a_headers[$field] = $value;
4529 private function _parse_address_list($str, $decode=true)
4531 // remove any newlines and carriage returns before
4532 $str = preg_replace('/\r?\n(\s|\t)?/', ' ', $str);
4534 // extract list items, remove comments
4535 $str = self::explode_header_string(',;', $str, true);
4538 // simplified regexp, supporting quoted local part
4539 $email_rx = '(\S+|("\s*(?:[^"\f\n\r\t\v\b\s]+\s*)+"))@\S+';
4541 foreach ($str as $key => $val) {
4546 if (preg_match('/(.*)<('.$email_rx.')>$/', $val, $m)) {
4548 $name = trim($m[1]);
4550 else if (preg_match('/^('.$email_rx.')$/', $val, $m)) {
4558 // dequote and/or decode name
4560 if ($name[0] == '"' && $name[strlen($name)-1] == '"') {
4561 $name = substr($name, 1, -1);
4562 $name = stripslashes($name);
4565 $name = $this->decode_header($name);
4569 if (!$address && $name) {
4574 $result[$key] = array('name' => $name, 'address' => $address);
4583 * Explodes header (e.g. address-list) string into array of strings
4584 * using specified separator characters with proper handling
4585 * of quoted-strings and comments (RFC2822)
4587 * @param string $separator String containing separator characters
4588 * @param string $str Header string
4589 * @param bool $remove_comments Enable to remove comments
4591 * @return array Header items
4593 static function explode_header_string($separator, $str, $remove_comments=false)
4595 $length = strlen($str);
4601 for ($i=0; $i<$length; $i++) {
4602 // we're inside a quoted string
4604 if ($str[$i] == '"') {
4607 else if ($str[$i] == '\\') {
4608 if ($comment <= 0) {
4614 // we're inside a comment string
4615 else if ($comment > 0) {
4616 if ($str[$i] == ')') {
4619 else if ($str[$i] == '(') {
4622 else if ($str[$i] == '\\') {
4627 // separator, add to result array
4628 else if (strpos($separator, $str[$i]) !== false) {
4635 // start of quoted string
4636 else if ($str[$i] == '"') {
4640 else if ($remove_comments && $str[$i] == '(') {
4644 if ($comment <= 0) {
4649 if ($out && $comment <= 0) {
4658 * This is our own debug handler for the IMAP connection
4661 public function debug_handler(&$imap, $message)
4663 write_log('imap', $message);
4666 } // end class rcube_imap
4670 * Class representing a message part
4674 class rcube_message_part
4677 var $ctype_primary = 'text';
4678 var $ctype_secondary = 'plain';
4679 var $mimetype = 'text/plain';
4680 var $disposition = '';
4682 var $encoding = '8bit';
4685 var $headers = array();
4686 var $d_parameters = array();
4687 var $ctype_parameters = array();
4691 if (isset($this->parts))
4692 foreach ($this->parts as $idx => $part)
4693 if (is_object($part))
4694 $this->parts[$idx] = clone $part;
4700 * Class for sorting an array of rcube_mail_header objects in a predetermined order.
4703 * @author Eric Stadtherr
4705 class rcube_header_sorter
4707 private $seqs = array();
4708 private $uids = array();
4712 * Set the predetermined sort order.
4714 * @param array $index Numerically indexed array of IMAP ID or UIDs
4715 * @param bool $is_uid Set to true if $index contains UIDs
4717 function set_index($index, $is_uid = false)
4719 $index = array_flip($index);
4722 $this->uids = $index;
4724 $this->seqs = $index;
4728 * Sort the array of header objects
4730 * @param array $headers Array of rcube_mail_header objects indexed by UID
4732 function sort_headers(&$headers)
4734 if (!empty($this->uids))
4735 uksort($headers, array($this, "compare_uids"));
4737 uasort($headers, array($this, "compare_seqnums"));
4741 * Sort method called by uasort()
4743 * @param rcube_mail_header $a
4744 * @param rcube_mail_header $b
4746 function compare_seqnums($a, $b)
4748 // First get the sequence number from the header object (the 'id' field).
4752 // then find each sequence number in my ordered list
4753 $posa = isset($this->seqs[$seqa]) ? intval($this->seqs[$seqa]) : -1;
4754 $posb = isset($this->seqs[$seqb]) ? intval($this->seqs[$seqb]) : -1;
4756 // return the relative position as the comparison value
4757 return $posa - $posb;
4761 * Sort method called by uksort()
4763 * @param int $a Array key (UID)
4764 * @param int $b Array key (UID)
4766 function compare_uids($a, $b)
4768 // then find each sequence number in my ordered list
4769 $posa = isset($this->uids[$a]) ? intval($this->uids[$a]) : -1;
4770 $posb = isset($this->uids[$b]) ? intval($this->uids[$b]) : -1;
4772 // return the relative position as the comparison value
4773 return $posa - $posb;