From 66536974fe12a02ca5ffcec4354bf5113282a0cc Mon Sep 17 00:00:00 2001 From: Thomas Bruederli <thomas@roundcube.net> Date: Mon, 07 Apr 2014 03:53:46 -0400 Subject: [PATCH] Merge branch 'dev-multi-folder-search' --- program/lib/Roundcube/rcube_imap.php | 687 ++++++++++++++++++++++++++++++++++++++------------------- 1 files changed, 456 insertions(+), 231 deletions(-) diff --git a/program/lib/Roundcube/rcube_imap.php b/program/lib/Roundcube/rcube_imap.php index 9df2bf6..f60be62 100644 --- a/program/lib/Roundcube/rcube_imap.php +++ b/program/lib/Roundcube/rcube_imap.php @@ -2,8 +2,6 @@ /* +-----------------------------------------------------------------------+ - | program/include/rcube_imap.php | - | | | This file is part of the Roundcube Webmail client | | Copyright (C) 2005-2012, The Roundcube Dev Team | | Copyright (C) 2011-2012, Kolab Systems AG | @@ -14,13 +12,11 @@ | | | PURPOSE: | | IMAP Storage Engine | - | | +-----------------------------------------------------------------------+ | Author: Thomas Bruederli <roundcube@gmail.com> | | Author: Aleksander Machniak <alec@alec.pl> | +-----------------------------------------------------------------------+ */ - /** * Interface class for accessing an IMAP server @@ -74,7 +70,7 @@ protected $search_sort_field = ''; protected $search_threads = false; protected $search_sorted = false; - protected $options = array('auth_method' => 'check'); + protected $options = array('auth_type' => 'check'); protected $caching = false; protected $messages_caching = false; protected $threading = false; @@ -151,7 +147,7 @@ $attempt = 0; do { - $data = rcube::get_instance()->plugins->exec_hook('imap_connect', + $data = rcube::get_instance()->plugins->exec_hook('storage_connect', array_merge($this->options, array('host' => $host, 'user' => $user, 'attempt' => ++$attempt))); @@ -312,14 +308,7 @@ */ public function set_folder($folder) { - if ($this->folder == $folder) { - return; - } - $this->folder = $folder; - - // clear messagecount cache for this folder - $this->clear_messagecount($folder); } @@ -343,6 +332,10 @@ $this->search_sort_field = $set[3]; $this->search_sorted = $set[4]; $this->search_threads = is_a($this->search_set, 'rcube_result_thread'); + + if (is_a($this->search_set, 'rcube_result_multifolder')) { + $this->set_threading(false); + } } @@ -402,10 +395,10 @@ public function check_permflag($flag) { $flag = strtoupper($flag); - $imap_flag = $this->conn->flags[$flag]; $perm_flags = $this->get_permflags($this->folder); + $imap_flag = $this->conn->flags[$flag]; - return in_array_nocase($imap_flag, $perm_flags); + return $imap_flag && !empty($perm_flags) && in_array_nocase($imap_flag, $perm_flags); } @@ -421,17 +414,7 @@ if (!strlen($folder)) { return array(); } -/* - Checking PERMANENTFLAGS is rather rare, so we disable caching of it - Re-think when we'll use it for more than only MDNSENT flag - $cache_key = 'mailboxes.permanentflags.' . $folder; - $permflags = $this->get_cache($cache_key); - - if ($permflags !== null) { - return explode(' ', $permflags); - } -*/ if (!$this->check_connection()) { return array(); } @@ -446,10 +429,7 @@ if (!is_array($permflags)) { $permflags = array(); } -/* - // Store permflags as string to limit cached object size - $this->update_cache($cache_key, implode(' ', $permflags)); -*/ + return $permflags; } @@ -571,7 +551,7 @@ * Get message count for a specific folder * * @param string $folder Folder name - * @param string $mode Mode for count [ALL|THREADS|UNSEEN|RECENT] + * @param string $mode Mode for count [ALL|THREADS|UNSEEN|RECENT|EXISTS] * @param boolean $force Force reading from server and update cache * @param boolean $status Enables storing folder status info (max UID/count), * required for folder_status() @@ -592,7 +572,7 @@ * protected method for getting nr of messages * * @param string $folder Folder name - * @param string $mode Mode for count [ALL|THREADS|UNSEEN|RECENT] + * @param string $mode Mode for count [ALL|THREADS|UNSEEN|RECENT|EXISTS] * @param boolean $force Force reading from server and update cache * @param boolean $status Enables storing folder status info (max UID/count), * required for folder_status() @@ -614,6 +594,10 @@ } } + // EXISTS is a special alias for ALL, it allows to get the number + // of all messages in a folder also when search is active and with + // any skip_deleted setting + $a_folder_cache = $this->get_cache('messagecount'); // return cached value @@ -626,7 +610,7 @@ } if ($mode == 'THREADS') { - $res = $this->fetch_threads($folder, $force); + $res = $this->threads($folder); $count = $res->count(); if ($status) { @@ -644,7 +628,7 @@ $count = $this->conn->countRecent($folder); } // use SEARCH for message counting - else if (!empty($this->options['skip_deleted'])) { + else if ($mode != 'EXISTS' && !empty($this->options['skip_deleted'])) { $search_str = "ALL UNDELETED"; $keys = array('COUNT'); @@ -656,11 +640,11 @@ $keys[] = 'ALL'; } if ($status) { - $keys[] = 'MAX'; + $keys[] = 'MAX'; } } - // @TODO: if $force==false && $mode == 'ALL' we could try to use cache index here + // @TODO: if $mode == 'ALL' we could try to use cache index here // get message count using (E)SEARCH // not very performant but more precise (using UNDELETED) @@ -683,8 +667,8 @@ } else { $count = $this->conn->countMessages($folder); - if ($status) { - $this->set_folder_stats($folder,'cnt', $count); + if ($status && $mode == 'ALL') { + $this->set_folder_stats($folder, 'cnt', $count); $this->set_folder_stats($folder, 'maxuid', $count ? $this->id2uid($count, $folder) : 0); } } @@ -696,6 +680,41 @@ $this->update_cache('messagecount', $a_folder_cache); return (int)$count; + } + + + /** + * Public method for listing message flags + * + * @param string $folder Folder name + * @param array $uids Message UIDs + * @param int $mod_seq Optional MODSEQ value (of last flag update) + * + * @return array Indexed array with message flags + */ + public function list_flags($folder, $uids, $mod_seq = null) + { + if (!strlen($folder)) { + $folder = $this->folder; + } + + if (!$this->check_connection()) { + return array(); + } + + // @TODO: when cache was synchronized in this request + // we might already have asked for flag updates, use it. + + $flags = $this->conn->fetch($folder, $uids, true, array('FLAGS'), $mod_seq); + $result = array(); + + if (!empty($flags)) { + foreach ($flags as $message) { + $result[$message->uid] = $message->flags; + } + } + + return $result; } @@ -791,7 +810,7 @@ $threads = $mcache->get_thread($folder); } else { - $threads = $this->fetch_threads($folder); + $threads = $this->threads($folder); } return $this->fetch_thread_headers($folder, $threads, $page, $slice); @@ -800,32 +819,47 @@ /** * Method for fetching threads data * - * @param string $folder Folder name - * @param bool $force Use IMAP server, no cache + * @param string $folder Folder name * * @return rcube_imap_thread Thread data object */ - function fetch_threads($folder, $force = false) + function threads($folder) { - if (!$force && ($mcache = $this->get_mcache_engine())) { + if ($mcache = $this->get_mcache_engine()) { // don't store in self's internal cache, cache has it's own internal cache return $mcache->get_thread($folder); } - if (empty($this->icache['threads'])) { - if (!$this->check_connection()) { - return new rcube_result_thread(); + if (!empty($this->icache['threads'])) { + if ($this->icache['threads']->get_parameters('MAILBOX') == $folder) { + return $this->icache['threads']; } - - // get all threads - $result = $this->conn->thread($folder, $this->threading, - $this->options['skip_deleted'] ? 'UNDELETED' : '', true); - - // add to internal (fast) cache - $this->icache['threads'] = $result; } - return $this->icache['threads']; + // get all threads + $result = $this->threads_direct($folder); + + // add to internal (fast) cache + return $this->icache['threads'] = $result; + } + + + /** + * Method for direct fetching of threads data + * + * @param string $folder Folder name + * + * @return rcube_imap_thread Thread data object + */ + function threads_direct($folder) + { + if (!$this->check_connection()) { + return new rcube_result_thread(); + } + + // get all threads + return $this->conn->thread($folder, $this->threading, + $this->options['skip_deleted'] ? 'UNDELETED' : '', true); } @@ -915,6 +949,54 @@ return array(); } + // gather messages from a multi-folder search + if ($this->search_set->multi) { + $page_size = $this->page_size; + $sort_field = $this->sort_field; + $search_set = $this->search_set; + + $this->sort_field = null; + $this->page_size = 1000; // fetch up to 1000 matching messages per folder + $this->threading = false; + + $a_msg_headers = array(); + foreach ($search_set->sets as $resultset) { + if (!$resultset->is_empty()) { + $this->search_set = $resultset; + $this->search_threads = $resultset instanceof rcube_result_thread; + $a_msg_headers = array_merge($a_msg_headers, $this->list_search_messages($resultset->get_parameters('MAILBOX'), 1)); + } + } + + // do sorting and paging + $cnt = $search_set->count(); + $from = ($page-1) * $page_size; + $to = $from + $page_size; + + // sort headers + if (!$this->threading && !empty($a_msg_headers)) { + $a_msg_headers = $this->conn->sortHeaders($a_msg_headers, $sort_field, $this->sort_order); + } + + // store (sorted) message index + $search_set->set_message_index($a_msg_headers, $sort_field, $this->sort_order); + + // only return the requested part of the set + $slice_length = min($page_size, $cnt - $from); + $a_msg_headers = array_slice(array_values($a_msg_headers), $from, $slice_length); + + if ($slice) { + $a_msg_headers = array_slice($a_msg_headers, -$slice, $slice); + } + + // restore members + $this->sort_field = $sort_field; + $this->page_size = $page_size; + $this->search_set = $search_set; + + return $a_msg_headers; + } + // use saved messages from searching if ($this->threading) { return $this->list_search_thread_messages($folder, $page, $slice); @@ -981,7 +1063,7 @@ // use memory less expensive (and quick) method for big result set $index = clone $this->index('', $this->sort_field, $this->sort_order); // get messages uids for one page... - $index->slice($start_msg, min($cnt-$from, $this->page_size)); + $index->slice($from, min($cnt-$from, $this->page_size)); if ($slice) { $index->slice(-$slice, $slice); @@ -1081,6 +1163,7 @@ } foreach ($headers as $h) { + $h->folder = $folder; $a_msg_headers[$h->uid] = $h; } @@ -1096,16 +1179,17 @@ /** - * Returns current status of folder + * Returns current status of a folder (compared to the last time use) * * We compare the maximum UID to determine the number of * new messages because the RECENT flag is not reliable. * * @param string $folder Folder name + * @param array $diff Difference data * - * @return int Folder status + * @return int Folder status */ - public function folder_status($folder = null) + public function folder_status($folder = null, &$diff = array()) { if (!strlen($folder)) { $folder = $this->folder; @@ -1126,6 +1210,9 @@ // got new messages if ($new['maxuid'] > $old['maxuid']) { $result += 1; + // get new message UIDs range, that can be used for example + // to get the data of these messages + $diff['new'] = ($old['maxuid'] + 1 < $new['maxuid'] ? ($old['maxuid']+1).':' : '') . $new['maxuid']; } // some messages has been deleted if ($new['cnt'] < $old['cnt']) { @@ -1176,12 +1263,15 @@ * @param string $folder Folder to get index from * @param string $sort_field Sort column * @param string $sort_order Sort order [ASC, DESC] + * @param bool $no_threads Get not threaded index + * @param bool $no_search Get index not limited to search result (optionally) * * @return rcube_result_index|rcube_result_thread List of messages (UIDs) */ - public function index($folder = '', $sort_field = NULL, $sort_order = NULL) - { - if ($this->threading) { + public function index($folder = '', $sort_field = NULL, $sort_order = NULL, + $no_threads = false, $no_search = false + ) { + if (!$no_threads && $this->threading) { return $this->thread_index($folder, $sort_field, $sort_order); } @@ -1193,43 +1283,55 @@ // we have a saved search result, get index from there if ($this->search_string) { - if ($this->search_threads) { - $this->search($folder, $this->search_string, $this->search_charset, $this->sort_field); + if ($this->search_set->is_empty()) { + return new rcube_result_index($folder, '* SORT'); } - // use message index sort as default sorting - if (!$this->sort_field || $this->search_sorted) { - if ($this->sort_field && $this->search_sort_field != $this->sort_field) { - $this->search($folder, $this->search_string, $this->search_charset, $this->sort_field); - } + if ($this->search_set instanceof rcube_result_multifolder) { + $index = $this->search_set; + $index->folder = $folder; + // TODO: handle changed sorting + } + // search result is an index with the same sorting? + else if (($this->search_set instanceof rcube_result_index) + && ((!$this->sort_field && !$this->search_sorted) || + ($this->search_sorted && $this->search_sort_field == $this->sort_field)) + ) { $index = $this->search_set; } - else if (!$this->check_connection()) { - return new rcube_result_index(); - } - else { - $index = $this->conn->index($folder, $this->search_set->get(), - $this->sort_field, $this->options['skip_deleted'], true, true); + // $no_search is enabled when we are not interested in + // fetching index for search result, e.g. to sort + // threaded search result we can use full mailbox index. + // This makes possible to use index from cache + else if (!$no_search) { + if (!$this->sort_field) { + // No sorting needed, just build index from the search result + // @TODO: do we need to sort by UID here? + $search = $this->search_set->get_compressed(); + $index = new rcube_result_index($folder, '* ESEARCH ALL ' . $search); + } + else { + $index = $this->index_direct($folder, $this->search_charset, + $this->sort_field, $this->search_set); + } } - if ($this->sort_order != $index->get_parameters('ORDER')) { - $index->revert(); - } + if (isset($index)) { + if ($this->sort_order != $index->get_parameters('ORDER')) { + $index->revert(); + } - return $index; + return $index; + } } // check local cache if ($mcache = $this->get_mcache_engine()) { - $index = $mcache->get_index($folder, $this->sort_field, $this->sort_order); - } - // fetch from IMAP server - else { - $index = $this->index_direct( - $folder, $this->sort_field, $this->sort_order); + return $mcache->get_index($folder, $this->sort_field, $this->sort_order); } - return $index; + // fetch from IMAP server + return $this->index_direct($folder, $this->sort_field, $this->sort_order); } @@ -1237,22 +1339,24 @@ * Return sorted list of message UIDs ignoring current search settings. * Doesn't uses cache by default. * - * @param string $folder Folder to get index from - * @param string $sort_field Sort column - * @param string $sort_order Sort order [ASC, DESC] - * @param bool $skip_cache Disables cache usage + * @param string $folder Folder to get index from + * @param string $sort_field Sort column + * @param string $sort_order Sort order [ASC, DESC] + * @param rcube_result_* $search Optional messages set to limit the result * * @return rcube_result_index Sorted list of message UIDs */ - public function index_direct($folder, $sort_field = null, $sort_order = null, $skip_cache = true) + public function index_direct($folder, $sort_field = null, $sort_order = null, $search = null) { - if (!$skip_cache && ($mcache = $this->get_mcache_engine())) { - $index = $mcache->get_index($folder, $sort_field, $sort_order); + if (!empty($search)) { + $search = $this->search_set->get_compressed(); } + // use message index sort as default sorting - else if (!$sort_field) { + if (!$sort_field) { // use search result from count() if possible - if ($this->options['skip_deleted'] && !empty($this->icache['undeleted_idx']) + if (empty($search) && $this->options['skip_deleted'] + && !empty($this->icache['undeleted_idx']) && $this->icache['undeleted_idx']->get_parameters('ALL') !== null && $this->icache['undeleted_idx']->get_parameters('MAILBOX') == $folder ) { @@ -1262,8 +1366,12 @@ return new rcube_result_index(); } else { - $index = $this->conn->search($folder, - 'ALL' .($this->options['skip_deleted'] ? ' UNDELETED' : ''), true); + $query = $this->options['skip_deleted'] ? 'UNDELETED' : ''; + if ($search) { + $query = trim($query . ' UID ' . $search); + } + + $index = $this->conn->search($folder, $query, true); } } else if (!$this->check_connection()) { @@ -1272,13 +1380,18 @@ // fetch complete message index else { if ($this->get_capability('SORT')) { - $index = $this->conn->sort($folder, $sort_field, - $this->options['skip_deleted'] ? 'UNDELETED' : '', true); + $query = $this->options['skip_deleted'] ? 'UNDELETED' : ''; + if ($search) { + $query = trim($query . ' UID ' . $search); + } + + $index = $this->conn->sort($folder, $sort_field, $query, true); } if (empty($index) || $index->is_error()) { - $index = $this->conn->index($folder, "1:*", $sort_field, - $this->options['skip_deleted'], false, true); + $index = $this->conn->index($folder, $search ? $search : "1:*", + $sort_field, $this->options['skip_deleted'], + $search ? true : false, true); } } @@ -1311,7 +1424,7 @@ } else { // get all threads (default sort order) - $threads = $this->fetch_threads($folder); + $threads = $this->threads($folder); } $this->set_sort_order($sort_field, $sort_order); @@ -1322,9 +1435,10 @@ /** - * Sort threaded result, using THREAD=REFS method + * Sort threaded result, using THREAD=REFS method if available. + * If not, use any method and re-sort the result in THREAD=REFS way. * - * @param rcube_result_thread $threads Threads result set + * @param rcube_result_thread $threads Threads result set */ protected function sort_threads($threads) { @@ -1336,17 +1450,16 @@ // THREAD=REFERENCES: sorting by sent date of root message // THREAD=REFS: sorting by the most recent date in each thread - if ($this->sort_field && ($this->sort_field != 'date' || $this->get_capability('THREAD') != 'REFS')) { - $index = $this->index_direct($this->folder, $this->sort_field, $this->sort_order, false); + if ($this->threading != 'REFS' || ($this->sort_field && $this->sort_field != 'date')) { + $sortby = $this->sort_field ? $this->sort_field : 'date'; + $index = $this->index($this->folder, $sortby, $this->sort_order, true, true); if (!$index->is_empty()) { $threads->sort($index); } } - else { - if ($this->sort_order != $threads->get_parameters('ORDER')) { - $threads->revert(); - } + else if ($this->sort_order != $threads->get_parameters('ORDER')) { + $threads->revert(); } } @@ -1367,11 +1480,34 @@ $str = 'ALL'; } - if (!strlen($folder)) { + if (empty($folder)) { $folder = $this->folder; } - $results = $this->search_index($folder, $str, $charset, $sort_field); + // multi-folder search + if (is_array($folder) && count($folder) > 1 && $str != 'ALL') { + new rcube_result_index; // trigger autoloader and make these classes available for threaded context + new rcube_result_thread; + + // connect IMAP to have all the required classes and settings loaded + $this->check_connection(); + + // disable threading + $this->threading = false; + + $searcher = new rcube_imap_search($this->options, $this->conn); + $results = $searcher->exec( + $folder, + $str, + $charset ? $charset : $this->default_charset, + $sort_field && $this->get_capability('SORT') ? $sort_field : null, + $this->threading + ); + } + else { + $folder = is_array($folder) ? $folder[0] : $folder; + $results = $this->search_index($folder, $str, $charset, $sort_field); + } $this->set_search_set(array($str, $results, $charset, $sort_field, $this->threading || $this->search_sorted ? true : false)); @@ -1389,7 +1525,7 @@ public function search_once($folder = null, $str = 'ALL') { if (!$str) { - return 'ALL'; + $str = 'ALL'; } if (!strlen($folder)) { @@ -1419,8 +1555,6 @@ */ protected function search_index($folder, $criteria='ALL', $charset=NULL, $sort_field=NULL) { - $orig_criteria = $criteria; - if (!$this->check_connection()) { if ($this->threading) { return new rcube_result_thread(); @@ -1447,7 +1581,7 @@ // but I've seen that Courier doesn't support UTF-8) if ($threads->is_error() && $charset && $charset != 'US-ASCII') { $threads = $this->conn->thread($folder, $this->threading, - $this->convert_criteria($criteria, $charset), true, 'US-ASCII'); + self::convert_criteria($criteria, $charset), true, 'US-ASCII'); } return $threads; @@ -1461,7 +1595,7 @@ // but I've seen Courier with disabled UTF-8 support) if ($messages->is_error() && $charset && $charset != 'US-ASCII') { $messages = $this->conn->sort($folder, $sort_field, - $this->convert_criteria($criteria, $charset), true, 'US-ASCII'); + self::convert_criteria($criteria, $charset), true, 'US-ASCII'); } if (!$messages->is_error()) { @@ -1476,7 +1610,7 @@ // Error, try with US-ASCII (some servers may support only US-ASCII) if ($messages->is_error() && $charset && $charset != 'US-ASCII') { $messages = $this->conn->search($folder, - $this->convert_criteria($criteria, $charset), true); + self::convert_criteria($criteria, $charset), true); } $this->search_sorted = false; @@ -1494,7 +1628,7 @@ * * @return string Search string */ - protected function convert_criteria($str, $charset, $dest_charset='US-ASCII') + public static function convert_criteria($str, $charset, $dest_charset='US-ASCII') { // convert strings to US_ASCII if (preg_match_all('/\{([0-9]+)\}\r\n/', $str, $matches, PREG_OFFSET_CAPTURE)) { @@ -1530,6 +1664,7 @@ public function refresh_search() { if (!empty($this->search_string)) { + // FIXME: make this work with saved multi-folder searches $this->search('', $this->search_string, $this->search_charset, $this->search_sort_field); } @@ -1550,6 +1685,11 @@ { if (!strlen($folder)) { $folder = $this->folder; + } + + // decode combined UID-folder identifier + if (preg_match('/^\d+-[^,]+$/', $uid)) { + list($uid, $folder) = explode('-', $uid); } // get cached headers @@ -1581,6 +1721,11 @@ { if (!strlen($folder)) { $folder = $this->folder; + } + + // decode combined UID-folder identifier + if (preg_match('/^\d+-[^,]+$/', $uid)) { + list($uid, $folder) = explode('-', $uid); } // Check internal cache @@ -1634,9 +1779,15 @@ // Example of structure for malformed MIME message: // ("text" "plain" NIL NIL NIL "7bit" 2154 70 NIL NIL NIL) if ($headers->ctype && !is_array($structure[0]) && $headers->ctype != 'text/plain' - && strtolower($structure[0].'/'.$structure[1]) == 'text/plain') { + && strtolower($structure[0].'/'.$structure[1]) == 'text/plain' + ) { + // A special known case "Content-type: text" (#1488968) + if ($headers->ctype == 'text') { + $structure[1] = 'plain'; + $headers->ctype = 'text/plain'; + } // we can handle single-part messages, by simple fix in structure (#1486898) - if (preg_match('/^(text|application)\/(.*)/', $headers->ctype, $m)) { + else if (preg_match('/^(text|application)\/(.*)/', $headers->ctype, $m)) { $structure[0] = $m[1]; $structure[1] = $m[2]; } @@ -1660,11 +1811,21 @@ $struct = $this->structure_part($structure, 0, '', $headers); } - // don't trust given content-type - if (empty($struct->parts) && !empty($headers->ctype)) { - $struct->mime_id = '1'; - $struct->mimetype = strtolower($headers->ctype); - list($struct->ctype_primary, $struct->ctype_secondary) = explode('/', $struct->mimetype); + // some workarounds on simple messages... + if (empty($struct->parts)) { + // ...don't trust given content-type + if (!empty($headers->ctype)) { + $struct->mime_id = '1'; + $struct->mimetype = strtolower($headers->ctype); + list($struct->ctype_primary, $struct->ctype_secondary) = explode('/', $struct->mimetype); + } + + // ...and charset (there's a case described in #1488968 where invalid content-type + // results in invalid charset in BODYSTRUCTURE) + if (!empty($headers->charset) && $headers->charset != $struct->ctype_parameters['charset']) { + $struct->charset = $headers->charset; + $struct->ctype_parameters['charset'] = $headers->charset; + } } $headers->structure = $struct; @@ -2045,16 +2206,18 @@ /** * Fetch message body of a specific message from the server * - * @param int $uid Message UID - * @param string $part Part number - * @param rcube_message_part $o_part Part object created by get_structure() - * @param mixed $print True to print part, ressource to write part contents in - * @param resource $fp File pointer to save the message part - * @param boolean $skip_charset_conv Disables charset conversion + * @param int Message UID + * @param string Part number + * @param rcube_message_part Part object created by get_structure() + * @param mixed True to print part, resource to write part contents in + * @param resource File pointer to save the message part + * @param boolean Disables charset conversion + * @param int Only read this number of bytes + * @param boolean Enables formatting of text/* parts bodies * * @return string Message/part body if not printed */ - public function get_message_part($uid, $part=1, $o_part=NULL, $print=NULL, $fp=NULL, $skip_charset_conv=false) + public function get_message_part($uid, $part=1, $o_part=NULL, $print=NULL, $fp=NULL, $skip_charset_conv=false, $max_bytes=0, $formatted=true) { if (!$this->check_connection()) { return null; @@ -2073,8 +2236,9 @@ } if ($o_part && $o_part->size) { + $formatted = $formatted && $o_part->ctype_primary == 'text'; $body = $this->conn->handlePartBody($this->folder, $uid, true, - $part ? $part : 'TEXT', $o_part->encoding, $print, $fp, $o_part->ctype_primary == 'text'); + $part ? $part : 'TEXT', $o_part->encoding, $print, $fp, $formatted, $max_bytes); } if ($fp || $print) { @@ -2084,7 +2248,7 @@ // convert charset (if text or message part) if ($body && preg_match('/^(text|message)$/', $o_part->ctype_primary)) { // Remove NULL characters if any (#1486189) - if (strpos($body, "\x00") !== false) { + if ($formatted && strpos($body, "\x00") !== false) { $body = str_replace("\x00", '', $body); } @@ -2219,16 +2383,18 @@ /** * Append a mail message (source) to a specific folder * - * @param string $folder Target folder - * @param string $message The message source string or filename - * @param string $headers Headers string if $message contains only the body - * @param boolean $is_file True if $message is a filename - * @param array $flags Message flags - * @param mixed $date Message internal date + * @param string $folder Target folder + * @param string|array $message The message source string or filename + * or array (of strings and file pointers) + * @param string $headers Headers string if $message contains only the body + * @param boolean $is_file True if $message is a filename + * @param array $flags Message flags + * @param mixed $date Message internal date + * @param bool $binary Enables BINARY append * * @return int|bool Appended message UID or True on success, False on error */ - public function save_message($folder, &$message, $headers='', $is_file=false, $flags = array(), $date = null) + public function save_message($folder, &$message, $headers='', $is_file=false, $flags = array(), $date = null, $binary = false) { if (!strlen($folder)) { $folder = $this->folder; @@ -2246,10 +2412,10 @@ $date = $this->date_format($date); if ($is_file) { - $saved = $this->conn->appendFromFile($folder, $message, $headers, $flags, $date); + $saved = $this->conn->appendFromFile($folder, $message, $headers, $flags, $date, $binary); } else { - $saved = $this->conn->append($folder, $message, $flags, $date); + $saved = $this->conn->append($folder, $message, $flags, $date, $binary); } if ($saved) { @@ -2291,19 +2457,7 @@ return false; } - // make sure folder exists - if ($to_mbox != 'INBOX' && !$this->folder_exists($to_mbox)) { - if (in_array($to_mbox, $this->default_folders)) { - if (!$this->create_folder($to_mbox, true)) { - return false; - } - } - else { - return false; - } - } - - $config = rcube::get_instance()->config; + $config = rcube::get_instance()->config; $to_trash = $to_mbox == $config->get('trash_mbox'); // flag messages as read before moving them @@ -2315,10 +2469,7 @@ // move messages $moved = $this->conn->move($uids, $from_mbox, $to_mbox); - // send expunge command in order to have the moved message - // really deleted from the source folder if ($moved) { - $this->expunge_message($uids, $from_mbox, false); $this->clear_messagecount($from_mbox); $this->clear_messagecount($to_mbox); } @@ -2338,7 +2489,7 @@ $this->refresh_search(); } else { - $this->search_set->filter(explode(',', $uids)); + $this->search_set->filter(explode(',', $uids), $this->folder); } } @@ -2375,18 +2526,6 @@ if (!$this->check_connection()) { return false; - } - - // make sure folder exists - if ($to_mbox != 'INBOX' && !$this->folder_exists($to_mbox)) { - if (in_array($to_mbox, $this->default_folders)) { - if (!$this->create_folder($to_mbox, true)) { - return false; - } - } - else { - return false; - } } // copy messages @@ -2620,7 +2759,6 @@ if ($list_extended) { // unsubscribe non-existent folders, remove from the list - // we can do this only when LIST response is available if (is_array($a_folders) && $name == '*' && !empty($this->conn->data['LIST'])) { foreach ($a_folders as $idx => $folder) { if (($opts = $this->conn->data['LIST'][$folder]) @@ -2633,19 +2771,14 @@ } } else { - // unsubscribe non-existent folders, remove them from the list, - // we can do this only when LIST response is available - if (is_array($a_folders) && $name == '*' && !empty($this->conn->data['LIST'])) { - foreach ($a_folders as $idx => $folder) { - if (!isset($this->conn->data['LIST'][$folder]) - || in_array('\\Noselect', $this->conn->data['LIST'][$folder]) - ) { - // Some servers returns \Noselect for existing folders - if (!$this->folder_exists($folder)) { - $this->conn->unsubscribe($folder); - unset($a_folders[$idx]); - } - } + // unsubscribe non-existent folders, remove them from the list + if (is_array($a_folders) && !empty($a_folders) && $name == '*') { + $existing = $this->list_folders($root, $name); + $nonexisting = array_diff($a_folders, $existing); + $a_folders = array_diff($a_folders, $nonexisting); + + foreach ($nonexisting as $folder) { + $this->conn->unsubscribe($folder); } } } @@ -2708,7 +2841,7 @@ // filter folders list according to rights requirements if ($rights && $this->get_capability('ACL')) { - $a_folders = $this->filter_rights($a_folders, $rights); + $a_mboxes = $this->filter_rights($a_mboxes, $rights); } // filter folders and sort them @@ -2764,7 +2897,6 @@ */ private function list_folders_update(&$result, $type = null) { - $delim = $this->get_hierarchy_delimiter(); $namespace = $this->get_namespace(); $search = array(); @@ -2814,12 +2946,21 @@ /** * Filter the given list of folders according to access rights + * + * For performance reasons we assume user has full rights + * on all personal folders. */ protected function filter_rights($a_folders, $rights) { $regex = '/('.$rights.')/'; + foreach ($a_folders as $idx => $folder) { + if ($this->folder_namespace($folder) == 'personal') { + continue; + } + $myrights = join('', (array)$this->my_rights($folder)); + if ($myrights !== null && !preg_match($regex, $myrights)) { unset($a_folders[$idx]); } @@ -2902,16 +3043,17 @@ * * @param string $folder New folder name * @param boolean $subscribe True if the new folder should be subscribed + * @param string $type Optional folder type (junk, trash, drafts, sent, archive) * * @return boolean True on success */ - public function create_folder($folder, $subscribe=false) + public function create_folder($folder, $subscribe = false, $type = null) { if (!$this->check_connection()) { return false; } - $result = $this->conn->createFolder($folder); + $result = $this->conn->createFolder($folder, $type ? array("\\" . ucfirst($type)) : null); // try to subscribe it if ($result) { @@ -3036,19 +3178,84 @@ /** - * Create all folders specified as default + * Detect special folder associations stored in storage backend */ - public function create_default_folders() + public function get_special_folders($forced = false) { - // create default folders if they do not exist - foreach ($this->default_folders as $folder) { - if (!$this->folder_exists($folder)) { - $this->create_folder($folder, true); - } - else if (!$this->folder_exists($folder, true)) { - $this->subscribe($folder); + $result = parent::get_special_folders(); + + if (isset($this->icache['special-use'])) { + return array_merge($result, $this->icache['special-use']); + } + + if (!$forced || !$this->get_capability('SPECIAL-USE')) { + return $result; + } + + if (!$this->check_connection()) { + return $result; + } + + $types = array_map(function($value) { return "\\" . ucfirst($value); }, rcube_storage::$folder_types); + $special = array(); + + // request \Subscribed flag in LIST response as performance improvement for folder_exists() + $folders = $this->conn->listMailboxes('', '*', array('SUBSCRIBED'), array('SPECIAL-USE')); + + foreach ($folders as $folder) { + if ($flags = $this->conn->data['LIST'][$folder]) { + foreach ($types as $type) { + if (in_array($type, $flags)) { + $type = strtolower(substr($type, 1)); + $special[$type] = $folder; + } + } } } + + $this->icache['special-use'] = $special; + unset($this->icache['special-folders']); + + return array_merge($result, $special); + } + + + /** + * Set special folder associations stored in storage backend + */ + public function set_special_folders($specials) + { + if (!$this->get_capability('SPECIAL-USE') || !$this->get_capability('METADATA')) { + return false; + } + + if (!$this->check_connection()) { + return false; + } + + $folders = $this->get_special_folders(true); + $old = (array) $this->icache['special-use']; + + foreach ($specials as $type => $folder) { + if (in_array($type, rcube_storage::$folder_types)) { + $old_folder = $old[$type]; + if ($old_folder !== $folder) { + // unset old-folder metadata + if ($old_folder !== null) { + $this->delete_metadata($old_folder, array('/private/specialuse')); + } + // set new folder metadata + if ($folder) { + $this->set_metadata($folder, array('/private/specialuse' => "\\" . ucfirst($type))); + } + } + } + } + + $this->icache['special-use'] = $specials; + unset($this->icache['special-folders']); + + return true; } @@ -3060,13 +3267,13 @@ * * @return boolean TRUE or FALSE */ - public function folder_exists($folder, $subscription=false) + public function folder_exists($folder, $subscription = false) { if ($folder == 'INBOX') { return true; } - $key = $subscription ? 'subscribed' : 'existing'; + $key = $subscription ? 'subscribed' : 'existing'; if (is_array($this->icache[$key]) && in_array($folder, $this->icache[$key])) { return true; @@ -3077,10 +3284,24 @@ } if ($subscription) { - $a_folders = $this->conn->listSubscribed('', $folder); + // It's possible we already called LIST command, check LIST data + if (!empty($this->conn->data['LIST']) && !empty($this->conn->data['LIST'][$folder]) + && in_array('\\Subscribed', $this->conn->data['LIST'][$folder]) + ) { + $a_folders = array($folder); + } + else { + $a_folders = $this->conn->listSubscribed('', $folder); + } } else { - $a_folders = $this->conn->listMailboxes('', $folder); + // It's possible we already called LIST command, check LIST data + if (!empty($this->conn->data['LIST']) && isset($this->conn->data['LIST'][$folder])) { + $a_folders = array($folder); + } + else { + $a_folders = $this->conn->listMailboxes('', $folder); + } } if (is_array($a_folders) && in_array($folder, $a_folders)) { @@ -3291,7 +3512,7 @@ $options['name'] = $folder; $options['attributes'] = $this->folder_attributes($folder, true); $options['namespace'] = $this->folder_namespace($folder); - $options['special'] = in_array($folder, $this->default_folders); + $options['special'] = $this->is_special_folder($folder); // Set 'noselect' flag if (is_array($options['attributes'])) { @@ -3353,7 +3574,6 @@ { if (!empty($this->options['fetch_headers'])) { $headers = explode(' ', $this->options['fetch_headers']); - $headers = array_map('strtoupper', $headers); } else { $headers = array(); @@ -3363,7 +3583,7 @@ $headers = array_merge($headers, $this->all_headers); } - return implode(' ', array_unique($headers)); + return $headers; } @@ -3676,7 +3896,7 @@ { if ($this->caching && !$this->cache) { $rcube = rcube::get_instance(); - $ttl = $rcube->config->get('message_cache_lifetime', '10d'); + $ttl = $rcube->config->get('imap_cache_ttl', '10d'); $this->cache = $rcube->get_cache('IMAP', $this->caching, $ttl); } @@ -3724,21 +3944,6 @@ } } - /** - * Delete outdated cache entries - */ - public function expunge_cache() - { - if ($this->mcache) { - $ttl = rcube::get_instance()->config->get('message_cache_lifetime', '10d'); - $this->mcache->expunge($ttl); - } - - if ($this->cache) { - $this->cache->expunge(); - } - } - /* -------------------------------- * message caching methods @@ -3747,12 +3952,17 @@ /** * Enable or disable messages caching * - * @param boolean $set Flag + * @param boolean $set Flag + * @param int $mode Cache mode */ - public function set_messages_caching($set) + public function set_messages_caching($set, $mode = null) { if ($set) { $this->messages_caching = true; + + if ($mode && ($cache = $this->get_mcache_engine())) { + $cache->set_mode($mode); + } } else { if ($this->mcache) { @@ -3772,8 +3982,10 @@ if ($this->messages_caching && !$this->mcache) { $rcube = rcube::get_instance(); if (($dbh = $rcube->get_dbh()) && ($userid = $rcube->get_user_id())) { + $ttl = $rcube->config->get('messages_cache_ttl', '10d'); + $threshold = $rcube->config->get('messages_cache_threshold', 50); $this->mcache = new rcube_imap_cache( - $dbh, $this, $userid, $this->options['skip_deleted']); + $dbh, $this, $userid, $this->options['skip_deleted'], $ttl, $threshold); } } @@ -3785,13 +3997,22 @@ * Clears the messages cache. * * @param string $folder Folder name - * @param array $uids Optional message UIDs to remove from cache + * @param array $uids Optional message UIDs to remove from cache */ protected function clear_message_cache($folder = null, $uids = null) { if ($mcache = $this->get_mcache_engine()) { $mcache->clear($folder, $uids); } + } + + + /** + * Delete outdated cache entries + */ + function cache_gc() + { + rcube_imap_cache::gc(); } @@ -3819,21 +4040,25 @@ /** * Sort folders first by default folders and then in alphabethical order * - * @param array $a_folders Folders list + * @param array $a_folders Folders list + * @param bool $skip_default Skip default folders handling + * + * @return array Sorted list */ - protected function sort_folder_list($a_folders) + public function sort_folder_list($a_folders, $skip_default = false) { $a_out = $a_defaults = $folders = array(); $delimiter = $this->get_hierarchy_delimiter(); + $specials = array_merge(array('INBOX'), array_values($this->get_special_folders())); // find default folders and skip folders starting with '.' - foreach ($a_folders as $i => $folder) { + foreach ($a_folders as $folder) { if ($folder[0] == '.') { continue; } - if (($p = array_search($folder, $this->default_folders)) !== false && !$a_defaults[$p]) { + if (!$skip_default && ($p = array_search($folder, $specials)) !== false && !$a_defaults[$p]) { $a_defaults[$p] = $folder; } else { @@ -4088,9 +4313,9 @@ return $this->index($folder, $sort_field, $sort_order); } - public function message_index_direct($folder, $sort_field = null, $sort_order = null, $skip_cache = true) + public function message_index_direct($folder, $sort_field = null, $sort_order = null) { - return $this->index_direct($folder, $sort_field, $sort_order, $skip_cache); + return $this->index_direct($folder, $sort_field, $sort_order); } public function list_mailboxes($root='', $name='*', $filter=null, $rights=null, $skip_sort=false) -- Gitblit v1.9.1