4 +-----------------------------------------------------------------------+
5 | program/include/rcube_imap.php |
7 | This file is part of the Roundcube Webmail client |
8 | Copyright (C) 2005-2010, The Roundcube Dev Team |
9 | Licensed under the GNU GPL |
14 +-----------------------------------------------------------------------+
15 | Author: Thomas Bruederli <roundcube@gmail.com> |
16 | Author: Aleksander Machniak <alec@alec.pl> |
17 +-----------------------------------------------------------------------+
19 $Id: rcube_imap.php 5281 2011-09-27 07:29:49Z alec $
25 * Interface class for accessing an IMAP server
28 * @author Thomas Bruederli <roundcube@gmail.com>
29 * @author Aleksander Machniak <alec@alec.pl>
34 public $debug_level = 1;
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_mdb2
57 * Instance of rcube_cache
62 private $mailbox = 'INBOX';
63 private $delimiter = NULL;
64 private $namespace = NULL;
65 private $sort_field = '';
66 private $sort_order = 'DESC';
67 private $default_charset = 'ISO-8859-1';
68 private $struct_charset = NULL;
69 private $default_folders = array('INBOX');
70 private $messages_caching = false;
71 private $icache = array();
72 private $uid_id_map = array();
73 private $msg_headers = array();
74 public $search_set = NULL;
75 public $search_string = '';
76 private $search_charset = '';
77 private $search_sort_field = '';
78 private $search_threads = false;
79 private $search_sorted = false;
80 private $db_header_fields = array('idx', 'uid', 'subject', 'from', 'to', 'cc', 'date', 'size');
81 private $options = array('auth_method' => 'check');
82 private $host, $user, $pass, $port, $ssl;
83 private $caching = false;
86 * All (additional) headers used (in any way) by Roundcube
87 * Not listed here: DATE, FROM, TO, CC, REPLY-TO, SUBJECT, CONTENT-TYPE, LIST-POST
88 * (used for messages listing) are hardcoded in rcube_imap_generic::fetchHeaders()
91 * @see rcube_imap::fetch_add_headers
93 private $all_headers = array(
97 'CONTENT-TRANSFER-ENCODING',
112 const ALREADYEXISTS = 6;
113 const NONEXISTENT = 7;
114 const CONTACTADMIN = 8;
118 * Object constructor.
120 function __construct()
122 $this->conn = new rcube_imap_generic();
124 // Set namespace and delimiter from session,
125 // so some methods would work before connection
126 if (isset($_SESSION['imap_namespace']))
127 $this->namespace = $_SESSION['imap_namespace'];
128 if (isset($_SESSION['imap_delimiter']))
129 $this->delimiter = $_SESSION['imap_delimiter'];
134 * Connect to an IMAP server
136 * @param string $host Host to connect
137 * @param string $user Username for IMAP account
138 * @param string $pass Password for IMAP account
139 * @param integer $port Port to connect to
140 * @param string $use_ssl SSL schema (either ssl or tls) or null if plain connection
141 * @return boolean TRUE on success, FALSE on failure
144 function connect($host, $user, $pass, $port=143, $use_ssl=null)
146 // check for OpenSSL support in PHP build
147 if ($use_ssl && extension_loaded('openssl'))
148 $this->options['ssl_mode'] = $use_ssl == 'imaps' ? 'ssl' : $use_ssl;
150 raise_error(array('code' => 403, 'type' => 'imap',
151 'file' => __FILE__, 'line' => __LINE__,
152 'message' => "OpenSSL not available"), true, false);
156 $this->options['port'] = $port;
158 if ($this->options['debug']) {
159 $this->conn->setDebug(true, array($this, 'debug_handler'));
161 $this->options['ident'] = array(
162 'name' => 'Roundcube Webmail',
163 'version' => RCMAIL_VERSION,
164 'php' => PHP_VERSION,
166 'command' => $_SERVER['REQUEST_URI'],
172 $data = rcmail::get_instance()->plugins->exec_hook('imap_connect',
173 array('host' => $host, 'user' => $user, 'attempt' => ++$attempt));
175 if (!empty($data['pass']))
176 $pass = $data['pass'];
178 $this->conn->connect($data['host'], $data['user'], $pass, $this->options);
179 } while(!$this->conn->connected() && $data['retry']);
181 $this->host = $data['host'];
182 $this->user = $data['user'];
185 $this->ssl = $use_ssl;
187 if ($this->conn->connected()) {
188 // get namespace and delimiter
193 else if ($this->conn->error) {
194 if ($pass && $user) {
195 $message = sprintf("Login failed for %s from %s. %s",
196 $user, rcmail_remote_ip(), $this->conn->error);
198 raise_error(array('code' => 403, 'type' => 'imap',
199 'file' => __FILE__, 'line' => __LINE__,
200 'message' => $message), true, false);
209 * Close IMAP connection
210 * Usually done on script shutdown
216 $this->conn->closeConnection();
221 * Close IMAP connection and re-connect
222 * This is used to avoid some strange socket errors when talking to Courier IMAP
228 $this->conn->closeConnection();
229 $connected = $this->connect($this->host, $this->user, $this->pass, $this->port, $this->ssl);
231 // issue SELECT command to restore connection status
232 if ($connected && strlen($this->mailbox))
233 $this->conn->select($this->mailbox);
238 * Returns code of last error
240 * @return int Error code
242 function get_error_code()
244 return $this->conn->errornum;
249 * Returns message of last error
251 * @return string Error message
253 function get_error_str()
255 return $this->conn->error;
260 * Returns code of last command response
262 * @return int Response code
264 function get_response_code()
266 switch ($this->conn->resultcode) {
270 return self::READONLY;
272 return self::TRYCREATE;
276 return self::OVERQUOTA;
277 case 'ALREADYEXISTS':
278 return self::ALREADYEXISTS;
280 return self::NONEXISTENT;
282 return self::CONTACTADMIN;
284 return self::UNKNOWN;
290 * Returns last command response
292 * @return string Response
294 function get_response_str()
296 return $this->conn->result;
301 * Set options to be used in rcube_imap_generic::connect()
303 * @param array $opt Options array
305 function set_options($opt)
307 $this->options = array_merge($this->options, (array)$opt);
312 * Set default message charset
314 * This will be used for message decoding if a charset specification is not available
316 * @param string $cs Charset string
319 function set_charset($cs)
321 $this->default_charset = $cs;
326 * This list of folders will be listed above all other folders
328 * @param array $arr Indexed list of folder names
331 function set_default_mailboxes($arr)
333 if (is_array($arr)) {
334 $this->default_folders = $arr;
336 // add inbox if not included
337 if (!in_array('INBOX', $this->default_folders))
338 array_unshift($this->default_folders, 'INBOX');
344 * Set internal mailbox reference.
346 * All operations will be perfomed on this mailbox/folder
348 * @param string $mailbox Mailbox/Folder name
351 function set_mailbox($mailbox)
353 if ($this->mailbox == $mailbox)
356 $this->mailbox = $mailbox;
358 // clear messagecount cache for this mailbox
359 $this->_clear_messagecount($mailbox);
364 * Forces selection of a mailbox
366 * @param string $mailbox Mailbox/Folder name
369 function select_mailbox($mailbox=null)
371 if (!strlen($mailbox)) {
372 $mailbox = $this->mailbox;
375 $selected = $this->conn->select($mailbox);
377 if ($selected && $this->mailbox != $mailbox) {
378 // clear messagecount cache for this mailbox
379 $this->_clear_messagecount($mailbox);
380 $this->mailbox = $mailbox;
386 * Set internal list page
388 * @param number $page Page number to list
391 function set_page($page)
393 $this->list_page = (int)$page;
398 * Set internal page size
400 * @param number $size Number of messages to display on one page
403 function set_pagesize($size)
405 $this->page_size = (int)$size;
410 * Save a set of message ids for future message listing methods
412 * @param string IMAP Search query
413 * @param array List of message ids or NULL if empty
414 * @param string Charset of search string
415 * @param string Sorting field
416 * @param string True if set is sorted (SORT was used for searching)
418 function set_search_set($str=null, $msgs=null, $charset=null, $sort_field=null, $threads=false, $sorted=false)
420 if (is_array($str) && $msgs == null)
421 list($str, $msgs, $charset, $sort_field, $threads, $sorted) = $str;
424 else if ($msgs != null && !is_array($msgs))
425 $msgs = explode(',', $msgs);
427 $this->search_string = $str;
428 $this->search_set = $msgs;
429 $this->search_charset = $charset;
430 $this->search_sort_field = $sort_field;
431 $this->search_threads = $threads;
432 $this->search_sorted = $sorted;
437 * Return the saved search set as hash array
438 * @return array Search set
440 function get_search_set()
442 return array($this->search_string,
444 $this->search_charset,
445 $this->search_sort_field,
446 $this->search_threads,
447 $this->search_sorted,
453 * Returns the currently used mailbox name
455 * @return string Name of the mailbox/folder
458 function get_mailbox_name()
460 return $this->conn->connected() ? $this->mailbox : '';
465 * Returns the IMAP server's capability
467 * @param string $cap Capability name
468 * @return mixed Capability value or TRUE if supported, FALSE if not
471 function get_capability($cap)
473 return $this->conn->getCapability(strtoupper($cap));
478 * Sets threading flag to the best supported THREAD algorithm
480 * @param boolean $enable TRUE to enable and FALSE
481 * @return string Algorithm or false if THREAD is not supported
484 function set_threading($enable=false)
486 $this->threading = false;
488 if ($enable && ($caps = $this->get_capability('THREAD'))) {
489 if (in_array('REFS', $caps))
490 $this->threading = 'REFS';
491 else if (in_array('REFERENCES', $caps))
492 $this->threading = 'REFERENCES';
493 else if (in_array('ORDEREDSUBJECT', $caps))
494 $this->threading = 'ORDEREDSUBJECT';
497 return $this->threading;
502 * Checks the PERMANENTFLAGS capability of the current mailbox
503 * and returns true if the given flag is supported by the IMAP server
505 * @param string $flag Permanentflag name
506 * @return boolean True if this flag is supported
509 function check_permflag($flag)
511 $flag = strtoupper($flag);
512 $imap_flag = $this->conn->flags[$flag];
513 return (in_array_nocase($imap_flag, $this->conn->data['PERMANENTFLAGS']));
518 * Returns the delimiter that is used by the IMAP server for folder separation
520 * @return string Delimiter string
523 function get_hierarchy_delimiter()
525 return $this->delimiter;
532 * @param string $name Namespace array index: personal, other, shared, prefix
534 * @return array Namespace data
537 function get_namespace($name=null)
539 $ns = $this->namespace;
542 return isset($ns[$name]) ? $ns[$name] : null;
545 unset($ns['prefix']);
551 * Sets delimiter and namespaces
555 private function set_env()
557 if ($this->delimiter !== null && $this->namespace !== null) {
561 $config = rcmail::get_instance()->config;
562 $imap_personal = $config->get('imap_ns_personal');
563 $imap_other = $config->get('imap_ns_other');
564 $imap_shared = $config->get('imap_ns_shared');
565 $imap_delimiter = $config->get('imap_delimiter');
567 if (!$this->conn->connected())
570 $ns = $this->conn->getNamespace();
572 // Set namespaces (NAMESPACE supported)
574 $this->namespace = $ns;
577 $this->namespace = array(
584 if ($imap_delimiter) {
585 $this->delimiter = $imap_delimiter;
587 if (empty($this->delimiter)) {
588 $this->delimiter = $this->namespace['personal'][0][1];
590 if (empty($this->delimiter)) {
591 $this->delimiter = $this->conn->getHierarchyDelimiter();
593 if (empty($this->delimiter)) {
594 $this->delimiter = '/';
597 // Overwrite namespaces
598 if ($imap_personal !== null) {
599 $this->namespace['personal'] = NULL;
600 foreach ((array)$imap_personal as $dir) {
601 $this->namespace['personal'][] = array($dir, $this->delimiter);
604 if ($imap_other !== null) {
605 $this->namespace['other'] = NULL;
606 foreach ((array)$imap_other as $dir) {
608 $this->namespace['other'][] = array($dir, $this->delimiter);
612 if ($imap_shared !== null) {
613 $this->namespace['shared'] = NULL;
614 foreach ((array)$imap_shared as $dir) {
616 $this->namespace['shared'][] = array($dir, $this->delimiter);
621 // Find personal namespace prefix for mod_mailbox()
622 // Prefix can be removed when there is only one personal namespace
623 if (is_array($this->namespace['personal']) && count($this->namespace['personal']) == 1) {
624 $this->namespace['prefix'] = $this->namespace['personal'][0][0];
627 $_SESSION['imap_namespace'] = $this->namespace;
628 $_SESSION['imap_delimiter'] = $this->delimiter;
633 * Get message count for a specific mailbox
635 * @param string $mailbox Mailbox/folder name
636 * @param string $mode Mode for count [ALL|THREADS|UNSEEN|RECENT]
637 * @param boolean $force Force reading from server and update cache
638 * @param boolean $status Enables storing folder status info (max UID/count),
639 * required for mailbox_status()
640 * @return int Number of messages
643 function messagecount($mailbox='', $mode='ALL', $force=false, $status=true)
645 if (!strlen($mailbox)) {
646 $mailbox = $this->mailbox;
649 return $this->_messagecount($mailbox, $mode, $force, $status);
654 * Private method for getting nr of messages
656 * @param string $mailbox Mailbox 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
663 * @see rcube_imap::messagecount()
665 private function _messagecount($mailbox, $mode='ALL', $force=false, $status=true)
667 $mode = strtoupper($mode);
670 if ($this->search_string && $mailbox == $this->mailbox && ($mode == 'ALL' || $mode == 'THREADS') && !$force) {
671 if ($this->search_threads)
672 return $mode == 'ALL' ? count((array)$this->search_set['depth']) : count((array)$this->search_set['tree']);
674 return count((array)$this->search_set);
677 $a_mailbox_cache = $this->get_cache('messagecount');
679 // return cached value
680 if (!$force && is_array($a_mailbox_cache[$mailbox]) && isset($a_mailbox_cache[$mailbox][$mode]))
681 return $a_mailbox_cache[$mailbox][$mode];
683 if (!is_array($a_mailbox_cache[$mailbox]))
684 $a_mailbox_cache[$mailbox] = array();
686 if ($mode == 'THREADS') {
687 $res = $this->_threadcount($mailbox, $msg_count);
688 $count = $res['count'];
691 $this->set_folder_stats($mailbox, 'cnt', $res['msgcount']);
692 $this->set_folder_stats($mailbox, 'maxuid', $res['maxuid'] ? $this->_id2uid($res['maxuid'], $mailbox) : 0);
695 // RECENT count is fetched a bit different
696 else if ($mode == 'RECENT') {
697 $count = $this->conn->countRecent($mailbox);
699 // use SEARCH for message counting
700 else if ($this->skip_deleted) {
701 $search_str = "ALL UNDELETED";
702 $keys = array('COUNT');
705 if ($mode == 'UNSEEN') {
706 $search_str .= " UNSEEN";
709 if ($this->messages_caching) {
718 // get message count using (E)SEARCH
719 // not very performant but more precise (using UNDELETED)
720 $index = $this->conn->search($mailbox, $search_str, $need_uid, $keys);
722 $count = is_array($index) ? $index['COUNT'] : 0;
724 if ($mode == 'ALL') {
725 if ($need_uid && $this->messages_caching) {
726 // Save messages index for check_cache_status()
727 $this->icache['all_undeleted_idx'] = $index['ALL'];
730 $this->set_folder_stats($mailbox, 'cnt', $count);
731 $this->set_folder_stats($mailbox, 'maxuid', is_array($index) ? $index['MAX'] : 0);
736 if ($mode == 'UNSEEN')
737 $count = $this->conn->countUnseen($mailbox);
739 $count = $this->conn->countMessages($mailbox);
741 $this->set_folder_stats($mailbox,'cnt', $count);
742 $this->set_folder_stats($mailbox, 'maxuid', $count ? $this->_id2uid($count, $mailbox) : 0);
747 $a_mailbox_cache[$mailbox][$mode] = (int)$count;
749 // write back to cache
750 $this->update_cache('messagecount', $a_mailbox_cache);
757 * Private method for getting nr of threads
759 * @param string $mailbox Folder name
761 * @returns array Array containing items: 'count' - threads count,
762 * 'msgcount' = messages count, 'maxuid' = max. UID in the set
765 private function _threadcount($mailbox)
769 if (!empty($this->icache['threads'])) {
770 $dcount = count($this->icache['threads']['depth']);
772 'count' => count($this->icache['threads']['tree']),
773 'msgcount' => $dcount,
774 'maxuid' => $dcount ? max(array_keys($this->icache['threads']['depth'])) : 0,
777 else if (is_array($result = $this->_fetch_threads($mailbox))) {
778 $dcount = count($result[1]);
780 'count' => count($result[0]),
781 'msgcount' => $dcount,
782 'maxuid' => $dcount ? max(array_keys($result[1])) : 0,
791 * Public method for listing headers
792 * convert mailbox name with root dir first
794 * @param string $mailbox Mailbox/folder name
795 * @param int $page Current page to list
796 * @param string $sort_field Header field to sort by
797 * @param string $sort_order Sort order [ASC|DESC]
798 * @param int $slice Number of slice items to extract from result array
799 * @return array Indexed array with message header objects
802 function list_headers($mailbox='', $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
804 if (!strlen($mailbox)) {
805 $mailbox = $this->mailbox;
808 return $this->_list_headers($mailbox, $page, $sort_field, $sort_order, false, $slice);
813 * Private method for listing message headers
815 * @param string $mailbox Mailbox 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
822 * @see rcube_imap::list_headers
824 private function _list_headers($mailbox='', $page=NULL, $sort_field=NULL, $sort_order=NULL, $recursive=false, $slice=0)
826 if (!strlen($mailbox))
829 // use saved message set
830 if ($this->search_string && $mailbox == $this->mailbox)
831 return $this->_list_header_set($mailbox, $page, $sort_field, $sort_order, $slice);
833 if ($this->threading)
834 return $this->_list_thread_headers($mailbox, $page, $sort_field, $sort_order, $recursive, $slice);
836 $this->_set_sort_order($sort_field, $sort_order);
838 $page = $page ? $page : $this->list_page;
839 $cache_key = $mailbox.'.msg';
841 if ($this->messages_caching) {
842 // cache is OK, we can get messages from local cache
843 // (assume cache is in sync when in recursive mode)
844 if ($recursive || $this->check_cache_status($mailbox, $cache_key)>0) {
845 $start_msg = ($page-1) * $this->page_size;
846 $a_msg_headers = $this->get_message_cache($cache_key, $start_msg,
847 $start_msg+$this->page_size, $this->sort_field, $this->sort_order);
848 $result = array_values($a_msg_headers);
850 $result = array_slice($result, -$slice, $slice);
853 // cache is incomplete, sync it (all messages in the folder)
854 else if (!$recursive) {
855 $this->sync_header_index($mailbox);
856 return $this->_list_headers($mailbox, $page, $this->sort_field, $this->sort_order, true, $slice);
860 // retrieve headers from IMAP
861 $a_msg_headers = array();
863 // use message index sort as default sorting (for better performance)
864 if (!$this->sort_field) {
865 if ($this->skip_deleted) {
866 // @TODO: this could be cached
867 if ($msg_index = $this->_search_index($mailbox, 'ALL UNDELETED')) {
868 $max = max($msg_index);
869 list($begin, $end) = $this->_get_message_range(count($msg_index), $page);
870 $msg_index = array_slice($msg_index, $begin, $end-$begin);
873 else if ($max = $this->conn->countMessages($mailbox)) {
874 list($begin, $end) = $this->_get_message_range($max, $page);
875 $msg_index = range($begin+1, $end);
878 $msg_index = array();
880 if ($slice && $msg_index)
881 $msg_index = array_slice($msg_index, ($this->sort_order == 'DESC' ? 0 : -$slice), $slice);
883 // fetch reqested headers from server
885 $this->_fetch_headers($mailbox, join(",", $msg_index), $a_msg_headers, $cache_key);
888 else if ($this->get_capability('SORT') &&
889 // Courier-IMAP provides SORT capability but allows to disable it by admin (#1486959)
890 ($msg_index = $this->conn->sort($mailbox, $this->sort_field, $this->skip_deleted ? 'UNDELETED' : '')) !== false
892 if (!empty($msg_index)) {
893 list($begin, $end) = $this->_get_message_range(count($msg_index), $page);
894 $max = max($msg_index);
895 $msg_index = array_slice($msg_index, $begin, $end-$begin);
898 $msg_index = array_slice($msg_index, ($this->sort_order == 'DESC' ? 0 : -$slice), $slice);
900 // fetch reqested headers from server
901 $this->_fetch_headers($mailbox, join(',', $msg_index), $a_msg_headers, $cache_key);
904 // fetch specified header for all messages and sort
905 else if ($a_index = $this->conn->fetchHeaderIndex($mailbox, "1:*", $this->sort_field, $this->skip_deleted)) {
906 asort($a_index); // ASC
907 $msg_index = array_keys($a_index);
908 $max = max($msg_index);
909 list($begin, $end) = $this->_get_message_range(count($msg_index), $page);
910 $msg_index = array_slice($msg_index, $begin, $end-$begin);
913 $msg_index = array_slice($msg_index, ($this->sort_order == 'DESC' ? 0 : -$slice), $slice);
915 // fetch reqested headers from server
916 $this->_fetch_headers($mailbox, join(",", $msg_index), $a_msg_headers, $cache_key);
919 // delete cached messages with a higher index than $max+1
920 // Changed $max to $max+1 to fix this bug : #1484295
921 $this->clear_message_cache($cache_key, $max + 1);
923 // kick child process to sync cache
926 // return empty array if no messages found
927 if (!is_array($a_msg_headers) || empty($a_msg_headers))
930 // use this class for message sorting
931 $sorter = new rcube_header_sorter();
932 $sorter->set_sequence_numbers($msg_index);
933 $sorter->sort_headers($a_msg_headers);
935 if ($this->sort_order == 'DESC')
936 $a_msg_headers = array_reverse($a_msg_headers);
938 return array_values($a_msg_headers);
943 * Private method for listing message headers using threads
945 * @param string $mailbox Mailbox/folder name
946 * @param int $page Current page to list
947 * @param string $sort_field Header field to sort by
948 * @param string $sort_order Sort order [ASC|DESC]
949 * @param boolean $recursive True if called recursively
950 * @param int $slice Number of slice items to extract from result array
951 * @return array Indexed array with message header objects
953 * @see rcube_imap::list_headers
955 private function _list_thread_headers($mailbox, $page=NULL, $sort_field=NULL, $sort_order=NULL, $recursive=false, $slice=0)
957 $this->_set_sort_order($sort_field, $sort_order);
959 $page = $page ? $page : $this->list_page;
960 // $cache_key = $mailbox.'.msg';
961 // $cache_status = $this->check_cache_status($mailbox, $cache_key);
963 // get all threads (default sort order)
964 list ($thread_tree, $msg_depth, $has_children) = $this->_fetch_threads($mailbox);
966 if (empty($thread_tree))
969 $msg_index = $this->_sort_threads($mailbox, $thread_tree);
971 return $this->_fetch_thread_headers($mailbox,
972 $thread_tree, $msg_depth, $has_children, $msg_index, $page, $slice);
977 * Private method for fetching threads data
979 * @param string $mailbox Mailbox/folder name
980 * @return array Array with thread data
983 private function _fetch_threads($mailbox)
985 if (empty($this->icache['threads'])) {
987 $result = $this->conn->thread($mailbox, $this->threading,
988 $this->skip_deleted ? 'UNDELETED' : '');
990 // add to internal (fast) cache
991 $this->icache['threads'] = array();
992 $this->icache['threads']['tree'] = is_array($result) ? $result[0] : array();
993 $this->icache['threads']['depth'] = is_array($result) ? $result[1] : array();
994 $this->icache['threads']['has_children'] = is_array($result) ? $result[2] : array();
998 $this->icache['threads']['tree'],
999 $this->icache['threads']['depth'],
1000 $this->icache['threads']['has_children'],
1006 * Private method for fetching threaded messages headers
1008 * @param string $mailbox Mailbox name
1009 * @param array $thread_tree Thread tree data
1010 * @param array $msg_depth Thread depth data
1011 * @param array $has_children Thread children data
1012 * @param array $msg_index Messages index
1013 * @param int $page List page number
1014 * @param int $slice Number of threads to slice
1015 * @return array Messages headers
1018 private function _fetch_thread_headers($mailbox, $thread_tree, $msg_depth, $has_children, $msg_index, $page, $slice=0)
1020 $cache_key = $mailbox.'.msg';
1021 // now get IDs for current page
1022 list($begin, $end) = $this->_get_message_range(count($msg_index), $page);
1023 $msg_index = array_slice($msg_index, $begin, $end-$begin);
1026 $msg_index = array_slice($msg_index, ($this->sort_order == 'DESC' ? 0 : -$slice), $slice);
1028 if ($this->sort_order == 'DESC')
1029 $msg_index = array_reverse($msg_index);
1031 // flatten threads array
1032 // @TODO: fetch children only in expanded mode (?)
1034 foreach ($msg_index as $root) {
1036 if (!empty($thread_tree[$root]))
1037 $all_ids = array_merge($all_ids, array_keys_recursive($thread_tree[$root]));
1040 // fetch reqested headers from server
1041 $this->_fetch_headers($mailbox, $all_ids, $a_msg_headers, $cache_key);
1043 // return empty array if no messages found
1044 if (!is_array($a_msg_headers) || empty($a_msg_headers))
1047 // use this class for message sorting
1048 $sorter = new rcube_header_sorter();
1049 $sorter->set_sequence_numbers($all_ids);
1050 $sorter->sort_headers($a_msg_headers);
1052 // Set depth, has_children and unread_children fields in headers
1053 $this->_set_thread_flags($a_msg_headers, $msg_depth, $has_children);
1055 return array_values($a_msg_headers);
1060 * Private method for setting threaded messages flags:
1061 * depth, has_children and unread_children
1063 * @param array $headers Reference to headers array indexed by message ID
1064 * @param array $msg_depth Array of messages depth indexed by message ID
1065 * @param array $msg_children Array of messages children flags indexed by message ID
1066 * @return array Message headers array indexed by message ID
1069 private function _set_thread_flags(&$headers, $msg_depth, $msg_children)
1073 foreach ($headers as $idx => $header) {
1075 $depth = $msg_depth[$id];
1076 $parents = array_slice($parents, 0, $depth);
1078 if (!empty($parents)) {
1079 $headers[$idx]->parent_uid = end($parents);
1081 $headers[$parents[0]]->unread_children++;
1083 array_push($parents, $header->uid);
1085 $headers[$idx]->depth = $depth;
1086 $headers[$idx]->has_children = $msg_children[$id];
1092 * Private method for listing a set of message headers (search results)
1094 * @param string $mailbox Mailbox/folder name
1095 * @param int $page Current page to list
1096 * @param string $sort_field Header field to sort by
1097 * @param string $sort_order Sort order [ASC|DESC]
1098 * @param int $slice Number of slice items to extract from result array
1099 * @return array Indexed array with message header objects
1101 * @see rcube_imap::list_header_set()
1103 private function _list_header_set($mailbox, $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
1105 if (!strlen($mailbox) || empty($this->search_set))
1108 // use saved messages from searching
1109 if ($this->threading)
1110 return $this->_list_thread_header_set($mailbox, $page, $sort_field, $sort_order, $slice);
1112 // search set is threaded, we need a new one
1113 if ($this->search_threads) {
1114 if (empty($this->search_set['tree']))
1116 $this->search('', $this->search_string, $this->search_charset, $sort_field);
1119 $msgs = $this->search_set;
1120 $a_msg_headers = array();
1121 $page = $page ? $page : $this->list_page;
1122 $start_msg = ($page-1) * $this->page_size;
1124 $this->_set_sort_order($sort_field, $sort_order);
1126 // quickest method (default sorting)
1127 if (!$this->search_sort_field && !$this->sort_field) {
1128 if ($sort_order == 'DESC')
1129 $msgs = array_reverse($msgs);
1131 // get messages uids for one page
1132 $msgs = array_slice(array_values($msgs), $start_msg, min(count($msgs)-$start_msg, $this->page_size));
1135 $msgs = array_slice($msgs, -$slice, $slice);
1138 $this->_fetch_headers($mailbox, join(',',$msgs), $a_msg_headers, NULL);
1140 // I didn't found in RFC that FETCH always returns messages sorted by index
1141 $sorter = new rcube_header_sorter();
1142 $sorter->set_sequence_numbers($msgs);
1143 $sorter->sort_headers($a_msg_headers);
1145 return array_values($a_msg_headers);
1148 // sorted messages, so we can first slice array and then fetch only wanted headers
1149 if ($this->search_sorted) { // SORT searching result
1150 // reset search set if sorting field has been changed
1151 if ($this->sort_field && $this->search_sort_field != $this->sort_field)
1152 $msgs = $this->search('', $this->search_string, $this->search_charset, $this->sort_field);
1154 // return empty array if no messages found
1158 if ($sort_order == 'DESC')
1159 $msgs = array_reverse($msgs);
1161 // get messages uids for one page
1162 $msgs = array_slice(array_values($msgs), $start_msg, min(count($msgs)-$start_msg, $this->page_size));
1165 $msgs = array_slice($msgs, -$slice, $slice);
1168 $this->_fetch_headers($mailbox, join(',',$msgs), $a_msg_headers, NULL);
1170 $sorter = new rcube_header_sorter();
1171 $sorter->set_sequence_numbers($msgs);
1172 $sorter->sort_headers($a_msg_headers);
1174 return array_values($a_msg_headers);
1176 else { // SEARCH result, need sorting
1177 $cnt = count($msgs);
1178 // 300: experimantal value for best result
1179 if (($cnt > 300 && $cnt > $this->page_size) || !$this->sort_field) {
1180 // use memory less expensive (and quick) method for big result set
1181 $a_index = $this->message_index('', $this->sort_field, $this->sort_order);
1182 // get messages uids for one page...
1183 $msgs = array_slice($a_index, $start_msg, min($cnt-$start_msg, $this->page_size));
1185 $msgs = array_slice($msgs, -$slice, $slice);
1186 // ...and fetch headers
1187 $this->_fetch_headers($mailbox, join(',', $msgs), $a_msg_headers, NULL);
1189 // return empty array if no messages found
1190 if (!is_array($a_msg_headers) || empty($a_msg_headers))
1193 $sorter = new rcube_header_sorter();
1194 $sorter->set_sequence_numbers($msgs);
1195 $sorter->sort_headers($a_msg_headers);
1197 return array_values($a_msg_headers);
1200 // for small result set we can fetch all messages headers
1201 $this->_fetch_headers($mailbox, join(',', $msgs), $a_msg_headers, NULL);
1203 // return empty array if no messages found
1204 if (!is_array($a_msg_headers) || empty($a_msg_headers))
1207 // if not already sorted
1208 $a_msg_headers = $this->conn->sortHeaders(
1209 $a_msg_headers, $this->sort_field, $this->sort_order);
1211 // only return the requested part of the set
1212 $a_msg_headers = array_slice(array_values($a_msg_headers),
1213 $start_msg, min($cnt-$start_msg, $this->page_size));
1216 $a_msg_headers = array_slice($a_msg_headers, -$slice, $slice);
1218 return $a_msg_headers;
1225 * Private method for listing a set of threaded message headers (search results)
1227 * @param string $mailbox Mailbox/folder name
1228 * @param int $page Current page to list
1229 * @param string $sort_field Header field to sort by
1230 * @param string $sort_order Sort order [ASC|DESC]
1231 * @param int $slice Number of slice items to extract from result array
1232 * @return array Indexed array with message header objects
1234 * @see rcube_imap::list_header_set()
1236 private function _list_thread_header_set($mailbox, $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
1238 // update search_set if previous data was fetched with disabled threading
1239 if (!$this->search_threads) {
1240 if (empty($this->search_set))
1242 $this->search('', $this->search_string, $this->search_charset, $sort_field);
1246 if (empty($this->search_set['tree']))
1249 $thread_tree = $this->search_set['tree'];
1250 $msg_depth = $this->search_set['depth'];
1251 $has_children = $this->search_set['children'];
1252 $a_msg_headers = array();
1254 $page = $page ? $page : $this->list_page;
1255 $start_msg = ($page-1) * $this->page_size;
1257 $this->_set_sort_order($sort_field, $sort_order);
1259 $msg_index = $this->_sort_threads($mailbox, $thread_tree, array_keys($msg_depth));
1261 return $this->_fetch_thread_headers($mailbox,
1262 $thread_tree, $msg_depth, $has_children, $msg_index, $page, $slice=0);
1267 * Helper function to get first and last index of the requested set
1269 * @param int $max Messages count
1270 * @param mixed $page Page number to show, or string 'all'
1271 * @return array Array with two values: first index, last index
1274 private function _get_message_range($max, $page)
1276 $start_msg = ($page-1) * $this->page_size;
1282 else if ($this->sort_order=='DESC') {
1283 $begin = $max - $this->page_size - $start_msg;
1284 $end = $max - $start_msg;
1287 $begin = $start_msg;
1288 $end = $start_msg + $this->page_size;
1291 if ($begin < 0) $begin = 0;
1292 if ($end < 0) $end = $max;
1293 if ($end > $max) $end = $max;
1295 return array($begin, $end);
1300 * Fetches message headers (used for loop)
1302 * @param string $mailbox Mailbox name
1303 * @param string $msgs Message index to fetch
1304 * @param array $a_msg_headers Reference to message headers array
1305 * @param string $cache_key Cache index key
1306 * @return int Messages count
1309 private function _fetch_headers($mailbox, $msgs, &$a_msg_headers, $cache_key)
1311 // fetch reqested headers from server
1312 $a_header_index = $this->conn->fetchHeaders(
1313 $mailbox, $msgs, false, false, $this->get_fetch_headers());
1315 if (empty($a_header_index))
1318 foreach ($a_header_index as $i => $headers) {
1319 $a_msg_headers[$headers->uid] = $headers;
1323 if ($this->messages_caching && $cache_key) {
1324 // cache is incomplete?
1325 $cache_index = $this->get_message_cache_index($cache_key);
1327 foreach ($a_header_index as $headers) {
1329 if ($cache_index[$headers->id] == $headers->uid) {
1330 unset($cache_index[$headers->id]);
1333 // wrong UID at this position
1334 if ($cache_index[$headers->id]) {
1335 $for_remove[] = $cache_index[$headers->id];
1336 unset($cache_index[$headers->id]);
1338 // message UID in cache but at wrong position
1339 if (is_int($key = array_search($headers->uid, $cache_index))) {
1340 $for_remove[] = $cache_index[$key];
1341 unset($cache_index[$key]);
1344 $for_create[] = $headers->uid;
1348 $this->remove_message_cache($cache_key, $for_remove);
1350 // add messages to cache
1351 foreach ((array)$for_create as $uid) {
1352 $headers = $a_msg_headers[$uid];
1353 $this->add_message_cache($cache_key, $headers->id, $headers, NULL, true);
1357 return count($a_msg_headers);
1362 * Returns current status of mailbox
1364 * We compare the maximum UID to determine the number of
1365 * new messages because the RECENT flag is not reliable.
1367 * @param string $mailbox Mailbox/folder name
1368 * @return int Folder status
1370 function mailbox_status($mailbox = null)
1372 if (!strlen($mailbox)) {
1373 $mailbox = $this->mailbox;
1375 $old = $this->get_folder_stats($mailbox);
1377 // refresh message count -> will update
1378 $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 $cache_key = $mailbox.'.msg';
1496 $cache_status = $this->check_cache_status($mailbox, $cache_key);
1499 if ($cache_status>0) {
1500 $a_index = $this->get_message_cache_index($cache_key,
1501 $this->sort_field, $this->sort_order);
1502 return array_keys($a_index);
1505 // use message index sort as default sorting
1506 if (!$this->sort_field) {
1507 if ($this->skip_deleted) {
1508 $a_index = $this->conn->search($mailbox, 'ALL UNDELETED');
1509 // I didn't found that SEARCH should return sorted IDs
1510 if (is_array($a_index))
1512 } else if ($max = $this->_messagecount($mailbox)) {
1513 $a_index = range(1, $max);
1516 if ($a_index !== false && $this->sort_order == 'DESC')
1517 $a_index = array_reverse($a_index);
1519 $this->icache[$key] = $a_index;
1521 // fetch complete message index
1522 else if ($this->get_capability('SORT') &&
1523 ($a_index = $this->conn->sort($mailbox,
1524 $this->sort_field, $this->skip_deleted ? 'UNDELETED' : '')) !== false
1526 if ($this->sort_order == 'DESC')
1527 $a_index = array_reverse($a_index);
1529 $this->icache[$key] = $a_index;
1531 else if ($a_index = $this->conn->fetchHeaderIndex(
1532 $mailbox, "1:*", $this->sort_field, $this->skip_deleted)) {
1533 if ($this->sort_order=="ASC")
1535 else if ($this->sort_order=="DESC")
1538 $this->icache[$key] = array_keys($a_index);
1541 return $this->icache[$key] !== false ? $this->icache[$key] : array();
1546 * Return sorted array of threaded message IDs (not UIDs)
1548 * @param string $mailbox Mailbox to get index from
1549 * @param string $sort_field Sort column
1550 * @param string $sort_order Sort order [ASC, DESC]
1551 * @return array Indexed array with message IDs
1553 function thread_index($mailbox='', $sort_field=NULL, $sort_order=NULL)
1555 $this->_set_sort_order($sort_field, $sort_order);
1557 if (!strlen($mailbox)) {
1558 $mailbox = $this->mailbox;
1560 $key = "{$mailbox}:{$this->sort_field}:{$this->sort_order}:{$this->search_string}.thi";
1562 // we have a saved search result, get index from there
1563 if (!isset($this->icache[$key]) && $this->search_string
1564 && $this->search_threads && $mailbox == $this->mailbox) {
1565 // use message IDs for better performance
1566 $ids = array_keys_recursive($this->search_set['tree']);
1567 $this->icache[$key] = $this->_flatten_threads($mailbox, $this->search_set['tree'], $ids);
1570 // have stored it in RAM
1571 if (isset($this->icache[$key]))
1572 return $this->icache[$key];
1574 // check local cache
1575 $cache_key = $mailbox.'.msg';
1576 $cache_status = $this->check_cache_status($mailbox, $cache_key);
1579 if ($cache_status>0) {
1580 $a_index = $this->get_message_cache_index($cache_key, $this->sort_field, $this->sort_order);
1581 return array_keys($a_index);
1584 // get all threads (default sort order)
1585 list ($thread_tree) = $this->_fetch_threads($mailbox);
1587 $this->icache[$key] = $this->_flatten_threads($mailbox, $thread_tree);
1589 return $this->icache[$key];
1594 * Return array of threaded messages (all, not only roots)
1596 * @param string $mailbox Mailbox to get index from
1597 * @param array $thread_tree Threaded messages array (see _fetch_threads())
1598 * @param array $ids Message IDs if we know what we need (e.g. search result)
1599 * for better performance
1600 * @return array Indexed array with message IDs
1604 private function _flatten_threads($mailbox, $thread_tree, $ids=null)
1606 if (empty($thread_tree))
1609 $msg_index = $this->_sort_threads($mailbox, $thread_tree, $ids);
1611 if ($this->sort_order == 'DESC')
1612 $msg_index = array_reverse($msg_index);
1614 // flatten threads array
1616 foreach ($msg_index as $root) {
1618 if (!empty($thread_tree[$root])) {
1619 foreach (array_keys_recursive($thread_tree[$root]) as $val)
1629 * @param string $mailbox Mailbox name
1632 private function sync_header_index($mailbox)
1634 $cache_key = $mailbox.'.msg';
1635 $cache_index = $this->get_message_cache_index($cache_key);
1638 // cache is empty, get all messages
1639 if (is_array($cache_index) && empty($cache_index)) {
1640 $max = $this->_messagecount($mailbox);
1641 // syncing a big folder maybe slow
1644 $end = min($chunk_size, $max);
1646 // do this in loop to save memory (1000 msgs ~= 10 MB)
1647 if ($headers = $this->conn->fetchHeaders($mailbox,
1648 "$start:$end", false, false, $this->get_fetch_headers())
1650 foreach ($headers as $header) {
1651 $this->add_message_cache($cache_key, $header->id, $header, NULL, true);
1654 if ($end - $start < $chunk_size - 1)
1657 $end = min($end+$chunk_size, $max);
1658 $start += $chunk_size;
1663 // fetch complete message index
1664 if (isset($this->icache['folder_index']))
1665 $a_message_index = &$this->icache['folder_index'];
1667 $a_message_index = $this->conn->fetchHeaderIndex($mailbox, "1:*", 'UID', $this->skip_deleted);
1669 if ($a_message_index === false || $cache_index === null)
1672 // compare cache index with real index
1673 foreach ($a_message_index as $id => $uid) {
1674 // message in cache at correct position
1675 if ($cache_index[$id] == $uid) {
1676 unset($cache_index[$id]);
1680 // other message at this position
1681 if (isset($cache_index[$id])) {
1682 $for_remove[] = $cache_index[$id];
1683 unset($cache_index[$id]);
1686 // message in cache but at wrong position
1687 if (is_int($key = array_search($uid, $cache_index))) {
1688 $for_remove[] = $uid;
1689 unset($cache_index[$key]);
1692 $for_update[] = $id;
1695 // remove messages at wrong positions and those deleted that are still in cache_index
1696 if (!empty($for_remove))
1697 $cache_index = array_merge($cache_index, $for_remove);
1699 if (!empty($cache_index))
1700 $this->remove_message_cache($cache_key, $cache_index);
1702 // fetch complete headers and add to cache
1703 if (!empty($for_update)) {
1704 // syncing a big folder maybe slow
1706 // To save memory do this in chunks
1707 $for_update = array_chunk($for_update, $chunk_size);
1708 foreach ($for_update as $uids) {
1709 if ($headers = $this->conn->fetchHeaders($mailbox,
1710 $uids, false, false, $this->get_fetch_headers())
1712 foreach ($headers as $header) {
1713 $this->add_message_cache($cache_key, $header->id, $header, NULL, true);
1722 * Invoke search request to IMAP server
1724 * @param string $mailbox Mailbox name to search in
1725 * @param string $str Search criteria
1726 * @param string $charset Search charset
1727 * @param string $sort_field Header field to sort by
1728 * @return array search results as list of message IDs
1731 function search($mailbox='', $str=NULL, $charset=NULL, $sort_field=NULL)
1736 if (!strlen($mailbox)) {
1737 $mailbox = $this->mailbox;
1740 $results = $this->_search_index($mailbox, $str, $charset, $sort_field);
1742 $this->set_search_set($str, $results, $charset, $sort_field, (bool)$this->threading,
1743 $this->threading || $this->search_sorted ? true : false);
1750 * Private search method
1752 * @param string $mailbox Mailbox name
1753 * @param string $criteria Search criteria
1754 * @param string $charset Charset
1755 * @param string $sort_field Sorting field
1756 * @return array search results as list of message ids
1758 * @see rcube_imap::search()
1760 private function _search_index($mailbox, $criteria='ALL', $charset=NULL, $sort_field=NULL)
1762 $orig_criteria = $criteria;
1764 if ($this->skip_deleted && !preg_match('/UNDELETED/', $criteria))
1765 $criteria = 'UNDELETED '.$criteria;
1767 if ($this->threading) {
1768 $a_messages = $this->conn->thread($mailbox, $this->threading, $criteria, $charset);
1770 // Error, try with US-ASCII (RFC5256: SORT/THREAD must support US-ASCII and UTF-8,
1771 // but I've seen that Courier doesn't support UTF-8)
1772 if ($a_messages === false && $charset && $charset != 'US-ASCII')
1773 $a_messages = $this->conn->thread($mailbox, $this->threading,
1774 $this->convert_criteria($criteria, $charset), 'US-ASCII');
1776 if ($a_messages !== false) {
1777 list ($thread_tree, $msg_depth, $has_children) = $a_messages;
1778 $a_messages = array(
1779 'tree' => $thread_tree,
1780 'depth' => $msg_depth,
1781 'children' => $has_children
1788 if ($sort_field && $this->get_capability('SORT')) {
1789 $charset = $charset ? $charset : $this->default_charset;
1790 $a_messages = $this->conn->sort($mailbox, $sort_field, $criteria, false, $charset);
1792 // Error, try with US-ASCII (RFC5256: SORT/THREAD must support US-ASCII and UTF-8,
1793 // but I've seen that Courier doesn't support UTF-8)
1794 if ($a_messages === false && $charset && $charset != 'US-ASCII')
1795 $a_messages = $this->conn->sort($mailbox, $sort_field,
1796 $this->convert_criteria($criteria, $charset), false, 'US-ASCII');
1798 if ($a_messages !== false) {
1799 $this->search_sorted = true;
1804 if ($orig_criteria == 'ALL') {
1805 $max = $this->_messagecount($mailbox);
1806 $a_messages = $max ? range(1, $max) : array();
1809 $a_messages = $this->conn->search($mailbox,
1810 ($charset ? "CHARSET $charset " : '') . $criteria);
1812 // Error, try with US-ASCII (some servers may support only US-ASCII)
1813 if ($a_messages === false && $charset && $charset != 'US-ASCII')
1814 $a_messages = $this->conn->search($mailbox,
1815 'CHARSET US-ASCII ' . $this->convert_criteria($criteria, $charset));
1817 // I didn't found that SEARCH should return sorted IDs
1818 if (is_array($a_messages) && !$this->sort_field)
1822 $this->search_sorted = false;
1829 * Direct (real and simple) SEARCH request to IMAP server,
1830 * without result sorting and caching
1832 * @param string $mailbox Mailbox name to search in
1833 * @param string $str Search string
1834 * @param boolean $ret_uid True if UIDs should be returned
1835 * @return array Search results as list of message IDs or UIDs
1838 function search_once($mailbox='', $str=NULL, $ret_uid=false)
1843 if (!strlen($mailbox)) {
1844 $mailbox = $this->mailbox;
1847 return $this->conn->search($mailbox, $str, $ret_uid);
1852 * Converts charset of search criteria string
1854 * @param string $str Search string
1855 * @param string $charset Original charset
1856 * @param string $dest_charset Destination charset (default US-ASCII)
1857 * @return string Search string
1860 private function convert_criteria($str, $charset, $dest_charset='US-ASCII')
1862 // convert strings to US_ASCII
1863 if (preg_match_all('/\{([0-9]+)\}\r\n/', $str, $matches, PREG_OFFSET_CAPTURE)) {
1864 $last = 0; $res = '';
1865 foreach ($matches[1] as $m) {
1866 $string_offset = $m[1] + strlen($m[0]) + 4; // {}\r\n
1867 $string = substr($str, $string_offset - 1, $m[0]);
1868 $string = rcube_charset_convert($string, $charset, $dest_charset);
1871 $res .= sprintf("%s{%d}\r\n%s", substr($str, $last, $m[1] - $last - 1), strlen($string), $string);
1872 $last = $m[0] + $string_offset - 1;
1874 if ($last < strlen($str))
1875 $res .= substr($str, $last, strlen($str)-$last);
1877 else // strings for conversion not found
1887 * @param string $mailbox Mailbox name
1888 * @param array $thread_tree Unsorted thread tree (rcube_imap_generic::thread() result)
1889 * @param array $ids Message IDs if we know what we need (e.g. search result)
1890 * @return array Sorted roots IDs
1893 private function _sort_threads($mailbox, $thread_tree, $ids=NULL)
1895 // THREAD=ORDEREDSUBJECT: sorting by sent date of root message
1896 // THREAD=REFERENCES: sorting by sent date of root message
1897 // THREAD=REFS: sorting by the most recent date in each thread
1899 if (!$this->sort_field || ($this->sort_field == 'date' && $this->threading == 'REFS')) {
1900 return array_keys((array)$thread_tree);
1902 // here we'll implement REFS sorting, for performance reason
1903 else { // ($sort_field == 'date' && $this->threading != 'REFS')
1905 if ($this->get_capability('SORT') &&
1906 ($a_index = $this->conn->sort($mailbox, $this->sort_field,
1907 !empty($ids) ? $ids : ($this->skip_deleted ? 'UNDELETED' : ''))) !== false
1909 // return unsorted tree if we've got no index data
1911 return array_keys((array)$thread_tree);
1914 // fetch specified headers for all messages and sort them
1915 $a_index = $this->conn->fetchHeaderIndex($mailbox, !empty($ids) ? $ids : "1:*",
1916 $this->sort_field, $this->skip_deleted);
1918 // return unsorted tree if we've got no index data
1920 return array_keys((array)$thread_tree);
1922 asort($a_index); // ASC
1923 $a_index = array_values($a_index);
1926 return $this->_sort_thread_refs($thread_tree, $a_index);
1932 * THREAD=REFS sorting implementation
1934 * @param array $tree Thread tree array (message identifiers as keys)
1935 * @param array $index Array of sorted message identifiers
1936 * @return array Array of sorted roots messages
1939 private function _sort_thread_refs($tree, $index)
1944 $index = array_combine(array_values($index), $index);
1947 foreach ($tree as $idx => $val) {
1948 $index[$idx] = $idx;
1950 $idx_arr = array_keys_recursive($tree[$idx]);
1951 foreach ($idx_arr as $subidx)
1952 $index[$subidx] = $idx;
1956 $index = array_values($index);
1958 // create sorted array of roots
1959 $msg_index = array();
1960 if ($this->sort_order != 'DESC') {
1961 foreach ($index as $idx)
1962 if (!isset($msg_index[$idx]))
1963 $msg_index[$idx] = $idx;
1964 $msg_index = array_values($msg_index);
1967 for ($x=count($index)-1; $x>=0; $x--)
1968 if (!isset($msg_index[$index[$x]]))
1969 $msg_index[$index[$x]] = $index[$x];
1970 $msg_index = array_reverse($msg_index);
1978 * Refresh saved search set
1980 * @return array Current search set
1982 function refresh_search()
1984 if (!empty($this->search_string))
1985 $this->search_set = $this->search('', $this->search_string, $this->search_charset,
1986 $this->search_sort_field, $this->search_threads, $this->search_sorted);
1988 return $this->get_search_set();
1993 * Check if the given message ID is part of the current search set
1995 * @param string $msgid Message id
1996 * @return boolean True on match or if no search request is stored
1998 function in_searchset($msgid)
2000 if (!empty($this->search_string)) {
2001 if ($this->search_threads)
2002 return isset($this->search_set['depth']["$msgid"]);
2004 return in_array("$msgid", (array)$this->search_set, true);
2012 * Return message headers object of a specific message
2014 * @param int $id Message ID
2015 * @param string $mailbox Mailbox to read from
2016 * @param boolean $is_uid True if $id is the message UID
2017 * @param boolean $bodystr True if we need also BODYSTRUCTURE in headers
2018 * @return object Message headers representation
2020 function get_headers($id, $mailbox=null, $is_uid=true, $bodystr=false)
2022 if (!strlen($mailbox)) {
2023 $mailbox = $this->mailbox;
2025 $uid = $is_uid ? $id : $this->_id2uid($id, $mailbox);
2027 // get cached headers
2028 if ($uid && ($headers = &$this->get_cached_message($mailbox.'.msg', $uid)))
2031 $headers = $this->conn->fetchHeader(
2032 $mailbox, $id, $is_uid, $bodystr, $this->get_fetch_headers());
2034 // write headers cache
2036 if ($headers->uid && $headers->id)
2037 $this->uid_id_map[$mailbox][$headers->uid] = $headers->id;
2039 $this->add_message_cache($mailbox.'.msg', $headers->id, $headers, NULL, false, true);
2047 * Fetch body structure from the IMAP server and build
2048 * an object structure similar to the one generated by PEAR::Mail_mimeDecode
2050 * @param int $uid Message UID to fetch
2051 * @param string $structure_str Message BODYSTRUCTURE string (optional)
2052 * @return object rcube_message_part Message part tree or False on failure
2054 function &get_structure($uid, $structure_str='')
2056 $cache_key = $this->mailbox.'.msg';
2057 $headers = &$this->get_cached_message($cache_key, $uid);
2059 // return cached message structure
2060 if (is_object($headers) && is_object($headers->structure)) {
2061 return $headers->structure;
2064 if (!$structure_str) {
2065 $structure_str = $this->conn->fetchStructureString($this->mailbox, $uid, true);
2067 $structure = rcube_mime_struct::parseStructure($structure_str);
2070 // parse structure and add headers
2071 if (!empty($structure)) {
2072 $headers = $this->get_headers($uid);
2073 $this->_msg_id = $headers->id;
2075 // set message charset from message headers
2076 if ($headers->charset)
2077 $this->struct_charset = $headers->charset;
2079 $this->struct_charset = $this->_structure_charset($structure);
2081 $headers->ctype = strtolower($headers->ctype);
2083 // Here we can recognize malformed BODYSTRUCTURE and
2084 // 1. [@TODO] parse the message in other way to create our own message structure
2085 // 2. or just show the raw message body.
2086 // Example of structure for malformed MIME message:
2087 // ("text" "plain" NIL NIL NIL "7bit" 2154 70 NIL NIL NIL)
2088 if ($headers->ctype && !is_array($structure[0]) && $headers->ctype != 'text/plain'
2089 && strtolower($structure[0].'/'.$structure[1]) == 'text/plain') {
2090 // we can handle single-part messages, by simple fix in structure (#1486898)
2091 if (preg_match('/^(text|application)\/(.*)/', $headers->ctype, $m)) {
2092 $structure[0] = $m[1];
2093 $structure[1] = $m[2];
2099 $struct = &$this->_structure_part($structure, 0, '', $headers);
2100 $struct->headers = get_object_vars($headers);
2102 // don't trust given content-type
2103 if (empty($struct->parts) && !empty($struct->headers['ctype'])) {
2104 $struct->mime_id = '1';
2105 $struct->mimetype = strtolower($struct->headers['ctype']);
2106 list($struct->ctype_primary, $struct->ctype_secondary) = explode('/', $struct->mimetype);
2109 // write structure to cache
2110 if ($this->messages_caching)
2111 $this->add_message_cache($cache_key, $this->_msg_id, $headers, $struct,
2112 $this->icache['message.id'][$uid], true);
2120 * Build message part object
2122 * @param array $part
2124 * @param string $parent
2127 function &_structure_part($part, $count=0, $parent='', $mime_headers=null)
2129 $struct = new rcube_message_part;
2130 $struct->mime_id = empty($parent) ? (string)$count : "$parent.$count";
2133 if (is_array($part[0])) {
2134 $struct->ctype_primary = 'multipart';
2136 /* RFC3501: BODYSTRUCTURE fields of multipart part
2142 2. parameters (optional)
2143 3. description (optional)
2144 4. language (optional)
2145 5. location (optional)
2148 // find first non-array entry
2149 for ($i=1; $i<count($part); $i++) {
2150 if (!is_array($part[$i])) {
2151 $struct->ctype_secondary = strtolower($part[$i]);
2156 $struct->mimetype = 'multipart/'.$struct->ctype_secondary;
2158 // build parts list for headers pre-fetching
2159 for ($i=0; $i<count($part); $i++) {
2160 if (!is_array($part[$i]))
2162 // fetch message headers if message/rfc822
2163 // or named part (could contain Content-Location header)
2164 if (!is_array($part[$i][0])) {
2165 $tmp_part_id = $struct->mime_id ? $struct->mime_id.'.'.($i+1) : $i+1;
2166 if (strtolower($part[$i][0]) == 'message' && strtolower($part[$i][1]) == 'rfc822') {
2167 $mime_part_headers[] = $tmp_part_id;
2169 else if (in_array('name', (array)$part[$i][2]) && empty($part[$i][3])) {
2170 $mime_part_headers[] = $tmp_part_id;
2175 // pre-fetch headers of all parts (in one command for better performance)
2176 // @TODO: we could do this before _structure_part() call, to fetch
2177 // headers for parts on all levels
2178 if ($mime_part_headers) {
2179 $mime_part_headers = $this->conn->fetchMIMEHeaders($this->mailbox,
2180 $this->_msg_id, $mime_part_headers);
2183 $struct->parts = array();
2184 for ($i=0, $count=0; $i<count($part); $i++) {
2185 if (!is_array($part[$i]))
2187 $tmp_part_id = $struct->mime_id ? $struct->mime_id.'.'.($i+1) : $i+1;
2188 $struct->parts[] = $this->_structure_part($part[$i], ++$count, $struct->mime_id,
2189 $mime_part_headers[$tmp_part_id]);
2195 /* RFC3501: BODYSTRUCTURE fields of non-multipart part
2206 7. envelope structure
2211 x. disposition (optional)
2212 x. language (optional)
2213 x. location (optional)
2217 $struct->ctype_primary = strtolower($part[0]);
2218 $struct->ctype_secondary = strtolower($part[1]);
2219 $struct->mimetype = $struct->ctype_primary.'/'.$struct->ctype_secondary;
2221 // read content type parameters
2222 if (is_array($part[2])) {
2223 $struct->ctype_parameters = array();
2224 for ($i=0; $i<count($part[2]); $i+=2)
2225 $struct->ctype_parameters[strtolower($part[2][$i])] = $part[2][$i+1];
2227 if (isset($struct->ctype_parameters['charset']))
2228 $struct->charset = $struct->ctype_parameters['charset'];
2231 // #1487700: workaround for lack of charset in malformed structure
2232 if (empty($struct->charset) && !empty($mime_headers) && $mime_headers->charset) {
2233 $struct->charset = $mime_headers->charset;
2236 // read content encoding
2237 if (!empty($part[5])) {
2238 $struct->encoding = strtolower($part[5]);
2239 $struct->headers['content-transfer-encoding'] = $struct->encoding;
2243 if (!empty($part[6]))
2244 $struct->size = intval($part[6]);
2246 // read part disposition
2248 if ($struct->ctype_primary == 'text') $di += 1;
2249 else if ($struct->mimetype == 'message/rfc822') $di += 3;
2251 if (is_array($part[$di]) && count($part[$di]) == 2) {
2252 $struct->disposition = strtolower($part[$di][0]);
2254 if (is_array($part[$di][1]))
2255 for ($n=0; $n<count($part[$di][1]); $n+=2)
2256 $struct->d_parameters[strtolower($part[$di][1][$n])] = $part[$di][1][$n+1];
2259 // get message/rfc822's child-parts
2260 if (is_array($part[8]) && $di != 8) {
2261 $struct->parts = array();
2262 for ($i=0, $count=0; $i<count($part[8]); $i++) {
2263 if (!is_array($part[8][$i]))
2265 $struct->parts[] = $this->_structure_part($part[8][$i], ++$count, $struct->mime_id);
2270 if (!empty($part[3])) {
2271 $struct->content_id = $part[3];
2272 $struct->headers['content-id'] = $part[3];
2274 if (empty($struct->disposition))
2275 $struct->disposition = 'inline';
2278 // fetch message headers if message/rfc822 or named part (could contain Content-Location header)
2279 if ($struct->ctype_primary == 'message' || ($struct->ctype_parameters['name'] && !$struct->content_id)) {
2280 if (empty($mime_headers)) {
2281 $mime_headers = $this->conn->fetchPartHeader(
2282 $this->mailbox, $this->_msg_id, false, $struct->mime_id);
2285 if (is_string($mime_headers))
2286 $struct->headers = $this->_parse_headers($mime_headers) + $struct->headers;
2287 else if (is_object($mime_headers))
2288 $struct->headers = get_object_vars($mime_headers) + $struct->headers;
2290 // get real content-type of message/rfc822
2291 if ($struct->mimetype == 'message/rfc822') {
2293 if (!is_array($part[8][0]))
2294 $struct->real_mimetype = strtolower($part[8][0] . '/' . $part[8][1]);
2297 for ($n=0; $n<count($part[8]); $n++)
2298 if (!is_array($part[8][$n]))
2300 $struct->real_mimetype = 'multipart/' . strtolower($part[8][$n]);
2304 if ($struct->ctype_primary == 'message' && empty($struct->parts)) {
2305 if (is_array($part[8]) && $di != 8)
2306 $struct->parts[] = $this->_structure_part($part[8], ++$count, $struct->mime_id);
2310 // normalize filename property
2311 $this->_set_part_filename($struct, $mime_headers);
2318 * Set attachment filename from message part structure
2320 * @param rcube_message_part $part Part object
2321 * @param string $headers Part's raw headers
2324 private function _set_part_filename(&$part, $headers=null)
2326 if (!empty($part->d_parameters['filename']))
2327 $filename_mime = $part->d_parameters['filename'];
2328 else if (!empty($part->d_parameters['filename*']))
2329 $filename_encoded = $part->d_parameters['filename*'];
2330 else if (!empty($part->ctype_parameters['name*']))
2331 $filename_encoded = $part->ctype_parameters['name*'];
2332 // RFC2231 value continuations
2333 // TODO: this should be rewrited to support RFC2231 4.1 combinations
2334 else if (!empty($part->d_parameters['filename*0'])) {
2336 while (isset($part->d_parameters['filename*'.$i])) {
2337 $filename_mime .= $part->d_parameters['filename*'.$i];
2340 // some servers (eg. dovecot-1.x) have no support for parameter value continuations
2341 // we must fetch and parse headers "manually"
2344 $headers = $this->conn->fetchPartHeader(
2345 $this->mailbox, $this->_msg_id, false, $part->mime_id);
2347 $filename_mime = '';
2349 while (preg_match('/filename\*'.$i.'\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
2350 $filename_mime .= $matches[1];
2355 else if (!empty($part->d_parameters['filename*0*'])) {
2357 while (isset($part->d_parameters['filename*'.$i.'*'])) {
2358 $filename_encoded .= $part->d_parameters['filename*'.$i.'*'];
2363 $headers = $this->conn->fetchPartHeader(
2364 $this->mailbox, $this->_msg_id, false, $part->mime_id);
2366 $filename_encoded = '';
2367 $i = 0; $matches = array();
2368 while (preg_match('/filename\*'.$i.'\*\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
2369 $filename_encoded .= $matches[1];
2374 else if (!empty($part->ctype_parameters['name*0'])) {
2376 while (isset($part->ctype_parameters['name*'.$i])) {
2377 $filename_mime .= $part->ctype_parameters['name*'.$i];
2382 $headers = $this->conn->fetchPartHeader(
2383 $this->mailbox, $this->_msg_id, false, $part->mime_id);
2385 $filename_mime = '';
2386 $i = 0; $matches = array();
2387 while (preg_match('/\s+name\*'.$i.'\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
2388 $filename_mime .= $matches[1];
2393 else if (!empty($part->ctype_parameters['name*0*'])) {
2395 while (isset($part->ctype_parameters['name*'.$i.'*'])) {
2396 $filename_encoded .= $part->ctype_parameters['name*'.$i.'*'];
2401 $headers = $this->conn->fetchPartHeader(
2402 $this->mailbox, $this->_msg_id, false, $part->mime_id);
2404 $filename_encoded = '';
2405 $i = 0; $matches = array();
2406 while (preg_match('/\s+name\*'.$i.'\*\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
2407 $filename_encoded .= $matches[1];
2412 // read 'name' after rfc2231 parameters as it may contains truncated filename (from Thunderbird)
2413 else if (!empty($part->ctype_parameters['name']))
2414 $filename_mime = $part->ctype_parameters['name'];
2415 // Content-Disposition
2416 else if (!empty($part->headers['content-description']))
2417 $filename_mime = $part->headers['content-description'];
2422 if (!empty($filename_mime)) {
2423 $part->filename = rcube_imap::decode_mime_string($filename_mime,
2424 $part->charset ? $part->charset : ($this->struct_charset ? $this->struct_charset :
2425 rc_detect_encoding($filename_mime, $this->default_charset)));
2427 else if (!empty($filename_encoded)) {
2428 // decode filename according to RFC 2231, Section 4
2429 if (preg_match("/^([^']*)'[^']*'(.*)$/", $filename_encoded, $fmatches)) {
2430 $filename_charset = $fmatches[1];
2431 $filename_encoded = $fmatches[2];
2433 $part->filename = rcube_charset_convert(urldecode($filename_encoded), $filename_charset);
2439 * Get charset name from message structure (first part)
2441 * @param array $structure Message structure
2442 * @return string Charset name
2445 private function _structure_charset($structure)
2447 while (is_array($structure)) {
2448 if (is_array($structure[2]) && $structure[2][0] == 'charset')
2449 return $structure[2][1];
2450 $structure = $structure[0];
2456 * Fetch message body of a specific message from the server
2458 * @param int $uid Message UID
2459 * @param string $part Part number
2460 * @param rcube_message_part $o_part Part object created by get_structure()
2461 * @param mixed $print True to print part, ressource to write part contents in
2462 * @param resource $fp File pointer to save the message part
2463 * @param boolean $skip_charset_conv Disables charset conversion
2465 * @return string Message/part body if not printed
2467 function &get_message_part($uid, $part=1, $o_part=NULL, $print=NULL, $fp=NULL, $skip_charset_conv=false)
2469 // get part encoding if not provided
2470 if (!is_object($o_part)) {
2471 $structure_str = $this->conn->fetchStructureString($this->mailbox, $uid, true);
2472 $structure = new rcube_mime_struct();
2473 // error or message not found
2474 if (!$structure->loadStructure($structure_str)) {
2478 $o_part = new rcube_message_part;
2479 $o_part->ctype_primary = strtolower($structure->getPartType($part));
2480 $o_part->encoding = strtolower($structure->getPartEncoding($part));
2481 $o_part->charset = $structure->getPartCharset($part);
2484 // TODO: Add caching for message parts
2490 $body = $this->conn->handlePartBody($this->mailbox, $uid, true, $part,
2491 $o_part->encoding, $print, $fp);
2493 if ($fp || $print) {
2497 // convert charset (if text or message part)
2498 if ($body && preg_match('/^(text|message)$/', $o_part->ctype_primary)) {
2499 // Remove NULL characters (#1486189)
2500 $body = str_replace("\x00", '', $body);
2502 if (!$skip_charset_conv) {
2503 if (!$o_part->charset || strtoupper($o_part->charset) == 'US-ASCII') {
2504 $o_part->charset = $this->default_charset;
2506 $body = rcube_charset_convert($body, $o_part->charset);
2515 * Fetch message body of a specific message from the server
2517 * @param int $uid Message UID
2518 * @return string $part Message/part body
2519 * @see rcube_imap::get_message_part()
2521 function &get_body($uid, $part=1)
2523 $headers = $this->get_headers($uid);
2524 return rcube_charset_convert($this->get_message_part($uid, $part, NULL),
2525 $headers->charset ? $headers->charset : $this->default_charset);
2530 * Returns the whole message source as string (or saves to a file)
2532 * @param int $uid Message UID
2533 * @param resource $fp File pointer to save the message
2535 * @return string Message source string
2537 function &get_raw_body($uid, $fp=null)
2539 return $this->conn->handlePartBody($this->mailbox, $uid,
2540 true, null, null, false, $fp);
2545 * Returns the message headers as string
2547 * @param int $uid Message UID
2548 * @return string Message headers string
2550 function &get_raw_headers($uid)
2552 return $this->conn->fetchPartHeader($this->mailbox, $uid, true);
2557 * Sends the whole message source to stdout
2559 * @param int $uid Message UID
2561 function print_raw_body($uid)
2563 $this->conn->handlePartBody($this->mailbox, $uid, true, NULL, NULL, true);
2568 * Set message flag to one or several messages
2570 * @param mixed $uids Message UIDs as array or comma-separated string, or '*'
2571 * @param string $flag Flag to set: SEEN, UNDELETED, DELETED, RECENT, ANSWERED, DRAFT, MDNSENT
2572 * @param string $mailbox Folder name
2573 * @param boolean $skip_cache True to skip message cache clean up
2575 * @return boolean Operation status
2577 function set_flag($uids, $flag, $mailbox=null, $skip_cache=false)
2579 if (!strlen($mailbox)) {
2580 $mailbox = $this->mailbox;
2583 $flag = strtoupper($flag);
2584 list($uids, $all_mode) = $this->_parse_uids($uids, $mailbox);
2586 if (strpos($flag, 'UN') === 0)
2587 $result = $this->conn->unflag($mailbox, $uids, substr($flag, 2));
2589 $result = $this->conn->flag($mailbox, $uids, $flag);
2592 // reload message headers if cached
2593 if ($this->messages_caching && !$skip_cache) {
2594 $cache_key = $mailbox.'.msg';
2596 $this->clear_message_cache($cache_key);
2598 $this->remove_message_cache($cache_key, explode(',', $uids));
2601 // clear cached counters
2602 if ($flag == 'SEEN' || $flag == 'UNSEEN') {
2603 $this->_clear_messagecount($mailbox, 'SEEN');
2604 $this->_clear_messagecount($mailbox, 'UNSEEN');
2606 else if ($flag == 'DELETED') {
2607 $this->_clear_messagecount($mailbox, 'DELETED');
2616 * Remove message flag for one or several messages
2618 * @param mixed $uids Message UIDs as array or comma-separated string, or '*'
2619 * @param string $flag Flag to unset: SEEN, DELETED, RECENT, ANSWERED, DRAFT, MDNSENT
2620 * @param string $mailbox Folder name
2622 * @return int Number of flagged messages, -1 on failure
2625 function unset_flag($uids, $flag, $mailbox=null)
2627 return $this->set_flag($uids, 'UN'.$flag, $mailbox);
2632 * Append a mail message (source) to a specific mailbox
2634 * @param string $mailbox Target mailbox
2635 * @param string $message The message source string or filename
2636 * @param string $headers Headers string if $message contains only the body
2637 * @param boolean $is_file True if $message is a filename
2639 * @return boolean True on success, False on error
2641 function save_message($mailbox, &$message, $headers='', $is_file=false)
2643 if (!strlen($mailbox)) {
2644 $mailbox = $this->mailbox;
2647 // make sure mailbox exists
2648 if ($this->mailbox_exists($mailbox)) {
2650 $saved = $this->conn->appendFromFile($mailbox, $message, $headers);
2652 $saved = $this->conn->append($mailbox, $message);
2656 // increase messagecount of the target mailbox
2657 $this->_set_messagecount($mailbox, 'ALL', 1);
2665 * Move a message from one mailbox to another
2667 * @param mixed $uids Message UIDs as array or comma-separated string, or '*'
2668 * @param string $to_mbox Target mailbox
2669 * @param string $from_mbox Source mailbox
2670 * @return boolean True on success, False on error
2672 function move_message($uids, $to_mbox, $from_mbox='')
2674 if (!strlen($from_mbox)) {
2675 $from_mbox = $this->mailbox;
2678 if ($to_mbox === $from_mbox) {
2682 list($uids, $all_mode) = $this->_parse_uids($uids, $from_mbox);
2684 // exit if no message uids are specified
2688 // make sure mailbox exists
2689 if ($to_mbox != 'INBOX' && !$this->mailbox_exists($to_mbox)) {
2690 if (in_array($to_mbox, $this->default_folders))
2691 $this->create_mailbox($to_mbox, true);
2696 $config = rcmail::get_instance()->config;
2697 $to_trash = $to_mbox == $config->get('trash_mbox');
2699 // flag messages as read before moving them
2700 if ($to_trash && $config->get('read_when_deleted')) {
2701 // don't flush cache (4th argument)
2702 $this->set_flag($uids, 'SEEN', $from_mbox, true);
2706 $moved = $this->conn->move($uids, $from_mbox, $to_mbox);
2708 // send expunge command in order to have the moved message
2709 // really deleted from the source mailbox
2711 $this->_expunge($from_mbox, false, $uids);
2712 $this->_clear_messagecount($from_mbox);
2713 $this->_clear_messagecount($to_mbox);
2716 else if ($to_trash && $config->get('delete_always', false)) {
2717 $moved = $this->delete_message($uids, $from_mbox);
2721 // unset threads internal cache
2722 unset($this->icache['threads']);
2724 // remove message ids from search set
2725 if ($this->search_set && $from_mbox == $this->mailbox) {
2726 // threads are too complicated to just remove messages from set
2727 if ($this->search_threads || $all_mode)
2728 $this->refresh_search();
2730 $uids = explode(',', $uids);
2731 foreach ($uids as $uid)
2732 $a_mids[] = $this->_uid2id($uid, $from_mbox);
2733 $this->search_set = array_diff($this->search_set, $a_mids);
2737 // update cached message headers
2738 $cache_key = $from_mbox.'.msg';
2739 if ($all_mode || ($start_index = $this->get_message_cache_index_min($cache_key, $uids))) {
2740 // clear cache from the lowest index on
2741 $this->clear_message_cache($cache_key, $all_mode ? 1 : $start_index);
2750 * Copy a message from one mailbox to another
2752 * @param mixed $uids Message UIDs as array or comma-separated string, or '*'
2753 * @param string $to_mbox Target mailbox
2754 * @param string $from_mbox Source mailbox
2755 * @return boolean True on success, False on error
2757 function copy_message($uids, $to_mbox, $from_mbox='')
2759 if (!strlen($from_mbox)) {
2760 $from_mbox = $this->mailbox;
2763 list($uids, $all_mode) = $this->_parse_uids($uids, $from_mbox);
2765 // exit if no message uids are specified
2770 // make sure mailbox exists
2771 if ($to_mbox != 'INBOX' && !$this->mailbox_exists($to_mbox)) {
2772 if (in_array($to_mbox, $this->default_folders))
2773 $this->create_mailbox($to_mbox, true);
2779 $copied = $this->conn->copy($uids, $from_mbox, $to_mbox);
2782 $this->_clear_messagecount($to_mbox);
2790 * Mark messages as deleted and expunge mailbox
2792 * @param mixed $uids Message UIDs as array or comma-separated string, or '*'
2793 * @param string $mailbox Source mailbox
2795 * @return boolean True on success, False on error
2797 function delete_message($uids, $mailbox='')
2799 if (!strlen($mailbox)) {
2800 $mailbox = $this->mailbox;
2803 list($uids, $all_mode) = $this->_parse_uids($uids, $mailbox);
2805 // exit if no message uids are specified
2809 $deleted = $this->conn->delete($mailbox, $uids);
2812 // send expunge command in order to have the deleted message
2813 // really deleted from the mailbox
2814 $this->_expunge($mailbox, false, $uids);
2815 $this->_clear_messagecount($mailbox);
2816 unset($this->uid_id_map[$mailbox]);
2818 // unset threads internal cache
2819 unset($this->icache['threads']);
2821 // remove message ids from search set
2822 if ($this->search_set && $mailbox == $this->mailbox) {
2823 // threads are too complicated to just remove messages from set
2824 if ($this->search_threads || $all_mode)
2825 $this->refresh_search();
2827 $uids = explode(',', $uids);
2828 foreach ($uids as $uid)
2829 $a_mids[] = $this->_uid2id($uid, $mailbox);
2830 $this->search_set = array_diff($this->search_set, $a_mids);
2834 // remove deleted messages from cache
2835 $cache_key = $mailbox.'.msg';
2836 if ($all_mode || ($start_index = $this->get_message_cache_index_min($cache_key, $uids))) {
2837 // clear cache from the lowest index on
2838 $this->clear_message_cache($cache_key, $all_mode ? 1 : $start_index);
2847 * Clear all messages in a specific mailbox
2849 * @param string $mailbox Mailbox name
2851 * @return int Above 0 on success
2853 function clear_mailbox($mailbox=null)
2855 if (!strlen($mailbox)) {
2856 $mailbox = $this->mailbox;
2859 // SELECT will set messages count for clearFolder()
2860 if ($this->conn->select($mailbox)) {
2861 $cleared = $this->conn->clearFolder($mailbox);
2864 // make sure the message count cache is cleared as well
2866 $this->clear_message_cache($mailbox.'.msg');
2867 $a_mailbox_cache = $this->get_cache('messagecount');
2868 unset($a_mailbox_cache[$mailbox]);
2869 $this->update_cache('messagecount', $a_mailbox_cache);
2877 * Send IMAP expunge command and clear cache
2879 * @param string $mailbox Mailbox name
2880 * @param boolean $clear_cache False if cache should not be cleared
2882 * @return boolean True on success
2884 function expunge($mailbox='', $clear_cache=true)
2886 if (!strlen($mailbox)) {
2887 $mailbox = $this->mailbox;
2890 return $this->_expunge($mailbox, $clear_cache);
2895 * Send IMAP expunge command and clear cache
2897 * @param string $mailbox Mailbox name
2898 * @param boolean $clear_cache False if cache should not be cleared
2899 * @param mixed $uids Message UIDs as array or comma-separated string, or '*'
2900 * @return boolean True on success
2902 * @see rcube_imap::expunge()
2904 private function _expunge($mailbox, $clear_cache=true, $uids=NULL)
2906 if ($uids && $this->get_capability('UIDPLUS'))
2907 $a_uids = is_array($uids) ? join(',', $uids) : $uids;
2911 // force mailbox selection and check if mailbox is writeable
2912 // to prevent a situation when CLOSE is executed on closed
2913 // or EXPUNGE on read-only mailbox
2914 $result = $this->conn->select($mailbox);
2918 if (!$this->conn->data['READ-WRITE']) {
2919 $this->conn->setError(rcube_imap_generic::ERROR_READONLY, "Mailbox is read-only");
2923 // CLOSE(+SELECT) should be faster than EXPUNGE
2924 if (empty($a_uids) || $a_uids == '1:*')
2925 $result = $this->conn->close();
2927 $result = $this->conn->expunge($mailbox, $a_uids);
2929 if ($result && $clear_cache) {
2930 $this->clear_message_cache($mailbox.'.msg');
2931 $this->_clear_messagecount($mailbox);
2939 * Parse message UIDs input
2941 * @param mixed $uids UIDs array or comma-separated list or '*' or '1:*'
2942 * @param string $mailbox Mailbox name
2943 * @return array Two elements array with UIDs converted to list and ALL flag
2946 private function _parse_uids($uids, $mailbox)
2948 if ($uids === '*' || $uids === '1:*') {
2949 if (empty($this->search_set)) {
2953 // get UIDs from current search set
2954 // @TODO: skip fetchUIDs() and work with IDs instead of UIDs (?)
2956 if ($this->search_threads)
2957 $uids = $this->conn->fetchUIDs($mailbox, array_keys($this->search_set['depth']));
2959 $uids = $this->conn->fetchUIDs($mailbox, $this->search_set);
2961 // save ID-to-UID mapping in local cache
2962 if (is_array($uids))
2963 foreach ($uids as $id => $uid)
2964 $this->uid_id_map[$mailbox][$uid] = $id;
2966 $uids = join(',', $uids);
2970 if (is_array($uids))
2971 $uids = join(',', $uids);
2973 if (preg_match('/[^0-9,]/', $uids))
2977 return array($uids, (bool) $all);
2982 * Translate UID to message ID
2984 * @param int $uid Message UID
2985 * @param string $mailbox Mailbox name
2987 * @return int Message ID
2989 function get_id($uid, $mailbox=null)
2991 if (!strlen($mailbox)) {
2992 $mailbox = $this->mailbox;
2995 return $this->_uid2id($uid, $mailbox);
3000 * Translate message number to UID
3002 * @param int $id Message ID
3003 * @param string $mailbox Mailbox name
3005 * @return int Message UID
3007 function get_uid($id, $mailbox=null)
3009 if (!strlen($mailbox)) {
3010 $mailbox = $this->mailbox;
3013 return $this->_id2uid($id, $mailbox);
3018 /* --------------------------------
3020 * --------------------------------*/
3023 * Public method for listing subscribed folders
3025 * @param string $root Optional root folder
3026 * @param string $name Optional name pattern
3027 * @param string $filter Optional filter
3029 * @return array List of mailboxes/folders
3032 function list_mailboxes($root='', $name='*', $filter=null)
3034 $a_mboxes = $this->_list_mailboxes($root, $name, $filter);
3036 // INBOX should always be available
3037 if ((!$filter || $filter == 'mail') && !in_array('INBOX', $a_mboxes)) {
3038 array_unshift($a_mboxes, 'INBOX');
3042 $a_mboxes = $this->_sort_mailbox_list($a_mboxes);
3049 * Private method for mailbox listing
3051 * @param string $root Optional root folder
3052 * @param string $name Optional name pattern
3053 * @param mixed $filter Optional filter
3055 * @return array List of mailboxes/folders
3056 * @see rcube_imap::list_mailboxes()
3059 private function _list_mailboxes($root='', $name='*', $filter=null)
3061 $cache_key = $root.':'.$name;
3062 if (!empty($filter)) {
3063 $cache_key .= ':'.(is_string($filter) ? $filter : serialize($filter));
3066 $cache_key = 'mailboxes.'.md5($cache_key);
3068 // get cached folder list
3069 $a_mboxes = $this->get_cache($cache_key);
3070 if (is_array($a_mboxes)) {
3074 $a_defaults = $a_out = array();
3076 // Give plugins a chance to provide a list of mailboxes
3077 $data = rcmail::get_instance()->plugins->exec_hook('mailboxes_list',
3078 array('root' => $root, 'name' => $name, 'filter' => $filter, 'mode' => 'LSUB'));
3080 if (isset($data['folders'])) {
3081 $a_folders = $data['folders'];
3083 else if (!$this->conn->connected()) {
3087 // Server supports LIST-EXTENDED, we can use selection options
3088 $config = rcmail::get_instance()->config;
3089 // #1486225: Some dovecot versions returns wrong result using LIST-EXTENDED
3090 if (!$config->get('imap_force_lsub') && $this->get_capability('LIST-EXTENDED')) {
3091 // This will also set mailbox options, LSUB doesn't do that
3092 $a_folders = $this->conn->listMailboxes($root, $name,
3093 NULL, array('SUBSCRIBED'));
3095 // unsubscribe non-existent folders, remove from the list
3096 if (is_array($a_folders) && $name == '*') {
3097 foreach ($a_folders as $idx => $folder) {
3098 if ($this->conn->data['LIST'] && ($opts = $this->conn->data['LIST'][$folder])
3099 && in_array('\\NonExistent', $opts)
3101 $this->conn->unsubscribe($folder);
3102 unset($a_folders[$idx]);
3107 // retrieve list of folders from IMAP server using LSUB
3109 $a_folders = $this->conn->listSubscribed($root, $name);
3111 // unsubscribe non-existent folders, remove from the list
3112 if (is_array($a_folders) && $name == '*') {
3113 foreach ($a_folders as $idx => $folder) {
3114 if ($this->conn->data['LIST'] && ($opts = $this->conn->data['LIST'][$folder])
3115 && in_array('\\Noselect', $opts)
3117 // Some servers returns \Noselect for existing folders
3118 if (!$this->mailbox_exists($folder)) {
3119 $this->conn->unsubscribe($folder);
3120 unset($a_folders[$idx]);
3128 if (!is_array($a_folders) || !sizeof($a_folders)) {
3129 $a_folders = array();
3132 // write mailboxlist to cache
3133 $this->update_cache($cache_key, $a_folders);
3140 * Get a list of all folders available on the IMAP server
3142 * @param string $root IMAP root dir
3143 * @param string $name Optional name pattern
3144 * @param mixed $filter Optional filter
3146 * @return array Indexed array with folder names
3148 function list_unsubscribed($root='', $name='*', $filter=null)
3151 // Give plugins a chance to provide a list of mailboxes
3152 $data = rcmail::get_instance()->plugins->exec_hook('mailboxes_list',
3153 array('root' => $root, 'name' => $name, 'filter' => $filter, 'mode' => 'LIST'));
3155 if (isset($data['folders'])) {
3156 $a_mboxes = $data['folders'];
3159 // retrieve list of folders from IMAP server
3160 $a_mboxes = $this->conn->listMailboxes($root, $name);
3163 if (!is_array($a_mboxes)) {
3164 $a_mboxes = array();
3167 // INBOX should always be available
3168 if ((!$filter || $filter == 'mail') && !in_array('INBOX', $a_mboxes)) {
3169 array_unshift($a_mboxes, 'INBOX');
3172 // filter folders and sort them
3173 $a_mboxes = $this->_sort_mailbox_list($a_mboxes);
3180 * Get mailbox quota information
3183 * @return mixed Quota info or False if not supported
3185 function get_quota()
3187 if ($this->get_capability('QUOTA'))
3188 return $this->conn->getQuota();
3195 * Get mailbox size (size of all messages in a mailbox)
3197 * @param string $mailbox Mailbox name
3199 * @return int Mailbox size in bytes, False on error
3201 function get_mailbox_size($mailbox)
3203 // @TODO: could we try to use QUOTA here?
3204 $result = $this->conn->fetchHeaderIndex($mailbox, '1:*', 'SIZE', false);
3206 if (is_array($result))
3207 $result = array_sum($result);
3214 * Subscribe to a specific mailbox(es)
3216 * @param array $a_mboxes Mailbox name(s)
3217 * @return boolean True on success
3219 function subscribe($a_mboxes)
3221 if (!is_array($a_mboxes))
3222 $a_mboxes = array($a_mboxes);
3224 // let this common function do the main work
3225 return $this->_change_subscription($a_mboxes, 'subscribe');
3230 * Unsubscribe mailboxes
3232 * @param array $a_mboxes Mailbox name(s)
3233 * @return boolean True on success
3235 function unsubscribe($a_mboxes)
3237 if (!is_array($a_mboxes))
3238 $a_mboxes = array($a_mboxes);
3240 // let this common function do the main work
3241 return $this->_change_subscription($a_mboxes, 'unsubscribe');
3246 * Create a new mailbox on the server and register it in local cache
3248 * @param string $mailbox New mailbox name
3249 * @param boolean $subscribe True if the new mailbox should be subscribed
3251 * @return boolean True on success
3253 function create_mailbox($mailbox, $subscribe=false)
3255 $result = $this->conn->createFolder($mailbox);
3257 // try to subscribe it
3260 $this->clear_cache('mailboxes', true);
3263 $this->subscribe($mailbox);
3271 * Set a new name to an existing mailbox
3273 * @param string $mailbox Mailbox to rename
3274 * @param string $new_name New mailbox name
3276 * @return boolean True on success
3278 function rename_mailbox($mailbox, $new_name)
3280 if (!strlen($new_name)) {
3284 $delm = $this->get_hierarchy_delimiter();
3286 // get list of subscribed folders
3287 if ((strpos($mailbox, '%') === false) && (strpos($mailbox, '*') === false)) {
3288 $a_subscribed = $this->_list_mailboxes('', $mailbox . $delm . '*');
3289 $subscribed = $this->mailbox_exists($mailbox, true);
3292 $a_subscribed = $this->_list_mailboxes();
3293 $subscribed = in_array($mailbox, $a_subscribed);
3296 $result = $this->conn->renameFolder($mailbox, $new_name);
3299 // unsubscribe the old folder, subscribe the new one
3301 $this->conn->unsubscribe($mailbox);
3302 $this->conn->subscribe($new_name);
3305 // check if mailbox children are subscribed
3306 foreach ($a_subscribed as $c_subscribed) {
3307 if (preg_match('/^'.preg_quote($mailbox.$delm, '/').'/', $c_subscribed)) {
3308 $this->conn->unsubscribe($c_subscribed);
3309 $this->conn->subscribe(preg_replace('/^'.preg_quote($mailbox, '/').'/',
3310 $new_name, $c_subscribed));
3315 $this->clear_message_cache($mailbox.'.msg');
3316 $this->clear_cache('mailboxes', true);
3324 * Remove mailbox from server
3326 * @param string $mailbox Mailbox name
3328 * @return boolean True on success
3330 function delete_mailbox($mailbox)
3332 $delm = $this->get_hierarchy_delimiter();
3334 // get list of folders
3335 if ((strpos($mailbox, '%') === false) && (strpos($mailbox, '*') === false))
3336 $sub_mboxes = $this->list_unsubscribed('', $mailbox . $delm . '*');
3338 $sub_mboxes = $this->list_unsubscribed();
3340 // send delete command to server
3341 $result = $this->conn->deleteFolder($mailbox);
3344 // unsubscribe mailbox
3345 $this->conn->unsubscribe($mailbox);
3347 foreach ($sub_mboxes as $c_mbox) {
3348 if (preg_match('/^'.preg_quote($mailbox.$delm, '/').'/', $c_mbox)) {
3349 $this->conn->unsubscribe($c_mbox);
3350 if ($this->conn->deleteFolder($c_mbox)) {
3351 $this->clear_message_cache($c_mbox.'.msg');
3356 // clear mailbox-related cache
3357 $this->clear_message_cache($mailbox.'.msg');
3358 $this->clear_cache('mailboxes', true);
3366 * Create all folders specified as default
3368 function create_default_folders()
3370 // create default folders if they do not exist
3371 foreach ($this->default_folders as $folder) {
3372 if (!$this->mailbox_exists($folder))
3373 $this->create_mailbox($folder, true);
3374 else if (!$this->mailbox_exists($folder, true))
3375 $this->subscribe($folder);
3381 * Checks if folder exists and is subscribed
3383 * @param string $mailbox Folder name
3384 * @param boolean $subscription Enable subscription checking
3386 * @return boolean TRUE or FALSE
3388 function mailbox_exists($mailbox, $subscription=false)
3390 if ($mailbox == 'INBOX') {
3394 $key = $subscription ? 'subscribed' : 'existing';
3396 if (is_array($this->icache[$key]) && in_array($mailbox, $this->icache[$key]))
3399 if ($subscription) {
3400 $a_folders = $this->conn->listSubscribed('', $mailbox);
3403 $a_folders = $this->conn->listMailboxes('', $mailbox);
3406 if (is_array($a_folders) && in_array($mailbox, $a_folders)) {
3407 $this->icache[$key][] = $mailbox;
3416 * Returns the namespace where the folder is in
3418 * @param string $mailbox Folder name
3420 * @return string One of 'personal', 'other' or 'shared'
3423 function mailbox_namespace($mailbox)
3425 if ($mailbox == 'INBOX') {
3429 foreach ($this->namespace as $type => $namespace) {
3430 if (is_array($namespace)) {
3431 foreach ($namespace as $ns) {
3432 if (strlen($ns[0])) {
3433 if ((strlen($ns[0])>1 && $mailbox == substr($ns[0], 0, -1))
3434 || strpos($mailbox, $ns[0]) === 0
3448 * Modify folder name according to namespace.
3449 * For output it removes prefix of the personal namespace if it's possible.
3450 * For input it adds the prefix. Use it before creating a folder in root
3451 * of the folders tree.
3453 * @param string $mailbox Folder name
3454 * @param string $mode Mode name (out/in)
3456 * @return string Folder name
3458 function mod_mailbox($mailbox, $mode = 'out')
3460 if (!strlen($mailbox)) {
3464 $prefix = $this->namespace['prefix']; // see set_env()
3465 $prefix_len = strlen($prefix);
3471 // remove prefix for output
3472 if ($mode == 'out') {
3473 if (substr($mailbox, 0, $prefix_len) === $prefix) {
3474 return substr($mailbox, $prefix_len);
3477 // add prefix for input (e.g. folder creation)
3479 return $prefix . $mailbox;
3487 * Gets folder options from LIST response, e.g. \Noselect, \Noinferiors
3489 * @param string $mailbox Folder name
3490 * @param bool $force Set to True if options should be refreshed
3491 * Options are available after LIST command only
3493 * @return array Options list
3495 function mailbox_options($mailbox, $force=false)
3497 if ($mailbox == 'INBOX') {
3501 if (!is_array($this->conn->data['LIST']) || !is_array($this->conn->data['LIST'][$mailbox])) {
3503 $this->conn->listMailboxes('', $mailbox);
3510 $opts = $this->conn->data['LIST'][$mailbox];
3512 return is_array($opts) ? $opts : array();
3517 * Returns extended information about the folder
3519 * @param string $mailbox Folder name
3521 * @return array Data
3523 function mailbox_info($mailbox)
3525 if ($this->icache['options'] && $this->icache['options']['name'] == $mailbox) {
3526 return $this->icache['options'];
3529 $acl = $this->get_capability('ACL');
3530 $namespace = $this->get_namespace();
3533 // check if the folder is a namespace prefix
3534 if (!empty($namespace)) {
3535 $mbox = $mailbox . $this->delimiter;
3536 foreach ($namespace as $ns) {
3538 foreach ($ns as $item) {
3539 if ($item[0] === $mbox) {
3540 $options['is_root'] = true;
3547 // check if the folder is other user virtual-root
3548 if (!$options['is_root'] && !empty($namespace) && !empty($namespace['other'])) {
3549 $parts = explode($this->delimiter, $mailbox);
3550 if (count($parts) == 2) {
3551 $mbox = $parts[0] . $this->delimiter;
3552 foreach ($namespace['other'] as $item) {
3553 if ($item[0] === $mbox) {
3554 $options['is_root'] = true;
3561 $options['name'] = $mailbox;
3562 $options['options'] = $this->mailbox_options($mailbox, true);
3563 $options['namespace'] = $this->mailbox_namespace($mailbox);
3564 $options['rights'] = $acl && !$options['is_root'] ? (array)$this->my_rights($mailbox) : array();
3565 $options['special'] = in_array($mailbox, $this->default_folders);
3567 // Set 'noselect' and 'norename' flags
3568 if (is_array($options['options'])) {
3569 foreach ($options['options'] as $opt) {
3570 $opt = strtolower($opt);
3571 if ($opt == '\noselect' || $opt == '\nonexistent') {
3572 $options['noselect'] = true;
3577 $options['noselect'] = true;
3580 if (!empty($options['rights'])) {
3581 $options['norename'] = !in_array('x', $options['rights']) && !in_array('d', $options['rights']);
3583 if (!$options['noselect']) {
3584 $options['noselect'] = !in_array('r', $options['rights']);
3588 $options['norename'] = $options['is_root'] || $options['namespace'] != 'personal';
3591 $this->icache['options'] = $options;
3598 * Get message header names for rcube_imap_generic::fetchHeader(s)
3600 * @return string Space-separated list of header names
3602 private function get_fetch_headers()
3604 $headers = explode(' ', $this->fetch_add_headers);
3605 $headers = array_map('strtoupper', $headers);
3607 if ($this->messages_caching || $this->get_all_headers)
3608 $headers = array_merge($headers, $this->all_headers);
3610 return implode(' ', array_unique($headers));
3614 /* -----------------------------------------
3615 * ACL and METADATA/ANNOTATEMORE methods
3616 * ----------------------------------------*/
3619 * Changes the ACL on the specified mailbox (SETACL)
3621 * @param string $mailbox Mailbox name
3622 * @param string $user User name
3623 * @param string $acl ACL string
3625 * @return boolean True on success, False on failure
3630 function set_acl($mailbox, $user, $acl)
3632 if ($this->get_capability('ACL'))
3633 return $this->conn->setACL($mailbox, $user, $acl);
3640 * Removes any <identifier,rights> pair for the
3641 * specified user from the ACL for the specified
3642 * mailbox (DELETEACL)
3644 * @param string $mailbox Mailbox name
3645 * @param string $user User name
3647 * @return boolean True on success, False on failure
3652 function delete_acl($mailbox, $user)
3654 if ($this->get_capability('ACL'))
3655 return $this->conn->deleteACL($mailbox, $user);
3662 * Returns the access control list for mailbox (GETACL)
3664 * @param string $mailbox Mailbox name
3666 * @return array User-rights array on success, NULL on error
3670 function get_acl($mailbox)
3672 if ($this->get_capability('ACL'))
3673 return $this->conn->getACL($mailbox);
3680 * Returns information about what rights can be granted to the
3681 * user (identifier) in the ACL for the mailbox (LISTRIGHTS)
3683 * @param string $mailbox Mailbox name
3684 * @param string $user User name
3686 * @return array List of user rights
3690 function list_rights($mailbox, $user)
3692 if ($this->get_capability('ACL'))
3693 return $this->conn->listRights($mailbox, $user);
3700 * Returns the set of rights that the current user has to
3701 * mailbox (MYRIGHTS)
3703 * @param string $mailbox Mailbox name
3705 * @return array MYRIGHTS response on success, NULL on error
3709 function my_rights($mailbox)
3711 if ($this->get_capability('ACL'))
3712 return $this->conn->myRights($mailbox);
3719 * Sets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
3721 * @param string $mailbox Mailbox name (empty for server metadata)
3722 * @param array $entries Entry-value array (use NULL value as NIL)
3724 * @return boolean True on success, False on failure
3728 function set_metadata($mailbox, $entries)
3730 if ($this->get_capability('METADATA') ||
3731 (!strlen($mailbox) && $this->get_capability('METADATA-SERVER'))
3733 return $this->conn->setMetadata($mailbox, $entries);
3735 else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
3736 foreach ((array)$entries as $entry => $value) {
3737 list($ent, $attr) = $this->md2annotate($entry);
3738 $entries[$entry] = array($ent, $attr, $value);
3740 return $this->conn->setAnnotation($mailbox, $entries);
3748 * Unsets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
3750 * @param string $mailbox Mailbox name (empty for server metadata)
3751 * @param array $entries Entry names array
3753 * @return boolean True on success, False on failure
3758 function delete_metadata($mailbox, $entries)
3760 if ($this->get_capability('METADATA') ||
3761 (!strlen($mailbox) && $this->get_capability('METADATA-SERVER'))
3763 return $this->conn->deleteMetadata($mailbox, $entries);
3765 else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
3766 foreach ((array)$entries as $idx => $entry) {
3767 list($ent, $attr) = $this->md2annotate($entry);
3768 $entries[$idx] = array($ent, $attr, NULL);
3770 return $this->conn->setAnnotation($mailbox, $entries);
3778 * Returns IMAP metadata/annotations (GETMETADATA/GETANNOTATION)
3780 * @param string $mailbox Mailbox name (empty for server metadata)
3781 * @param array $entries Entries
3782 * @param array $options Command options (with MAXSIZE and DEPTH keys)
3784 * @return array Metadata entry-value hash array on success, NULL on error
3789 function get_metadata($mailbox, $entries, $options=array())
3791 if ($this->get_capability('METADATA') ||
3792 (!strlen($mailbox) && $this->get_capability('METADATA-SERVER'))
3794 return $this->conn->getMetadata($mailbox, $entries, $options);
3796 else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
3800 // Convert entry names
3801 foreach ((array)$entries as $entry) {
3802 list($ent, $attr) = $this->md2annotate($entry);
3803 $queries[$attr][] = $ent;
3806 // @TODO: Honor MAXSIZE and DEPTH options
3807 foreach ($queries as $attrib => $entry)
3808 if ($result = $this->conn->getAnnotation($mailbox, $entry, $attrib))
3809 $res = array_merge_recursive($res, $result);
3819 * Converts the METADATA extension entry name into the correct
3820 * entry-attrib names for older ANNOTATEMORE version.
3822 * @param string $entry Entry name
3824 * @return array Entry-attribute list, NULL if not supported (?)
3826 private function md2annotate($entry)
3828 if (substr($entry, 0, 7) == '/shared') {
3829 return array(substr($entry, 7), 'value.shared');
3831 else if (substr($entry, 0, 8) == '/private') {
3832 return array(substr($entry, 8), 'value.priv');
3840 /* --------------------------------
3841 * internal caching methods
3842 * --------------------------------*/
3845 * Enable or disable indexes caching
3847 * @param string $type Cache type (@see rcmail::get_cache)
3850 function set_caching($type)
3853 $this->caching = $type;
3857 $this->cache->close();
3858 $this->cache = null;
3859 $this->caching = false;
3864 * Getter for IMAP cache object
3866 private function get_cache_engine()
3868 if ($this->caching && !$this->cache) {
3869 $rcmail = rcmail::get_instance();
3870 $this->cache = $rcmail->get_cache('IMAP', $this->caching);
3873 return $this->cache;
3877 * Returns cached value
3879 * @param string $key Cache key
3883 function get_cache($key)
3885 if ($cache = $this->get_cache_engine()) {
3886 return $cache->get($key);
3893 * @param string $key Cache key
3894 * @param mixed $data Data
3897 function update_cache($key, $data)
3899 if ($cache = $this->get_cache_engine()) {
3900 $cache->set($key, $data);
3907 * @param string $key Cache key name or pattern
3908 * @param boolean $prefix_mode Enable it to clear all keys starting
3909 * with prefix specified in $key
3912 function clear_cache($key=null, $prefix_mode=false)
3914 if ($cache = $this->get_cache_engine()) {
3915 $cache->remove($key, $prefix_mode);
3920 /* --------------------------------
3921 * message caching methods
3922 * --------------------------------*/
3925 * Enable or disable messages caching
3927 * @param boolean $set Flag
3930 function set_messages_caching($set)
3932 $rcmail = rcmail::get_instance();
3934 if ($set && ($dbh = $rcmail->get_dbh())) {
3936 $this->messages_caching = true;
3939 $this->messages_caching = false;
3944 * Checks if the cache is up-to-date
3946 * @param string $mailbox Mailbox name
3947 * @param string $cache_key Internal cache key
3948 * @return int Cache status: -3 = off, -2 = incomplete, -1 = dirty, 1 = OK
3950 private function check_cache_status($mailbox, $cache_key)
3952 if (!$this->messages_caching)
3955 $cache_index = $this->get_message_cache_index($cache_key);
3956 $msg_count = $this->_messagecount($mailbox);
3957 $cache_count = count($cache_index);
3961 return $cache_count ? -2 : 1;
3964 if ($cache_count == $msg_count) {
3965 if ($this->skip_deleted) {
3966 if (!empty($this->icache['all_undeleted_idx'])) {
3967 $uids = rcube_imap_generic::uncompressMessageSet($this->icache['all_undeleted_idx']);
3968 $uids = array_flip($uids);
3969 foreach ($cache_index as $uid) {
3974 // get all undeleted messages excluding cached UIDs
3975 $uids = $this->search_once($mailbox, 'ALL UNDELETED NOT UID '.
3976 rcube_imap_generic::compressMessageSet($cache_index));
3982 // get UID of the message with highest index
3983 $uid = $this->_id2uid($msg_count, $mailbox);
3984 $cache_uid = array_pop($cache_index);
3986 // uids of highest message matches -> cache seems OK
3987 if ($cache_uid == $uid) {
3995 // if cache count differs less than 10% report as dirty
3996 return (abs($msg_count - $cache_count) < $msg_count/10) ? -1 : -2;
4001 * @param string $key Cache key
4002 * @param string $from
4004 * @param string $sort_field
4005 * @param string $sort_order
4008 private function get_message_cache($key, $from, $to, $sort_field, $sort_order)
4010 if (!$this->messages_caching)
4013 // use idx sort as default sorting
4014 if (!$sort_field || !in_array($sort_field, $this->db_header_fields)) {
4015 $sort_field = 'idx';
4020 $sql_result = $this->db->limitquery(
4021 "SELECT idx, uid, headers".
4022 " FROM ".get_table_name('messages').
4025 " ORDER BY ".$this->db->quoteIdentifier($sort_field)." ".strtoupper($sort_order),
4028 $_SESSION['user_id'],
4031 while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
4032 $uid = intval($sql_arr['uid']);
4033 $result[$uid] = $this->db->decode(unserialize($sql_arr['headers']));
4035 // featch headers if unserialize failed
4036 if (empty($result[$uid]))
4037 $result[$uid] = $this->conn->fetchHeader(
4038 preg_replace('/.msg$/', '', $key), $uid, true, false, $this->get_fetch_headers());
4046 * @param string $key Cache key
4047 * @param int $uid Message UID
4051 private function &get_cached_message($key, $uid)
4053 $internal_key = 'message';
4055 if ($this->messages_caching && !isset($this->icache[$internal_key][$uid])) {
4056 $sql_result = $this->db->query(
4057 "SELECT idx, headers, structure, message_id".
4058 " FROM ".get_table_name('messages').
4062 $_SESSION['user_id'],
4066 if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
4067 $this->icache['message.id'][$uid] = intval($sql_arr['message_id']);
4068 $this->uid_id_map[preg_replace('/\.msg$/', '', $key)][$uid] = intval($sql_arr['idx']);
4069 $this->icache[$internal_key][$uid] = $this->db->decode(unserialize($sql_arr['headers']));
4071 if (is_object($this->icache[$internal_key][$uid]) && !empty($sql_arr['structure']))
4072 $this->icache[$internal_key][$uid]->structure = $this->db->decode(unserialize($sql_arr['structure']));
4076 return $this->icache[$internal_key][$uid];
4081 * @param string $key Cache key
4082 * @param string $sort_field Sorting column
4083 * @param string $sort_order Sorting order
4084 * @return array Messages index
4087 private function get_message_cache_index($key, $sort_field='idx', $sort_order='ASC')
4089 if (!$this->messages_caching || empty($key))
4092 // use idx sort as default
4093 if (!$sort_field || !in_array($sort_field, $this->db_header_fields))
4094 $sort_field = 'idx';
4096 if (array_key_exists('index', $this->icache)
4097 && $this->icache['index']['key'] == $key
4098 && $this->icache['index']['sort_field'] == $sort_field
4100 if ($this->icache['index']['sort_order'] == $sort_order)
4101 return $this->icache['index']['result'];
4103 return array_reverse($this->icache['index']['result'], true);
4106 $this->icache['index'] = array(
4107 'result' => array(),
4109 'sort_field' => $sort_field,
4110 'sort_order' => $sort_order,
4113 $sql_result = $this->db->query(
4115 " FROM ".get_table_name('messages').
4118 " ORDER BY ".$this->db->quote_identifier($sort_field)." ".$sort_order,
4119 $_SESSION['user_id'],
4122 while ($sql_arr = $this->db->fetch_assoc($sql_result))
4123 $this->icache['index']['result'][$sql_arr['idx']] = intval($sql_arr['uid']);
4125 return $this->icache['index']['result'];
4132 private function add_message_cache($key, $index, $headers, $struct=null, $force=false, $internal_cache=false)
4134 if (empty($key) || !is_object($headers) || empty($headers->uid))
4137 // add to internal (fast) cache
4138 if ($internal_cache) {
4139 $this->icache['message'][$headers->uid] = clone $headers;
4140 $this->icache['message'][$headers->uid]->structure = $struct;
4143 // no further caching
4144 if (!$this->messages_caching)
4148 if (is_int($force) && $force > 0) {
4149 $message_id = $force;
4151 // check for an existing record (probably headers are cached but structure not)
4153 $sql_result = $this->db->query(
4154 "SELECT message_id".
4155 " FROM ".get_table_name('messages').
4159 $_SESSION['user_id'],
4163 if ($sql_arr = $this->db->fetch_assoc($sql_result))
4164 $message_id = $sql_arr['message_id'];
4167 // update cache record
4170 "UPDATE ".get_table_name('messages').
4171 " SET idx=?, headers=?, structure=?".
4172 " WHERE message_id=?",
4174 serialize($this->db->encode(clone $headers)),
4175 is_object($struct) ? serialize($this->db->encode(clone $struct)) : NULL,
4179 else { // insert new record
4181 "INSERT INTO ".get_table_name('messages').
4182 " (user_id, del, cache_key, created, idx, uid, subject, ".
4183 $this->db->quoteIdentifier('from').", ".
4184 $this->db->quoteIdentifier('to').", ".
4185 "cc, date, size, headers, structure)".
4186 " VALUES (?, 0, ?, ".$this->db->now().", ?, ?, ?, ?, ?, ?, ".
4187 $this->db->fromunixtime($headers->timestamp).", ?, ?, ?)",
4188 $_SESSION['user_id'],
4192 (string)mb_substr($this->db->encode($this->decode_header($headers->subject, true)), 0, 128),
4193 (string)mb_substr($this->db->encode($this->decode_header($headers->from, true)), 0, 128),
4194 (string)mb_substr($this->db->encode($this->decode_header($headers->to, true)), 0, 128),
4195 (string)mb_substr($this->db->encode($this->decode_header($headers->cc, true)), 0, 128),
4196 (int)$headers->size,
4197 serialize($this->db->encode(clone $headers)),
4198 is_object($struct) ? serialize($this->db->encode(clone $struct)) : NULL
4202 unset($this->icache['index']);
4209 private function remove_message_cache($key, $ids, $idx=false)
4211 if (!$this->messages_caching)
4215 "DELETE FROM ".get_table_name('messages').
4218 " AND ".($idx ? "idx" : "uid")." IN (".$this->db->array2list($ids, 'integer').")",
4219 $_SESSION['user_id'],
4222 unset($this->icache['index']);
4227 * @param string $key Cache key
4228 * @param int $start_index Start index
4231 private function clear_message_cache($key, $start_index=1)
4233 if (!$this->messages_caching)
4237 "DELETE FROM ".get_table_name('messages').
4241 $_SESSION['user_id'], $key, $start_index);
4243 unset($this->icache['index']);
4250 private function get_message_cache_index_min($key, $uids=NULL)
4252 if (!$this->messages_caching)
4255 if (!empty($uids) && !is_array($uids)) {
4256 if ($uids == '*' || $uids == '1:*')
4259 $uids = explode(',', $uids);
4262 $sql_result = $this->db->query(
4263 "SELECT MIN(idx) AS minidx".
4264 " FROM ".get_table_name('messages').
4267 .(!empty($uids) ? " AND uid IN (".$this->db->array2list($uids, 'integer').")" : ''),
4268 $_SESSION['user_id'],
4271 if ($sql_arr = $this->db->fetch_assoc($sql_result))
4272 return $sql_arr['minidx'];
4279 * @param string $key Cache key
4280 * @param int $id Message (sequence) ID
4281 * @return int Message UID
4284 private function get_cache_id2uid($key, $id)
4286 if (!$this->messages_caching)
4289 if (array_key_exists('index', $this->icache)
4290 && $this->icache['index']['key'] == $key
4292 return $this->icache['index']['result'][$id];
4295 $sql_result = $this->db->query(
4297 " FROM ".get_table_name('messages').
4301 $_SESSION['user_id'], $key, $id);
4303 if ($sql_arr = $this->db->fetch_assoc($sql_result))
4304 return intval($sql_arr['uid']);
4311 * @param string $key Cache key
4312 * @param int $uid Message UID
4313 * @return int Message (sequence) ID
4316 private function get_cache_uid2id($key, $uid)
4318 if (!$this->messages_caching)
4321 if (array_key_exists('index', $this->icache)
4322 && $this->icache['index']['key'] == $key
4324 return array_search($uid, $this->icache['index']['result']);
4327 $sql_result = $this->db->query(
4329 " FROM ".get_table_name('messages').
4333 $_SESSION['user_id'], $key, $uid);
4335 if ($sql_arr = $this->db->fetch_assoc($sql_result))
4336 return intval($sql_arr['idx']);
4342 /* --------------------------------
4343 * encoding/decoding methods
4344 * --------------------------------*/
4347 * Split an address list into a structured array list
4349 * @param string $input Input string
4350 * @param int $max List only this number of addresses
4351 * @param boolean $decode Decode address strings
4352 * @return array Indexed list of addresses
4354 function decode_address_list($input, $max=null, $decode=true)
4356 $a = $this->_parse_address_list($input, $decode);
4358 // Special chars as defined by RFC 822 need to in quoted string (or escaped).
4359 $special_chars = '[\(\)\<\>\\\.\[\]@,;:"]';
4367 foreach ($a as $val) {
4369 $address = trim($val['address']);
4370 $name = trim($val['name']);
4372 if ($name && $address && $name != $address)
4373 $string = sprintf('%s <%s>', preg_match("/$special_chars/", $name) ? '"'.addcslashes($name, '"').'"' : $name, $address);
4381 'mailto' => $address,
4385 if ($max && $j==$max)
4394 * Decode a message header value
4396 * @param string $input Header value
4397 * @param boolean $remove_quotas Remove quotes if necessary
4398 * @return string Decoded string
4400 function decode_header($input, $remove_quotes=false)
4402 $str = rcube_imap::decode_mime_string((string)$input, $this->default_charset);
4403 if ($str[0] == '"' && $remove_quotes)
4404 $str = str_replace('"', '', $str);
4411 * Decode a mime-encoded string to internal charset
4413 * @param string $input Header value
4414 * @param string $fallback Fallback charset if none specified
4416 * @return string Decoded string
4419 public static function decode_mime_string($input, $fallback=null)
4421 if (!empty($fallback)) {
4422 $default_charset = $fallback;
4425 $default_charset = rcmail::get_instance()->config->get('default_charset', 'ISO-8859-1');
4428 // rfc: all line breaks or other characters not found
4429 // in the Base64 Alphabet must be ignored by decoding software
4430 // delete all blanks between MIME-lines, differently we can
4431 // receive unnecessary blanks and broken utf-8 symbols
4432 $input = preg_replace("/\?=\s+=\?/", '?==?', $input);
4434 // encoded-word regexp
4435 $re = '/=\?([^?]+)\?([BbQq])\?([^?\n]*)\?=/';
4437 // Find all RFC2047's encoded words
4438 if (preg_match_all($re, $input, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) {
4439 // Initialize variables
4444 foreach ($matches as $idx => $m) {
4446 $charset = $m[1][0];
4447 $encoding = $m[2][0];
4449 $length = strlen($m[0][0]);
4451 // Append everything that is before the text to be decoded
4452 if ($start != $pos) {
4453 $substr = substr($input, $start, $pos-$start);
4454 $out .= rcube_charset_convert($substr, $default_charset);
4459 // Per RFC2047, each string part "MUST represent an integral number
4460 // of characters . A multi-octet character may not be split across
4461 // adjacent encoded-words." However, some mailers break this, so we
4462 // try to handle characters spanned across parts anyway by iterating
4463 // through and aggregating sequential encoded parts with the same
4464 // character set and encoding, then perform the decoding on the
4465 // aggregation as a whole.
4468 if ($next_match = $matches[$idx+1]) {
4469 if ($next_match[0][1] == $start
4470 && $next_match[1][0] == $charset
4471 && $next_match[2][0] == $encoding
4477 $count = count($tmp);
4480 // Decode and join encoded-word's chunks
4481 if ($encoding == 'B' || $encoding == 'b') {
4482 // base64 must be decoded a segment at a time
4483 for ($i=0; $i<$count; $i++)
4484 $text .= base64_decode($tmp[$i]);
4486 else { //if ($encoding == 'Q' || $encoding == 'q') {
4487 // quoted printable can be combined and processed at once
4488 for ($i=0; $i<$count; $i++)
4491 $text = str_replace('_', ' ', $text);
4492 $text = quoted_printable_decode($text);
4495 $out .= rcube_charset_convert($text, $charset);
4499 // add the last part of the input string
4500 if ($start != strlen($input)) {
4501 $out .= rcube_charset_convert(substr($input, $start), $default_charset);
4504 // return the results
4508 // no encoding information, use fallback
4509 return rcube_charset_convert($input, $default_charset);
4514 * Decode a mime part
4516 * @param string $input Input string
4517 * @param string $encoding Part encoding
4518 * @return string Decoded string
4520 function mime_decode($input, $encoding='7bit')
4522 switch (strtolower($encoding)) {
4523 case 'quoted-printable':
4524 return quoted_printable_decode($input);
4526 return base64_decode($input);
4531 return convert_uudecode($input);
4540 * Convert body charset to RCMAIL_CHARSET according to the ctype_parameters
4542 * @param string $body Part body to decode
4543 * @param string $ctype_param Charset to convert from
4544 * @return string Content converted to internal charset
4546 function charset_decode($body, $ctype_param)
4548 if (is_array($ctype_param) && !empty($ctype_param['charset']))
4549 return rcube_charset_convert($body, $ctype_param['charset']);
4551 // defaults to what is specified in the class header
4552 return rcube_charset_convert($body, $this->default_charset);
4556 /* --------------------------------
4558 * --------------------------------*/
4561 * Validate the given input and save to local properties
4563 * @param string $sort_field Sort column
4564 * @param string $sort_order Sort order
4567 private function _set_sort_order($sort_field, $sort_order)
4569 if ($sort_field != null)
4570 $this->sort_field = asciiwords($sort_field);
4571 if ($sort_order != null)
4572 $this->sort_order = strtoupper($sort_order) == 'DESC' ? 'DESC' : 'ASC';
4577 * Sort mailboxes first by default folders and then in alphabethical order
4579 * @param array $a_folders Mailboxes list
4582 private function _sort_mailbox_list($a_folders)
4584 $a_out = $a_defaults = $folders = array();
4586 $delimiter = $this->get_hierarchy_delimiter();
4588 // find default folders and skip folders starting with '.'
4589 foreach ($a_folders as $i => $folder) {
4590 if ($folder[0] == '.')
4593 if (($p = array_search($folder, $this->default_folders)) !== false && !$a_defaults[$p])
4594 $a_defaults[$p] = $folder;
4596 $folders[$folder] = rcube_charset_convert($folder, 'UTF7-IMAP');
4599 // sort folders and place defaults on the top
4600 asort($folders, SORT_LOCALE_STRING);
4602 $folders = array_merge($a_defaults, array_keys($folders));
4604 // finally we must rebuild the list to move
4605 // subfolders of default folders to their place...
4606 // ...also do this for the rest of folders because
4607 // asort() is not properly sorting case sensitive names
4608 while (list($key, $folder) = each($folders)) {
4609 // set the type of folder name variable (#1485527)
4610 $a_out[] = (string) $folder;
4611 unset($folders[$key]);
4612 $this->_rsort($folder, $delimiter, $folders, $a_out);
4622 private function _rsort($folder, $delimiter, &$list, &$out)
4624 while (list($key, $name) = each($list)) {
4625 if (strpos($name, $folder.$delimiter) === 0) {
4626 // set the type of folder name variable (#1485527)
4627 $out[] = (string) $name;
4629 $this->_rsort($name, $delimiter, $list, $out);
4637 * @param int $uid Message UID
4638 * @param string $mailbox Mailbox name
4639 * @return int Message (sequence) ID
4642 private function _uid2id($uid, $mailbox=NULL)
4644 if (!strlen($mailbox)) {
4645 $mailbox = $this->mailbox;
4648 if (!isset($this->uid_id_map[$mailbox][$uid])) {
4649 if (!($id = $this->get_cache_uid2id($mailbox.'.msg', $uid)))
4650 $id = $this->conn->UID2ID($mailbox, $uid);
4652 $this->uid_id_map[$mailbox][$uid] = $id;
4655 return $this->uid_id_map[$mailbox][$uid];
4660 * @param int $id Message (sequence) ID
4661 * @param string $mailbox Mailbox name
4663 * @return int Message UID
4666 private function _id2uid($id, $mailbox=null)
4668 if (!strlen($mailbox)) {
4669 $mailbox = $this->mailbox;
4672 if ($uid = array_search($id, (array)$this->uid_id_map[$mailbox])) {
4676 if (!($uid = $this->get_cache_id2uid($mailbox.'.msg', $id))) {
4677 $uid = $this->conn->ID2UID($mailbox, $id);
4680 $this->uid_id_map[$mailbox][$uid] = $id;
4687 * Subscribe/unsubscribe a list of mailboxes and update local cache
4690 private function _change_subscription($a_mboxes, $mode)
4694 if (is_array($a_mboxes))
4695 foreach ($a_mboxes as $i => $mailbox) {
4696 $a_mboxes[$i] = $mailbox;
4698 if ($mode == 'subscribe')
4699 $updated = $this->conn->subscribe($mailbox);
4700 else if ($mode == 'unsubscribe')
4701 $updated = $this->conn->unsubscribe($mailbox);
4704 // clear cached mailbox list(s)
4706 $this->clear_cache('mailboxes', true);
4714 * Increde/decrese messagecount for a specific mailbox
4717 private function _set_messagecount($mailbox, $mode, $increment)
4719 $mode = strtoupper($mode);
4720 $a_mailbox_cache = $this->get_cache('messagecount');
4722 if (!is_array($a_mailbox_cache[$mailbox]) || !isset($a_mailbox_cache[$mailbox][$mode]) || !is_numeric($increment))
4725 // add incremental value to messagecount
4726 $a_mailbox_cache[$mailbox][$mode] += $increment;
4728 // there's something wrong, delete from cache
4729 if ($a_mailbox_cache[$mailbox][$mode] < 0)
4730 unset($a_mailbox_cache[$mailbox][$mode]);
4732 // write back to cache
4733 $this->update_cache('messagecount', $a_mailbox_cache);
4740 * Remove messagecount of a specific mailbox from cache
4743 private function _clear_messagecount($mailbox, $mode=null)
4745 $a_mailbox_cache = $this->get_cache('messagecount');
4747 if (is_array($a_mailbox_cache[$mailbox])) {
4749 unset($a_mailbox_cache[$mailbox][$mode]);
4752 unset($a_mailbox_cache[$mailbox]);
4754 $this->update_cache('messagecount', $a_mailbox_cache);
4760 * Split RFC822 header string into an associative array
4763 private function _parse_headers($headers)
4765 $a_headers = array();
4766 $headers = preg_replace('/\r?\n(\t| )+/', ' ', $headers);
4767 $lines = explode("\n", $headers);
4770 for ($i=0; $i<$c; $i++) {
4771 if ($p = strpos($lines[$i], ': ')) {
4772 $field = strtolower(substr($lines[$i], 0, $p));
4773 $value = trim(substr($lines[$i], $p+1));
4775 $a_headers[$field] = $value;
4786 private function _parse_address_list($str, $decode=true)
4788 // remove any newlines and carriage returns before
4789 $str = preg_replace('/\r?\n(\s|\t)?/', ' ', $str);
4791 // extract list items, remove comments
4792 $str = self::explode_header_string(',;', $str, true);
4795 // simplified regexp, supporting quoted local part
4796 $email_rx = '(\S+|("\s*(?:[^"\f\n\r\t\v\b\s]+\s*)+"))@\S+';
4798 foreach ($str as $key => $val) {
4803 if (preg_match('/(.*)<('.$email_rx.')>$/', $val, $m)) {
4805 $name = trim($m[1]);
4807 else if (preg_match('/^('.$email_rx.')$/', $val, $m)) {
4815 // dequote and/or decode name
4817 if ($name[0] == '"' && $name[strlen($name)-1] == '"') {
4818 $name = substr($name, 1, -1);
4819 $name = stripslashes($name);
4822 $name = $this->decode_header($name);
4826 if (!$address && $name) {
4831 $result[$key] = array('name' => $name, 'address' => $address);
4840 * Explodes header (e.g. address-list) string into array of strings
4841 * using specified separator characters with proper handling
4842 * of quoted-strings and comments (RFC2822)
4844 * @param string $separator String containing separator characters
4845 * @param string $str Header string
4846 * @param bool $remove_comments Enable to remove comments
4848 * @return array Header items
4850 static function explode_header_string($separator, $str, $remove_comments=false)
4852 $length = strlen($str);
4858 for ($i=0; $i<$length; $i++) {
4859 // we're inside a quoted string
4861 if ($str[$i] == '"') {
4864 else if ($str[$i] == '\\') {
4865 if ($comment <= 0) {
4871 // we're inside a comment string
4872 else if ($comment > 0) {
4873 if ($str[$i] == ')') {
4876 else if ($str[$i] == '(') {
4879 else if ($str[$i] == '\\') {
4884 // separator, add to result array
4885 else if (strpos($separator, $str[$i]) !== false) {
4892 // start of quoted string
4893 else if ($str[$i] == '"') {
4897 else if ($remove_comments && $str[$i] == '(') {
4901 if ($comment <= 0) {
4906 if ($out && $comment <= 0) {
4915 * This is our own debug handler for the IMAP connection
4918 public function debug_handler(&$imap, $message)
4920 write_log('imap', $message);
4923 } // end class rcube_imap
4927 * Class representing a message part
4931 class rcube_message_part
4934 var $ctype_primary = 'text';
4935 var $ctype_secondary = 'plain';
4936 var $mimetype = 'text/plain';
4937 var $disposition = '';
4939 var $encoding = '8bit';
4942 var $headers = array();
4943 var $d_parameters = array();
4944 var $ctype_parameters = array();
4948 if (isset($this->parts))
4949 foreach ($this->parts as $idx => $part)
4950 if (is_object($part))
4951 $this->parts[$idx] = clone $part;
4957 * Class for sorting an array of rcube_mail_header objects in a predetermined order.
4960 * @author Eric Stadtherr
4962 class rcube_header_sorter
4964 var $sequence_numbers = array();
4967 * Set the predetermined sort order.
4969 * @param array $seqnums Numerically indexed array of IMAP message sequence numbers
4971 function set_sequence_numbers($seqnums)
4973 $this->sequence_numbers = array_flip($seqnums);
4977 * Sort the array of header objects
4979 * @param array $headers Array of rcube_mail_header objects indexed by UID
4981 function sort_headers(&$headers)
4984 * uksort would work if the keys were the sequence number, but unfortunately
4985 * the keys are the UIDs. We'll use uasort instead and dereference the value
4986 * to get the sequence number (in the "id" field).
4988 * uksort($headers, array($this, "compare_seqnums"));
4990 uasort($headers, array($this, "compare_seqnums"));
4994 * Sort method called by uasort()
4996 * @param rcube_mail_header $a
4997 * @param rcube_mail_header $b
4999 function compare_seqnums($a, $b)
5001 // First get the sequence number from the header object (the 'id' field).
5005 // then find each sequence number in my ordered list
5006 $posa = isset($this->sequence_numbers[$seqa]) ? intval($this->sequence_numbers[$seqa]) : -1;
5007 $posb = isset($this->sequence_numbers[$seqb]) ? intval($this->sequence_numbers[$seqb]) : -1;
5009 // return the relative position as the comparison value
5010 return $posa - $posb;